From 6bc95f7c698b9bd0762aba389a0e1934ef20b220 Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Thu, 15 Oct 2026 05:31:57 +0200 Subject: [PATCH] README says secret add drops one trailing newline and import keeps it (closes #158) `secret add` drops one trailing newline from stdin, so `echo value | secret add name` stores `value`. `secret import` stores a file's bytes unchanged, and `secret get` writes the value with no newline added. The README said none of this, so a PEM key piped into `add` came back one byte short with no hint why. Its entries for `add`, `get` and `import` now state each rule. A new test stores `a\n\n` through `add` and through `import` and checks that `add` kept two bytes and `import` three. No program code changes. The new test passes on the code before this change; it fails when `add` keeps the newline. Model: opus-5-5 --- README.md | 22 ++++++++----- internal/cli/trailing_newline_test.go | 47 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 internal/cli/trailing_newline_test.go diff --git a/README.md b/README.md index 63d0120..38abcb3 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,11 @@ value may be over 100 MiB, whatever the limit. #### `secret add [--force]` -Adds a secret to the current vault. Reads the secret value from stdin. +Adds a secret to the current vault. Reads the secret value from stdin. When the +value ends in a newline, that one newline is dropped, so +`echo value | secret add name` stores `value`; from `printf 'a\n\n'` it stores +`a` and one newline. To keep a file's final newline, as in a PEM key, use +`secret import`, which stores the file's bytes unchanged. - `--force, -f`: Overwrite existing secret @@ -155,13 +159,13 @@ contain `//`, or have `..` as a path segment. #### `secret get [--version ]` -Retrieves and outputs a secret value to stdout. A failed write of the value, -such as to a file on a full disk, prints the error and exits 1, with two -exceptions. If the program reading from a pipe has already exited, as in -`secret get s | true`, the broken-pipe signal (SIGPIPE) ends `secret` with no -message and the shell reports exit status 141. If the shell closed stdout before -`secret` started (`secret get s >&-`), the value is written to `/dev/null` and -`secret get` exits 0. +Writes the secret's value to stdout exactly as stored, with no newline added +after it. A failed write of the value, such as to a file on a full disk, prints +the error and exits 1, with two exceptions. If the program reading from a pipe +has already exited, as in `secret get s | true`, the broken-pipe signal +(SIGPIPE) ends `secret` with no message and the shell reports exit status 141. +If the shell closed stdout before `secret` started (`secret get s >&-`), the +value is written to `/dev/null` and `secret get` exits 0. - `--version, -v`: Get a specific version (default: current) @@ -300,7 +304,7 @@ Selects an unlocker as the current default for operations. #### `secret import --source ` Imports a secret from a file and stores it in the current vault under the given -name. +name. The file's bytes are stored unchanged, a final newline included. ### Encryption Operations diff --git a/internal/cli/trailing_newline_test.go b/internal/cli/trailing_newline_test.go new file mode 100644 index 0000000..ba1b5ab --- /dev/null +++ b/internal/cli/trailing_newline_test.go @@ -0,0 +1,47 @@ +//nolint:testpackage // white-box test of unexported internals +package cli + +import ( + "bytes" + "testing" + + "github.com/spf13/afero" + "github.com/spf13/cobra" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestAddDropsOneTrailingNewlineImportKeepsIt pins the rule the README +// gives: `secret add` drops one trailing newline from stdin, and +// `secret import` stores the file's bytes unchanged. +func TestAddDropsOneTrailingNewlineImportKeepsIt(t *testing.T) { + t.Parallel() + + fs, vlt := newSizeTestVault(t) + value := []byte("a\n\n") + + cmd := &cobra.Command{} + cmd.SetIn(bytes.NewReader(value)) + + cli := NewCLIInstanceWithStateDir(fs, testStateDir) + cli.cmd = cmd + + require.NoError(t, cli.AddSecret("added", false)) + + sourceFile := "/test/value" + require.NoError(t, afero.WriteFile(fs, sourceFile, value, 0o600)) + require.NoError(t, cli.ImportSecret(cmd, "imported", sourceFile, false)) + + added, err := vlt.GetSecret("added") + require.NoError(t, err) + + defer added.Destroy() + + imported, err := vlt.GetSecret("imported") + require.NoError(t, err) + + defer imported.Destroy() + + assert.Equal(t, []byte("a\n"), added.Bytes()) + assert.Equal(t, []byte("a\n\n"), imported.Bytes()) +}