From 33c70705c1af8270c8fecfc51c928699578dc72a Mon Sep 17 00:00:00 2001 From: Harsh Kapse Date: Fri, 18 Sep 2026 20:25:53 +0530 Subject: [PATCH] fix(cli): validate and reject misplaced flags placed after '--' - Add validateNoMisplacedFlags to detect when options are placed after '--' - Provide clear error messaging with guidance on flag placement - Allow literal files starting with '-' when existing on disk or index - Add unit test TestValidateNoMisplacedFlags --- cmd/diff_test.go | 52 ++++++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 20 +++++++++++++++++++ 2 files changed, 72 insertions(+) diff --git a/cmd/diff_test.go b/cmd/diff_test.go index c3d17fc0..2ae6d90f 100644 --- a/cmd/diff_test.go +++ b/cmd/diff_test.go @@ -327,3 +327,55 @@ func TestGitExternal7ArgsFailFast(t *testing.T) { t.Errorf("output = %q, want substring %q", string(out), want) } } + +func TestValidateNoMisplacedFlags(t *testing.T) { + tests := []struct { + name string + args []string + wantErr string + }{ + { + name: "valid positional args", + args: []string{"main", "cmd/diff_test.go"}, + }, + { + name: "stdin symbol", + args: []string{"-"}, + }, + { + name: "flag with value after separator", + args: []string{"main", "cmd/diff_test.go", "--color=always"}, + wantErr: `flag "--color=always" cannot be placed after '--'`, + }, + { + name: "short flag after separator", + args: []string{"main", "cmd/diff_test.go", "-f", "sbs"}, + wantErr: `flag "-f" cannot be placed after '--'`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateNoMisplacedFlags(tt.args) + if tt.wantErr == "" { + if err != nil { + t.Errorf("validateNoMisplacedFlags(%v) error = %v, want nil", tt.args, err) + } + return + } + if err == nil || !strings.Contains(err.Error(), tt.wantErr) { + t.Errorf("validateNoMisplacedFlags(%v) error = %v, want substring %q", tt.args, err, tt.wantErr) + } + }) + } + + t.Run("existing file starting with dash", func(t *testing.T) { + dashFile := filepath.Join(t.TempDir(), "-testfile.go") + if err := os.WriteFile(dashFile, []byte("package main\n"), 0o644); err != nil { + t.Fatalf("failed to write temp file: %v", err) + } + if err := validateNoMisplacedFlags([]string{dashFile}); err != nil { + t.Errorf("validateNoMisplacedFlags(%v) error = %v, want nil", dashFile, err) + } + }) +} diff --git a/cmd/root.go b/cmd/root.go index 19b76ced..0d948fb1 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -87,6 +87,11 @@ Examples: return refs, cobra.ShellCompDirectiveDefault }, Run: func(cmd *cobra.Command, args []string) { + if err := validateNoMisplacedFlags(args); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + cfg, err := config.Load() if err != nil { fmt.Fprintf(os.Stderr, "Warning: %v\n", err) @@ -827,6 +832,21 @@ func isFileOrDevNull(path string) bool { return err == nil && !info.IsDir() } +// Cobra stops parsing flags at '--' and treats everything after as a positional +// arg. Catch flags accidentally put after '--', unless they're real files starting with '-'. +func validateNoMisplacedFlags(args []string) error { + for _, arg := range args { + if strings.HasPrefix(arg, "-") && arg != "-" && !isFileOrDevNull(arg) && !git.IsTrackedFile(".", arg) { + return fmt.Errorf("flag %q cannot be placed after '--'\n\n"+ + "In CLI syntax, '--' marks the end of options; all subsequent arguments are treated as paths.\n"+ + "Place flags before '--' or omit '--':\n"+ + " diffm [flags] [refs...] [--] [paths...]\n"+ + " diffm [refs...] [paths...] [flags]", arg) + } + } + return nil +} + func runFileDiff(cmd *cobra.Command, fileA, fileB, displayPath string, format string, ignoreComments bool, parseErrorLimit int, sizeLimitKB int, lineLimitLines int, noPager bool) { uiMode, _ := cmd.Flags().GetBool("ui") fullMode, _ := cmd.Flags().GetBool("full")