From 671a97ca71daaf49ec364df3c287b1a7f3562ef3 Mon Sep 17 00:00:00 2001 From: m3ta-chiron Date: Sun, 18 Oct 2026 20:46:39 +0200 Subject: [PATCH] =?UTF-8?q?fix(cli):=20Erstes=20=E2=80=9E--=E2=80=9C=20sch?= =?UTF-8?q?irmt=20Positionsargumente=20vollst=C3=A4ndig=20ab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Interspersed-Loop von Task 03 nahm nach jedem Positionsargument das Flag-Parsen wieder auf und hob damit die Stdlib-Terminator-Semantik von „--“ auf: Argumente, die für einen NACHFOLGENDEN Befehl gemeint waren, wurden als talk-Flags fehlinterpretiert (talk wrap -- foo --bar wäre an --bar als unbekanntem Flag gescheitert; auch talk -- „Text --flag“ brach). Jetzt trennt parseInterspersed argv am ersten allein stehenden „--“: Nur der Teil davor wird interspersed geparst, der Rest ist bedingungslos Positionsargument. Die Abnahme-Form „Flag nach dem Text“ ohne „--“ (talk "Text" --provider=x) bleibt unverändert — Regressionstests für beide Formen in parse_test.go. --- cmd/talk/cli.go | 29 ++++++++++++- cmd/talk/parse_test.go | 98 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 1 deletion(-) create mode 100644 cmd/talk/parse_test.go diff --git a/cmd/talk/cli.go b/cmd/talk/cli.go index 3e55175..a3306d2 100644 --- a/cmd/talk/cli.go +++ b/cmd/talk/cli.go @@ -5,6 +5,7 @@ import ( "flag" "fmt" "io" + "slices" ) // Exit-Codes. @@ -151,12 +152,38 @@ func run(argv []string, stdin io.Reader, stdout, stderr io.Writer) int { } // parseInterspersed parst Flags, die überall in argv stehen dürfen. Die -// Go-stdlib-flag stoppt beim ersten Nicht-Flag-AArgument; die CLI-Surface +// Go-stdlib-flag stoppt beim ersten Nicht-Flag-Argument; die CLI-Surface // verlangt aber `talk "Text" --provider=x` (Abnahmekriterium Task 03) // genauso wie `talk --provider=x "Text"`. Trick: Parse über den Rest // wiederholen und jeweils das Positionsargument vor dem Weiterparsen // entnehmen. Fehler (unbekannte Flags, -h) kommen wie gewohnt vom FlagSet. +// +// Seit Task 07 schirmt das erste „--" (als eigenständiges Argument) alles +// dahinter vollständig ab — die Stdlib-Terminator-Semantik, die der +// Interspersed-Loop zuvor aushebelte: Er nahm nach jedem Positionsargument +// das Parsen wieder auf und fraß damit Flags, die für den NACHFOLGENDEN +// Befehl gemeint waren (`talk wrap -- foo --bar` wäre an --bar als +// vermeintlichem talk-Flag gescheitert). Jetzt wird argv am ersten „--" +// getrennt: Nur der Teil davor wird interspersed geparst, der Rest ist +// bedingungslos Positionsargument — für say (`talk -- "Text --flag"`) +// genauso wie für wrap (`talk wrap -- foo --bar`). Die Abnahme-Form +// `talk "Text" --provider=x` (Flag nach dem Text, ohne „--") ist davon +// unberührt. Ein „--" als Flag-Wert (unmittelbar nach --voice) trifft +// diese Trennung nicht, weil nur ein allein stehendes „--" trennt. func parseInterspersed(fs *flag.FlagSet, argv []string) ([]string, error) { + if i := slices.Index(argv, "--"); i >= 0 { + positional, err := parseInterspersedLoop(fs, argv[:i]) + if err != nil { + return nil, err + } + return append(positional, argv[i+1:]...), nil + } + return parseInterspersedLoop(fs, argv) +} + +// parseInterspersedLoop ist der Interspersed-Durchlauf — argv enthält kein +// allein stehendes „--" mehr (parseInterspersed hat davor getrennt). +func parseInterspersedLoop(fs *flag.FlagSet, argv []string) ([]string, error) { var positional []string rest := argv for { diff --git a/cmd/talk/parse_test.go b/cmd/talk/parse_test.go new file mode 100644 index 0000000..7e3ce22 --- /dev/null +++ b/cmd/talk/parse_test.go @@ -0,0 +1,98 @@ +package main + +import ( + "flag" + "io" + "strings" + "testing" +) + +// TestParseInterspersedDoubleDash (Regression, Task 07): Das erste allein +// stehende „--" schirmt alles Folgenden von der Flag-Auswertung ab — der +// Kern der wrap-Aufrufform `talk wrap -- foo --bar`. Vorher nahm der +// Interspersed-Loop nach jedem Positionsargument das Parsen wieder auf und +// fraß damit Flags des nachfolgenden Befehls. Gleichzeitig bleibt die +// Abnahme-Form „Flag nach dem Text" (ohne „--") intakt. +func TestParseInterspersedDoubleDash(t *testing.T) { + newWrapFS := func() (*flag.FlagSet, *sayOptions) { + fs := flag.NewFlagSet("talk wrap", flag.ContinueOnError) + fs.SetOutput(io.Discard) + fs.Usage = func() {} + return fs, addSayFlags(fs) + } + + t.Run("Doppelminus schirmt Befehls-Flags", func(t *testing.T) { + fs, opts := newWrapFS() + positional, err := parseInterspersed(fs, []string{"--", "foo", "--bar"}) + if err != nil { + t.Fatalf("parse: %v", err) + } + if strings.Join(positional, " ") != "foo --bar" { + t.Errorf("Positionsargumente = %q, want %q", positional, []string{"foo", "--bar"}) + } + if opts.wait { + t.Error("--bar darf kein talk-Flag geworden sein (Shielding kaputt)") + } + }) + + t.Run("talk-Flags vor dem Doppelminus gelten", func(t *testing.T) { + fs, opts := newWrapFS() + positional, err := parseInterspersed(fs, []string{"--wait", "--", "sleep", "1"}) + if err != nil { + t.Fatalf("parse: %v", err) + } + if !opts.wait { + t.Error("--wait vor dem -- muss als talk-Flag gelten") + } + if strings.Join(positional, " ") != "sleep 1" { + t.Errorf("Positionsargumente = %q, want %q", positional, []string{"sleep", "1"}) + } + }) + + t.Run("Positional vor dem Doppelminus wird mitgeschleppt", func(t *testing.T) { + fs, _ := newWrapFS() + positional, err := parseInterspersed(fs, []string{"foo", "--", "--bar"}) + if err != nil { + t.Fatalf("parse: %v", err) + } + if strings.Join(positional, " ") != "foo --bar" { + t.Errorf("Positionsargumente = %q, want %q", positional, []string{"foo", "--bar"}) + } + }) + + t.Run("abgeschirmtes talk-Flag setzt den Flag nicht", func(t *testing.T) { + fs, opts := newWrapFS() + positional, err := parseInterspersed(fs, []string{"--", "--wait", "true"}) + if err != nil { + t.Fatalf("parse: %v", err) + } + if opts.wait { + t.Error("--wait NACH dem -- gehört dem Befehl, nicht talk") + } + if strings.Join(positional, " ") != "--wait true" { + t.Errorf("Positionsargumente = %q, want %q", positional, []string{"--wait", "true"}) + } + }) + + t.Run("Flag nach dem Text ohne Doppelminus bleibt erlaubt", func(t *testing.T) { + fs, opts := newWrapFS() + positional, err := parseInterspersed(fs, []string{"Text", "--lang=en"}) + if err != nil { + t.Fatalf("parse: %v (Abnahme-Form Flag-nach-Text kaputt)", err) + } + if strings.Join(positional, " ") != "Text" || opts.lang != "en" { + t.Errorf("Positionsargumente = %q, lang = %q — Flag-nach-Text muss weitergehen", positional, opts.lang) + } + }) + + t.Run("Text nach dem Doppelminus kommt vollständig durch (say)", func(t *testing.T) { + fs, _ := newWrapFS() + positional, err := parseInterspersed(fs, []string{"--", "Bericht", "mit --flag", "fertig"}) + if err != nil { + t.Fatalf("parse: %v", err) + } + if strings.Join(positional, " ") != "Bericht mit --flag fertig" { + t.Errorf("Positionsargumente = %q, want Wörter unverändert", positional) + } + }) +}