Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 31 additions & 2 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -181,12 +181,15 @@ enum Command {
json: bool,
},
/// Set a secret. Reads the value from a hidden prompt unless --value/--stdin is given.
#[command(
after_help = "Input modes:\n Without --value or --stdin, enter the value at the hidden prompt.\n --stdin reads until EOF and preserves every byte, including line endings and trailing newlines. It does not trim or interpolate input. A redirected file becomes one complete secret value, not a dotenv import.\n --value and --stdin cannot be combined. Real credentials passed with --value may appear in shell history and process arguments.\n\nBefore piping input:\n Initialise the local store and project, and unlock the local identity first. --stdin replaces only the value prompt; it does not bypass these requirements or grant write access through a machine token.\n\nExamples (POSIX shell):\n sotto set DATABASE_URL\n sotto set PRIVATE_KEY --stdin < ./example-key.txt\n printf '%s' 'example-value' | sotto set EXAMPLE --stdin\n\nThe pipeline uses a synthetic value. printf avoids the extra newline commonly added by echo. Do not put real credentials in an inline shell example: they can still enter shell history."
)]
Set {
name: String,
/// Provide the value inline (warning: visible in shell history).
/// Provide the value inline (warning: visible in shell history and process arguments).
#[arg(long, conflicts_with = "stdin")]
value: Option<String>,
/// Read the value from standard input.
/// Read the complete value from standard input until EOF, without trimming.
#[arg(long)]
stdin: bool,
},
Expand Down Expand Up @@ -2722,6 +2725,32 @@ mod tests {
assert!(help.contains("Sotto options go before --"));
}

#[test]
fn set_help_explains_prompt_and_stdin_input() {
let mut command = Cli::command();
let help = command
.find_subcommand_mut("set")
.expect("set subcommand should exist")
.render_long_help()
.to_string();

for expected in [
"sotto set DATABASE_URL",
"sotto set PRIVATE_KEY --stdin < ./example-key.txt",
"printf '%s' 'example-value' | sotto set EXAMPLE --stdin",
"reads until EOF",
"trailing newlines",
"does not trim or interpolate",
"not a dotenv import",
"--value and --stdin cannot be combined",
"shell history and process arguments",
"unlock the local identity first",
"synthetic value",
] {
assert!(help.contains(expected), "missing {expected:?} in:\n{help}");
}
}

#[test]
fn import_help_explains_literal_and_quoted_values() {
let mut command = Cli::command();
Expand Down
Loading