feat(show): add configurable fuzzy-search fallback toggle (#3449)

* feat(show): add configurable fuzzy-search fallback toggle

* fix(tests): update TestMountConfig expected output for show.fuzzysearch default

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>

---------

Signed-off-by: Dominik Schulz <dominik.schulz@gauner.org>
This commit is contained in:
Dominik Schulz
2026-06-06 21:21:35 +02:00
committed by GitHub
parent e7d27b42a8
commit 7fd76df83c
11 changed files with 75 additions and 8 deletions
+1
View File
@@ -18,6 +18,7 @@
* [FEATURE] Add stable structured exit codes; --exit-codes flag lists all codes (I-2)
* [FEATURE] Add JSON output to audit, list, find, and recipients commands (I-3)
* [FEATURE] Add show.hidden-keys config option for customizable safecontent redaction (I-5)
* [FEATURE] Add show.fuzzysearch config and --nofuzzysearch flag to control automatic fuzzy lookup in show
* [FEATURE] Add --stdin, --file, and --exec modes to gopass env
* [ENHANCEMENT] Unified secret name validation rejects path traversal and consecutive slashes (I-1)
* [CLEANUP] Split Action handler into focused handler types (A-1)
+2
View File
@@ -30,6 +30,7 @@ Flag | Aliases | Description
`--password` | `-o` | Display only the password. For use in scripts. Takes precedence over other flags.
`--revision` | `-r` | Display a specific revision of the entry. Use an exact version identifier from `gopass history` or the special `-<N>` syntax. Does not work with native (e.g. git) refs.
`--noparsing` | `-n` | Do not parse the content, disable YAML and Key-Value functions.
`--nofuzzysearch` | | Do not start fuzzy search if the requested entry is not found.
`--nosync` | | Disable auto-sync for this invocation.
`--chars` | | Display selected characters from the password.
@@ -50,6 +51,7 @@ Note: The parser ensures every parsed secret contains a terminating newline, eve
* The `--clip` flag will copy the value of the `Password` field to the clipboard and doesn't display any part of the secret.
* The `--alsoclip` option will copy the value of the `Password` field but also display the secret content depending on the `safecontent` setting, i.e. obstructing the `Password` field if `safecontent` is `true` or just displaying it if not.
* The `--qr` flags operates complementary to other flags. It will *additionally* format the value of the `Password` entry as a QR code and display it. Other than that it will honor the other options, e.g. `gopass show --qr` will display the QR code *and* the whole secret content below. One special case is the `-o` flag, this flag doesn't make a lot of sense in combination, so if both `--qr` and `-o` are given only the QR code will be displayed.
* When an entry is not found, `gopass show` can start an interactive fuzzy search by default. This can be disabled globally with `show.fuzzysearch=false` or for one invocation via `--nofuzzysearch`.
* Since gopass plans to supports different RCS backends we do not support arbitrary git refs as arguments to the `--revision` flag. Using those might work, but this is explicitly not supported and bug reports will be closed as `wont-fix`. There are two issues with using arbitrary git refs is that (a) this doesn't work with non-git RCS backends and (b) git versions a whole repository, not single files. So the revision `HEAD^`
might not have any changes for a given entry. Thus we only support specifc revisions obtained from `gopass history` or our custom syntax `-N` where N is an integer identifying a specific commit before `HEAD` (cf. `HEAD~N`).
+1
View File
@@ -128,6 +128,7 @@ This is a list of available options:
| `recipients.check` | `bool` | Check recipients hash. The global config option takes precedence over local ones here for security reasons. | `false` |
| `recipients.hash` | `string` | SHA256 hash of the recipients file. Used to notify the user when the recipients files change. Not set, nor read at the local level for security reasons. | `` |
| `show.autoclip` | `bool` | Autoclip in `gopass show` by default. | `false` |
| `show.fuzzysearch` | `bool` | Automatically start fuzzy search in `gopass show` when an entry is not found. | `true` |
| `show.post-hook` | `string` | This hook is run right after displaying a secret with `gopass show`. | `None` |
| `show.safecontent` | `bool` | Only output _safe content_ (i.e. everything but the first line of a secret) to the terminal. Use _copy_ (`-c`) to retrieve the password in the clipboard, or _force_ (`-f`) to still print it. | `false` |
| `show.hidden-keys` | `string` (repeatable) | Additional secret field names to redact when `show.safecontent` is enabled. Set this key multiple times to hide multiple fields. The built-in keys (`password`, `totp`, `hotp`, `otpauth`) are always hidden regardless of this setting. Example: `gopass config show.hidden-keys api_token` | *(none)* |
-4
View File
@@ -78,8 +78,6 @@ github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4=
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
github.com/gopasspw/clipboard v0.0.4 h1:v3HUlVHfBXPx9woIQnsBIbs9ZM3i77OCtVKRMLhmR+c=
github.com/gopasspw/clipboard v0.0.4/go.mod h1:i0cShr7JEbOXZ/iKM5RyfBLbu1FPzouO8BTCJy0uHy8=
github.com/gopasspw/clipboard v0.0.5-0.20260524141134-6b387ae5aa1a h1:L7qDnmetJ62Fr59V/8Rrhb5l1+TdrmxORyHJ/WNlwdg=
github.com/gopasspw/clipboard v0.0.5-0.20260524141134-6b387ae5aa1a/go.mod h1:i0cShr7JEbOXZ/iKM5RyfBLbu1FPzouO8BTCJy0uHy8=
github.com/gopasspw/gitconfig v0.0.4 h1:7JE0iTm92OdXCtkS33CnbqcAEqQXYWTUriYFf3sRTBk=
@@ -240,8 +238,6 @@ golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220919091848-fb04ddd9f9c8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+4
View File
@@ -117,6 +117,10 @@ func ShowFlags() []cli.Flag {
Aliases: []string{"n"},
Usage: "Do not parse the output.",
},
&cli.BoolFlag{
Name: "nofuzzysearch",
Usage: "Do not start fuzzy search when an entry is not found",
},
&cli.BoolFlag{
Name: "nosync",
Usage: "Disable auto-sync",
+5 -2
View File
@@ -52,7 +52,8 @@ core.notifications = true
generate.autoclip = true
`
want += "mounts.path = " + fsutil.ShrinkPath(u.StoreDir("")) + "\n" +
"pwgen.xkcd-lang = en\n"
"pwgen.xkcd-lang = en\n" +
"show.fuzzysearch = true\n"
assert.Equal(t, want, buf.String())
})
@@ -98,7 +99,8 @@ core.notifications = true
generate.autoclip = true
`
want += "mounts.path = " + fsutil.ShrinkPath(u.StoreDir("")) + "\n" +
"pwgen.xkcd-lang = en\n"
"pwgen.xkcd-lang = en\n" +
"show.fuzzysearch = true\n"
assert.Equal(t, want, buf.String(), "action.printConfigValues")
})
@@ -137,6 +139,7 @@ core.notifications
generate.autoclip
mounts.path
pwgen.xkcd-lang
show.fuzzysearch
`
assert.Equal(t, want, buf.String())
})
+9 -1
View File
@@ -48,6 +48,14 @@ func isTrailingFlag(arg string) bool {
arg == "-C" || arg == "--alsoclip"
}
func isShowFuzzySearchEnabled(ctx context.Context, cmd *cli.Command) bool {
if cmd != nil && cmd.Bool("nofuzzysearch") {
return false
}
return config.Bool(ctx, "show.fuzzysearch")
}
func showParseTrailingFlags(ctx context.Context, cmd *cli.Command) context.Context {
for i := 1; i < cmd.Args().Len(); i++ {
arg := cmd.Args().Get(i)
@@ -472,7 +480,7 @@ func (s *secretHandler) hasAliasDomain(ctx context.Context, name string) string
// showHandleError handles errors retrieving secrets.
func (s *secretHandler) showHandleError(ctx context.Context, cmd *cli.Command, name string, recurse bool, err error) error {
if !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) {
if !errors.Is(err, store.ErrNotFound) || !recurse || !ctxutil.IsTerminal(ctx) || !isShowFuzzySearchEnabled(ctx, cmd) {
if IsClip(ctx) {
_ = notify.Notify(ctx, "gopass - error", fmt.Sprintf("failed to retrieve secret %q: %s", name, err))
}
+48
View File
@@ -2,6 +2,7 @@ package action
import (
"bytes"
"context"
"fmt"
"os"
"testing"
@@ -10,11 +11,13 @@ import (
"github.com/gopasspw/clipboard"
"github.com/gopasspw/gopass/internal/config"
"github.com/gopasspw/gopass/internal/out"
"github.com/gopasspw/gopass/internal/store"
"github.com/gopasspw/gopass/pkg/ctxutil"
"github.com/gopasspw/gopass/pkg/gopass/secrets"
"github.com/gopasspw/gopass/tests/gptest"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/urfave/cli/v3"
)
func TestShowMulti(t *testing.T) {
@@ -596,6 +599,51 @@ func TestShowHandleError(t *testing.T) {
buf.Reset()
}
func TestShowHandleErrorFuzzySearchToggle(t *testing.T) {
u := gptest.NewUnitTester(t)
ctx := config.NewContextInMemory()
ctx = ctxutil.WithAlwaysYes(ctx, true)
ctx = ctxutil.WithTerminal(ctx, true)
act, err := newMock(ctx, u.StoreDir(""))
require.NoError(t, err)
require.NotNil(t, act)
ctx = act.cfg.WithConfig(ctx)
called := false
act.secrets.findFuzzyFn = func(context.Context, *cli.Command) error {
called = true
return nil
}
t.Run("default config enables fuzzy search", func(t *testing.T) {
called = false
c := gptest.CliCtx(ctx, t)
require.NoError(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
assert.True(t, called)
})
t.Run("show.fuzzysearch=false disables fuzzy search", func(t *testing.T) {
called = false
require.NoError(t, act.cfg.Set("", "show.fuzzysearch", "false"))
c := gptest.CliCtx(ctx, t)
require.Error(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
assert.False(t, called)
})
t.Run("--nofuzzysearch disables fuzzy search for one invocation", func(t *testing.T) {
called = false
require.NoError(t, act.cfg.Set("", "show.fuzzysearch", "true"))
c := gptest.CliCtxWithFlags(ctx, t, map[string]string{"nofuzzysearch": "true"})
require.Error(t, act.showHandleError(ctx, c, "missing", true, store.ErrNotFound))
assert.False(t, called)
})
}
func TestShowPrintQR(t *testing.T) {
u := gptest.NewUnitTester(t)
+1
View File
@@ -58,6 +58,7 @@ var defaults = map[string]string{
"core.notifications": "true",
"core.follow-references": "false",
"pwgen.xkcd-lang": "en",
"show.fuzzysearch": "true",
}
// Config is a gopass config handler.
+1
View File
@@ -47,6 +47,7 @@ func TestConfig(t *testing.T) {
"env.string",
"mounts.path",
"pwgen.xkcd-lang",
"show.fuzzysearch",
}, cfg.Keys(""))
for key, expected := range defaults {
assert.Equal(t, expected, cfg.Get(key))
+3 -1
View File
@@ -27,7 +27,8 @@ core.follow-references = false
core.notifications = true
`
wanted += "mounts.path = " + ts.storeDir("root") + "\n" +
"pwgen.xkcd-lang = en"
`pwgen.xkcd-lang = en
show.fuzzysearch = true`
assert.Equal(t, wanted, out)
@@ -86,6 +87,7 @@ core.notifications = true
wanted += "mounts.path = " + ts.storeDir("root") + "\n"
wanted += "pwgen.xkcd-lang = en\n"
wanted += "recipients.mnt/m1.hash = 9a4c4b1e0eb9ade2e692ff948f43d9668145eca3df88ffff67e0e21426252907\n"
wanted += "show.fuzzysearch = true\n"
out, err := ts.run("config")
require.NoError(t, err)