From 66e22de9b55fa5f3d9b90d2c36ea75ccb4a41b8d Mon Sep 17 00:00:00 2001 From: Bob Lail Date: Mon, 20 Jul 2026 14:48:06 -0700 Subject: [PATCH] fix: Stop passing `which`'s own flags to `Identify()` --- which_cmd.go | 58 +++++++++++++++++++++++++++++------------------ which_cmd_test.go | 34 +++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 22 deletions(-) create mode 100644 which_cmd_test.go diff --git a/which_cmd.go b/which_cmd.go index df33c63..85d11e6 100644 --- a/which_cmd.go +++ b/which_cmd.go @@ -25,33 +25,47 @@ EXAMPLES // WhichExec implements the 'which' command. func WhichExec(e *Entrypoint, args, _ []string) error { - if cmd, _, err := e.Identify(args); err != nil { + identifyArgs, willResolveSymlinks := splitWhichArgs(args) + + cmd, _, err := e.Identify(identifyArgs) + if err != nil { return err } else if IsNull(cmd) { return exit.ErrUnknownSubcommand - } else { - willResolveSymlinks := false - - for _, arg := range args { - if arg == "--" { - break - } else if arg == "--follow-symlinks" || arg == "-s" { - willResolveSymlinks = true - } - } + } - path := cmd.Path() - if willResolveSymlinks { - resolvedPath, err := filepath.EvalSymlinks(path) - if err != nil { - fmt.Fprintf(os.Stderr, "ERROR: Unable to follow symlinks in %s\n", path) - return err - } else { - path = resolvedPath - } + path := cmd.Path() + if willResolveSymlinks { + resolvedPath, err := filepath.EvalSymlinks(path) + if err != nil { + fmt.Fprintf(os.Stderr, "ERROR: Unable to follow symlinks in %s\n", path) + return err } + path = resolvedPath + } - fmt.Println(path) - return nil + fmt.Println(path) + return nil +} + +// splitWhichArgs separates which's own --follow-symlinks/-s flags from the +// arguments that identify the command. The flags are consumed here rather than +// forwarded to Identify: left in, a flag becomes a trailing argument and causes +// Identify to resolve the command's default subcommand instead of the command +// itself. Everything after a `--` terminator is left untouched for the command. +func splitWhichArgs(args []string) (identifyArgs []string, followSymlinks bool) { + identifyArgs = make([]string, 0, len(args)) + + for i, arg := range args { + if arg == "--" { + identifyArgs = append(identifyArgs, args[i:]...) + break + } else if arg == "--follow-symlinks" || arg == "-s" { + followSymlinks = true + } else { + identifyArgs = append(identifyArgs, arg) + } } + + return identifyArgs, followSymlinks } diff --git a/which_cmd_test.go b/which_cmd_test.go new file mode 100644 index 0000000..e8bacf8 --- /dev/null +++ b/which_cmd_test.go @@ -0,0 +1,34 @@ +package exoskeleton + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +// -s/--follow-symlinks are which's own flags. They must be consumed by which +// and not forwarded to Identify -- regardless of whether they appear before or +// after the command name -- since a trailing flag would make Identify resolve +// the command's default subcommand instead of the command itself. +func TestWhichConsumesFollowSymlinksFlag(t *testing.T) { + scenarios := []struct { + args []string + wantIdentifyArgs []string + wantFollowSymlinks bool + }{ + {[]string{"tool"}, []string{"tool"}, false}, + {[]string{"tool", "-s"}, []string{"tool"}, true}, + {[]string{"tool", "--follow-symlinks"}, []string{"tool"}, true}, + {[]string{"-s", "tool"}, []string{"tool"}, true}, + {[]string{"--follow-symlinks", "tool"}, []string{"tool"}, true}, + + // Everything after `--` is left for the command, flag or not. + {[]string{"tool", "--", "-s"}, []string{"tool", "--", "-s"}, false}, + } + + for _, s := range scenarios { + identifyArgs, followSymlinks := splitWhichArgs(s.args) + assert.Equal(t, s.wantIdentifyArgs, identifyArgs, "which %v", s.args) + assert.Equal(t, s.wantFollowSymlinks, followSymlinks, "which %v", s.args) + } +}