fix(cli): Erstes „--“ schirmt Positionsargumente vollständig ab

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.
This commit is contained in:
m3ta-chiron committed 2026-10-18 20:46:39 +02:00
1 parent a3424c36e5
commit 671a97ca71
2 files changed
+126 -1

No files matched your search

+28 -1
View File
@@ -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 {
+98
View File
@@ -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)
}
})
}