# Review package: 30bd535..HEAD ## Commits acfba19 docs(readme): talk wrap dokumentieren — --Semantik, Label, Exit-Codes 6877d64 test: Prozessgrenzen-Tests für talk wrap an echter CLI + Daemon 51cd5a6 feat(cli): talk wrap — Befehlsummwicklung mit Ansage und Code-Durchreichung 671a97c fix(cli): Erstes „--“ schirmt Positionsargumente vollständig ab a3424c3 feat(config): Ansage-Label wrap.label mit Env-Override TALK_WRAP_LABEL ## Files changed README.md | 47 ++++++- cmd/talk/cli.go | 39 +++++- cmd/talk/cli_test.go | 25 +++- cmd/talk/daemon.go | 14 +- cmd/talk/degraded.go | 7 +- cmd/talk/main.go | 9 +- cmd/talk/parse_test.go | 98 ++++++++++++++ cmd/talk/say.go | 28 ++-- cmd/talk/wrap.go | 195 ++++++++++++++++++++++++++++ cmd/talk/wrap_test.go | 281 +++++++++++++++++++++++++++++++++++++++++ internal/config/config.go | 15 +++ internal/config/config_test.go | 52 ++++++++ test/wrap_test.go | 168 ++++++++++++++++++++++++ 13 files changed, 947 insertions(+), 31 deletions(-) ## Diff README.md --- 1/2 --- Text 30 30 31 **Hinweis:** In diesem Stand sind i 31 **Hinweis:** In diesem Stand sind i .. mplementiert: `say` (und `talk "Tex .. mplementiert: `say` (und `talk "Tex .. t"`) .. t"`) 32 mit der kompletten Provider-Kette u 32 mit der kompletten Provider-Kette u .. nd Daemon-Anbindung (non-blocking ü .. nd Daemon-Anbindung (non-blocking ü .. ber .. ber 33 `talkd`, siehe unten), `mute`, `unm 33 `talkd`, siehe unten), `mute`, `unm .. ute`, `status`, `cancel`. Die übrig .. ute`, `status`, `cancel` sowie `wra .. en .. p`. 34 Befehle sind Stubs und melden `not 34 Die übrigen Befehle sind Stubs und .. implemented yet` (Exit-Code ≠ 0). .. melden `not implemented yet` (Exit- .. .. Code .. 35 ≠ 0). 35 36 36 ## Daemon talkd (Task 04–06) 37 ## Daemon talkd (Task 04–06) 37 38 README.md --- 2/2 --- Text 219 220 `TALK_AUDIO_SINK=null` (oder `audio.sink = "null"` in der config.toml) 220 221 schaltet jede Tongabe ab — für CI und Server. 221 222 ... 223 ## `talk wrap` — Befehle annoncieren (Task 07) ... 224 ... 225 `talk wrap -- [Argumente …]` führt den Befehl aus und macht das ... 226 Ergebnis hörbar: stdin/stdout/stderr werden durchgereicht — stdout gehört ... 227 allein dem Befehl, Informationszeilen von talk (z. B. die Item-ID der ... 228 Ansage) gehen auf stderr, damit Pipes und Redirects sauber bleiben. Die ... 229 Wandzeit wird gemessen und nach dem Ende des Befehls angesagt: ... 230 ... 231 - Erfolg: „sleep erfolgreich. Dauer: 2 Sekunden." ... 232 - Misserfolg: „sh fehlgeschlagen, Code 2." (ohne Dauer) ... 233 ... 234 ```console ... 235 talk wrap -- sleep 2 ... 236 talk wrap --wait -- make test ... 237 ``` ... 238 ... 239 - **`--` schirmt den Befehl komplett ab:** Das erste allein stehende `--` ... 240 beendet die talk-Flag-Auswertung — alles danach geht unverändert an den ... 241 Befehl. `talk wrap -- foo --bar` reicht `--bar` an foo weiter, statt es ... 242 als talk-Flag zu fehlzuinterpretieren. talk-Flags für die Ansage stehen ... 243 deshalb davor (`talk wrap --wait -- …`); ohne `--` gelten die üblichen ... 244 Regeln (Flags überall, auch nach Positionalargumenten). ... 245 - **Label** der Ansage: `wrap.label` in der config.toml (Env ... 246 `TALK_WRAP_LABEL` schlägt die Datei) oder der Basisname des Befehls ... 247 (`/usr/bin/make` → „make"). Die Sprache erkennt langdetect am Label ... 248 (de/en, deutscher Default); `--lang` überschreibt. ... 249 - **Exit-Code-Politik:** wrap exitet immer mit dem Code des Befehls — ... 250 scheitert die Ansage selbst, bleibt der Befehls-Code trotzdem (nur eine ... 251 Warning auf stderr; talk-Fehler ≠ Befehls-Fehler). Nicht startbare ... 252 Befehle (Binary nicht gefunden) zählen als 127 (Shell-Konvention), ... 253 Signaltode portabel als 1. Benutzungsfehler von talk selbst (kein ... 254 Befehl, ungültige Flags) scheitern vor dem Start des Befehls. ... 255 - **Ansageweg:** derselbe wie say — Daemon-Pfad (enqueue, non-blocking; ... 256 `--wait` respektiert), ohne Daemon Degraded Mode über die Lock-Datei. ... 257 wrap erbt die say-Flags (`--wait`, `--now`, `--priority`, `--voice`, ... 258 `--lang`, `--provider`) und reicht sie an die Ansage durch. ... 259 ... 260 ```toml ... 261 [wrap] ... 262 label = "Deployment" # Ansage-Label statt Befehls-Basisname (TALK_WRAP_LABEL) ... 263 ``` ... 264 222 265 ## Entwicklung 223 266 224 267 ```console cmd/talk/cli.go --- 1/3 --- Go 5 5 "flag" 6 6 "fmt" 7 7 "io" . 8 "slices" 8 9 ) 9 10 10 11 // Exit-Codes. cmd/talk/cli.go --- 2/3 --- Go 44 name: "wrap", 45 name: "wrap", 45 short: "Befehl ausführen u 46 short: "Befehl ausführen .. nd Ergebnis ansagen", .. und Ergebnis ansagen", 46 args: "-- [Argum 47 args: "-- [Arg .. ente …]", .. umente …]", 47 detail: "Führt den Befehl a 48 detail: "Führt den Befehl .. us (stdin/stdout/stderr werden durc .. aus (stdin/stdout/stderr werden du .. hgereicht) und spricht danach Erfol .. rchgereicht) und spricht danach Erf .. g bzw. Misserfolg und Dauer.", .. olg bzw. Misserfolg und Dauer. Das .. .. erste „--\" schirmt den Befehl komp .. .. lett von den talk-Flags ab: `talk w .. .. rap -- foo --bar` reicht --bar an f .. .. oo weiter. Der Exit-Code des Befehl .. .. s wird durchgereicht; scheitert die .. .. Ansage selbst, bleibt er trotzdem. .. .. Label der Ansage: config `wrap.lab .. .. el` oder der Basisname des Befehls. .. .. wrap erbt die say-Flags (--wait, - .. .. -now, --priority, --voice, --lang, .. .. --provider).", .. 49 register: addSayFlags, .. 50 handler: runWrap, 48 }, 51 }, 49 { 52 { 50 name: "mute", 53 name: "mute", cmd/talk/cli.go --- 3/3 --- Go 151 } 154 } 152 155 153 // parseInterspersed parst Flags, d 156 // parseInterspersed parst Flags, d ... ie überall in argv stehen dürfen. D ... ie überall in argv stehen dürfen. D ... ie ... ie 154 // Go-stdlib-flag stoppt beim erste 157 // Go-stdlib-flag stoppt beim erste ... n Nicht-Flag-AArgument; die CLI-Sur ... n Nicht-Flag-Argument; die CLI-Surf ... face ... ace 155 // verlangt aber `talk "Text" --pro 158 // verlangt aber `talk "Text" --pro ... vider=x` (Abnahmekriterium Task 03) ... vider=x` (Abnahmekriterium Task 03) 156 // genauso wie `talk --provider=x " 159 // genauso wie `talk --provider=x " ... Text"`. Trick: Parse über den Rest ... Text"`. Trick: Parse über den Rest 157 // wiederholen und jeweils das Posi 160 // wiederholen und jeweils das Posi ... tionsargument vor dem Weiterparsen ... tionsargument vor dem Weiterparsen 158 // entnehmen. Fehler (unbekannte Fl 161 // entnehmen. Fehler (unbekannte Fl ... ags, -h) kommen wie gewohnt vom Fla ... ags, -h) kommen wie gewohnt vom Fla ... gSet. ... gSet. ... 162 // ... 163 // Seit Task 07 schirmt das erste „ ... ... --" (als eigenständiges Argument) a ... ... lles ... 164 // dahinter vollständig ab — die St ... ... dlib-Terminator-Semantik, die der ... 165 // Interspersed-Loop zuvor aushebel ... ... te: Er nahm nach jedem Positionsarg ... ... ument ... 166 // das Parsen wieder auf und fraß d ... ... amit Flags, die für den NACHFOLGEND ... ... EN ... 167 // Befehl gemeint waren (`talk wrap ... ... -- foo --bar` wäre an --bar als ... 168 // vermeintlichem talk-Flag geschei ... ... tert). Jetzt wird argv am ersten „- ... ... -" ... 169 // getrennt: Nur der Teil davor wir ... ... d interspersed geparst, der Rest is ... ... t ... 170 // bedingungslos Positionsargument ... ... — für say (`talk -- "Text --flag"`) ... 171 // genauso wie für wrap (`talk wrap ... ... -- foo --bar`). Die Abnahme-Form ... 172 // `talk "Text" --provider=x` (Flag ... ... nach dem Text, ohne „--") ist davo ... ... n ... 173 // unberührt. Ein „--" als Flag-Wer ... ... t (unmittelbar nach --voice) trifft ... 174 // diese Trennung nicht, weil nur e ... ... in allein stehendes „--" trennt. 159 func parseInterspersed(fs *flag.Fla 175 func parseInterspersed(fs *flag.Fla ... gSet, argv []string) ([]string, err ... gSet, argv []string) ([]string, err ... or) { ... or) { ... 176 if i := slices.Index(argv, "--" ... ... ); i >= 0 { ... 177 positional, err := parseInt ... ... erspersedLoop(fs, argv[:i]) ... 178 if err != nil { ... 179 return nil, err ... 180 } ... 181 return append(positional, a ... ... rgv[i+1:]...), nil ... 182 } ... 183 return parseInterspersedLoop(fs ... ... , argv) ... 184 } ... 185 ... 186 // parseInterspersedLoop ist der In ... ... terspersed-Durchlauf — argv enthält ... ... kein ... 187 // allein stehendes „--" mehr (pars ... ... eInterspersed hat davor getrennt). ... 188 func parseInterspersedLoop(fs *flag ... ... .FlagSet, argv []string) ([]string, ... ... error) { 160 var positional []string 189 var positional []string 161 rest := argv 190 rest := argv 162 for { 191 for { cmd/talk/cli_test.go --- 1/2 --- Go 15 } 15 } 16 16 17 // wantStubCommands ist wantSubcomm 17 // wantStubCommands ist wantSubcomm .. ands ohne die implementierten Befeh .. ands ohne die implementierten Befeh .. le — .. le — 18 // say (Task 02/04) sowie mute/unmu 18 // say (Task 02/04), mute/unmute/st .. te/status/cancel (Task 04, Daemon- .. atus/cancel (Task 04, Daemon-Aufruf .. .. e) .. 19 // und wrap (Task 07, Befehlsummich .. .. tung mit Ansage) sind real; die 19 // Aufrufe) sind real; die restlich 20 // restlichen bleiben Stubs bis zu .. en bleiben Stubs bis zu ihren Tasks .. ihren Tasks. .. . .. 20 var wantStubCommands = []string{ 21 var wantStubCommands = []string{ 21 "wrap", "voices", "test", "doct 22 "voices", "test", "doctor", "se .. or", "setup", .. tup", 22 } 23 } 23 24 24 // runCLI ist der Test-Einstieg in 25 // runCLI ist der Test-Einstieg in .. run mit leerem stdin (kein Text übe .. run mit leerem stdin (kein Text übe .. r .. r cmd/talk/cli_test.go --- 2/2 --- Go 106 107 } 107 108 } ... 109 ... 110 // TestWrapHelpZeigtSayFlags: wrap erbt die say-Flags (mindestens --wait) ... 111 // und ist kein Stub mehr. ... 112 func TestWrapHelpZeigtSayFlags(t *testing.T) { ... 113 var out, errBuf bytes.Buffer ... 114 code := runCLI([]string{"wrap", "--help"}, &out, &errBuf) ... 115 if code != exitOK { ... 116 t.Fatalf("talk wrap --help: Exit-Code = %d, want %d (stderr: %q)", code, exitOK, errBuf.String()) ... 117 } ... 118 for _, f := range []string{"-wait", "-provider"} { ... 119 if !strings.Contains(out.String(), f) { ... 120 t.Errorf("talk wrap --help zeigt Flag %s nicht (Output: %q)", f, out.String()) ... 121 } ... 122 } ... 123 if strings.Contains(out.String(), "noch nicht implementiert") { ... 124 t.Errorf("talk wrap --help soll wrap nicht als Stub markieren: %q", out.String()) ... 125 } ... 126 } 108 127 109 128 func TestHelpSubcommand(t *testing.T) { 110 129 t.Run("ohne Argument", func(t *testing.T) { cmd/talk/daemon.go --- 1/3 --- Go 24 24 25 // sayViaDaemon ist der Daemon-Pfad 25 // sayViaDaemon ist der Daemon-Pfad .. von say: Erkennung über GET /status, .. von say: Erkennung über GET /status, 26 // dann enqueue (POST /speak). Defau 26 // dann enqueue (POST /speak). Defau .. lt: sofort zurückkehren (non-blockin .. lt: sofort zurückkehren (non-blockin .. g), .. g), 27 // stdout erhält die Item-ID; --wait 27 // info erhält die Item-ID; --wait p .. pollt /status, bis die Ansage weder .. ollt /status, bis die Ansage weder 28 // läuft noch wartet. Bei aktivem Mu 28 // läuft noch wartet. Bei aktivem Mu .. te verwirft der Daemon die Ansage — .. te verwirft der Daemon die Ansage — 29 // stdout sagt „muted". 29 // info sagt „muted". info ist der W .. .. riter für Informations-Token (Item-I .. .. D, .. 30 // muted, discarded): say reicht std .. .. out, wrap stderr durch — stdout eine .. .. s .. 31 // gewrappten Befehls darf nicht um .. .. talk-Zeilen verunreinigt werden. 30 // 32 // 31 // handled=false heißt: kein Daemon 33 // handled=false heißt: kein Daemon .. erreichbar → der Aufrufer geht in de .. erreichbar → der Aufrufer geht in de .. n .. n 32 // Degraded Mode. Stirbt der Daemon 34 // Degraded Mode. Stirbt der Daemon .. zwischen Erkennung und enqueue .. zwischen Erkennung und enqueue 33 // (Netz-Fehler), gilt dasselbe; ein 35 // (Netz-Fehler), gilt dasselbe; ein .. e Daemon-Antwort mit Fehlerstatus .. e Daemon-Antwort mit Fehlerstatus 34 // (z. B. 400) wird dagegen angezeig 36 // (z. B. 400) wird dagegen angezeig .. t — die Ansage war beim Daemon .. t — die Ansage war beim Daemon 35 // angekommen und wurde bewusst abge 37 // angekommen und wurde bewusst abge .. lehnt. .. lehnt. 36 func sayViaDaemon(cfg config.Config, 38 func sayViaDaemon(cfg config.Config, .. text string, opts *sayOptions, stdo .. text string, opts *sayOptions, info .. ut, stderr io.Writer) (code int, han .. , stderr io.Writer) (code int, handl .. dled bool) { .. ed bool) { 37 client := api.NewClient(cfg.Serv 39 client := api.NewClient(cfg.Serv .. er.Port) .. er.Port) 38 40 39 probeCtx, cancelProbe := context 41 probeCtx, cancelProbe := context .. .WithTimeout(context.Background(), d .. .WithTimeout(context.Background(), d .. aemonProbeTimeout) .. aemonProbeTimeout) cmd/talk/daemon.go --- 2/3 --- Go 61 return 0, false 63 return 0, false 62 } 64 } 63 if resp.Muted { 65 if resp.Muted { 64 fmt.Fprintln(stdout, "muted" 66 fmt.Fprintln(info, "muted") .. ) .. 65 fmt.Fprintln(stderr, "talk: 67 fmt.Fprintln(stderr, "talk: .. stumm geschaltet (mute) — Ansage ver .. stumm geschaltet (mute) — Ansage ver .. worfen") .. worfen") 66 return exitOK, true 68 return exitOK, true 67 } 69 } cmd/talk/daemon.go --- 3/3 --- Go 73 if resp.Toast { 75 if resp.Toast { 74 line += " (toast)" 76 line += " (toast)" 75 } 77 } 76 fmt.Fprintln(stdout, line) 78 fmt.Fprintln(info, line) 77 fmt.Fprintln(stderr, "talk: 79 fmt.Fprintln(stderr, "talk: .. "+discardHint(resp.Discarded, resp.T .. "+discardHint(resp.Discarded, resp.T .. oast)) .. oast)) 78 return exitOK, true 80 return exitOK, true 79 } 81 } 80 fmt.Fprintln(stdout, resp.ID) 82 fmt.Fprintln(info, resp.ID) 81 if !opts.wait { 83 if !opts.wait { 82 return exitOK, true 84 return exitOK, true 83 } 85 } cmd/talk/degraded.go --- Go 31 // dann Spracherkennung → Provider-K 31 // dann Spracherkennung → Provider-K .. ette → Sink — identische Kette wie i .. ette → Sink — identische Kette wie i .. m .. m 32 // Daemon, nur im CLI-Prozess (block 32 // Daemon, nur im CLI-Prozess (block .. ierend; ohne Daemon ist --wait das .. ierend; ohne Daemon ist --wait das 33 // Default-Verhalten). Best-effort: 33 // Default-Verhalten). Best-effort: .. Ohne Config-Dir läuft es ohne Lock u .. Ohne Config-Dir läuft es ohne Lock u .. nd .. nd 34 // Mute weiter, statt zu scheitern. 34 // Mute weiter, statt zu scheitern. .. .. info ist der Writer für Informations .. .. - .. 35 // Token („muted"): say reicht stdou .. .. t, wrap stderr durch. 35 func sayDegraded(cfg config.Config, 36 func sayDegraded(cfg config.Config, .. text string, opts *sayOptions, stdou .. text string, opts *sayOptions, info, .. t, stderr io.Writer) int { .. stderr io.Writer) int { 36 if dir, err := config.Dir(getenv 37 if dir, err := config.Dir(getenv .. ); err == nil { .. ); err == nil { 37 mute := queue.MuteStore{Path 38 mute := queue.MuteStore{Path .. : filepath.Join(dir, queue.MuteFileN .. : filepath.Join(dir, queue.MuteFileN .. ame)} .. ame)} 38 if mute.Muted() { 39 if mute.Muted() { 39 fmt.Fprintln(stdout, "mu 40 fmt.Fprintln(info, "mute .. ted") .. d") 40 fmt.Fprintln(stderr, "ta 41 fmt.Fprintln(stderr, "ta .. lk: stumm geschaltet (mute) — Ansage .. lk: stumm geschaltet (mute) — Ansage .. verworfen") .. verworfen") 41 return exitOK 42 return exitOK 42 } 43 } cmd/talk/main.go --- Go 1 // talk ist die CLI für Sprach-Ausga 1 // talk ist die CLI für Sprach-Ausga . ben (talk 2.0). . ben (talk 2.0). 2 // 2 // 3 // Dieser Stand (Task 04): say (und 3 // Dieser Stand (Task 07): say (und . der Default-Aufruf `talk "Text"`) re . der Default-Aufruf `talk "Text"`) re . icht . icht 4 // Ansagen in den Daemon talkd ein — 4 // Ansagen in den Daemon talkd ein — . non-blocking, --wait pollt bis zur . non-blocking, --wait pollt bis zur 5 // Fertigstellung, --now springt vor 5 // Fertigstellung, --now springt vor . . Ohne Daemon fällt say in den Degra . . Ohne Daemon fällt say in den Degra . ded . ded 6 // Mode (Lock-Datei, sequenziell lok 6 // Mode (Lock-Datei, sequenziell lok . al, mute respektiert). mute/unmute/ . al, mute respektiert). mute/unmute/ 7 // status/cancel sind Daemon-Aufrufe 7 // status/cancel sind Daemon-Aufrufe . mit plain Output. Die übrigen Befeh . mit plain Output. `talk wrap -- ` . 8 // führt Befehle aus (stdin/stdout/s . . tderr durchgereicht, Exit-Code . 9 // durchgereicht) und sagt danach Er . . folg/Misserfolg und Dauer an. Die 8 // sind Stubs; TUI und weitere Aufga 10 // übrigen Befehle sind Stubs; TUI u . ben folgen in den Tasks 05–09. .. nd weitere Aufgaben folgen in den . 11 // Tasks 08–09. 9 package main 12 package main 10 13 11 import "os" 14 import "os" cmd/talk/parse_test.go --- Go 1 package main 2 3 import ( 4 "flag" 5 "io" 6 "strings" 7 "testing" 8 ) 9 10 // TestParseInterspersedDoubleDash (Regression, Task 07): Das erste allein 11 // stehende „--" schirmt alles Folgenden von der Flag-Auswertung ab — der 12 // Kern der wrap-Aufrufform `talk wrap -- foo --bar`. Vorher nahm der 13 // Interspersed-Loop nach jedem Positionsargument das Parsen wieder auf und 14 // fraß damit Flags des nachfolgenden Befehls. Gleichzeitig bleibt die 15 // Abnahme-Form „Flag nach dem Text" (ohne „--") intakt. 16 func TestParseInterspersedDoubleDash(t *testing.T) { 17 newWrapFS := func() (*flag.FlagSet, *sayOptions) { 18 fs := flag.NewFlagSet("talk wrap", flag.ContinueOnError) 19 fs.SetOutput(io.Discard) 20 fs.Usage = func() {} 21 return fs, addSayFlags(fs) 22 } 23 24 t.Run("Doppelminus schirmt Befehls-Flags", func(t *testing.T) { 25 fs, opts := newWrapFS() 26 positional, err := parseInterspersed(fs, []string{"--", "foo", "--bar"}) 27 if err != nil { 28 t.Fatalf("parse: %v", err) 29 } 30 if strings.Join(positional, " ") != "foo --bar" { 31 t.Errorf("Positionsargumente = %q, want %q", positional, []string{"foo", "--bar"}) 32 } 33 if opts.wait { 34 t.Error("--bar darf kein talk-Flag geworden sein (Shielding kaputt)") 35 } 36 }) 37 38 t.Run("talk-Flags vor dem Doppelminus gelten", func(t *testing.T) { 39 fs, opts := newWrapFS() 40 positional, err := parseInterspersed(fs, []string{"--wait", "--", "sleep", "1"}) 41 if err != nil { 42 t.Fatalf("parse: %v", err) 43 } 44 if !opts.wait { 45 t.Error("--wait vor dem -- muss als talk-Flag gelten") 46 } 47 if strings.Join(positional, " ") != "sleep 1" { 48 t.Errorf("Positionsargumente = %q, want %q", positional, []string{"sleep", "1"}) 49 } 50 }) 51 52 t.Run("Positional vor dem Doppelminus wird mitgeschleppt", func(t *testing.T) { 53 fs, _ := newWrapFS() 54 positional, err := parseInterspersed(fs, []string{"foo", "--", "--bar"}) 55 if err != nil { 56 t.Fatalf("parse: %v", err) 57 } 58 if strings.Join(positional, " ") != "foo --bar" { 59 t.Errorf("Positionsargumente = %q, want %q", positional, []string{"foo", "--bar"}) 60 } 61 }) 62 63 t.Run("abgeschirmtes talk-Flag setzt den Flag nicht", func(t *testing.T) { 64 fs, opts := newWrapFS() 65 positional, err := parseInterspersed(fs, []string{"--", "--wait", "true"}) 66 if err != nil { 67 t.Fatalf("parse: %v", err) 68 } 69 if opts.wait { 70 t.Error("--wait NACH dem -- gehört dem Befehl, nicht talk") 71 } 72 if strings.Join(positional, " ") != "--wait true" { 73 t.Errorf("Positionsargumente = %q, want %q", positional, []string{"--wait", "true"}) 74 } 75 }) 76 77 t.Run("Flag nach dem Text ohne Doppelminus bleibt erlaubt", func(t *testing.T) { 78 fs, opts := newWrapFS() 79 positional, err := parseInterspersed(fs, []string{"Text", "--lang=en"}) 80 if err != nil { 81 t.Fatalf("parse: %v (Abnahme-Form Flag-nach-Text kaputt)", err) 82 } 83 if strings.Join(positional, " ") != "Text" || opts.lang != "en" { 84 t.Errorf("Positionsargumente = %q, lang = %q — Flag-nach-Text muss weitergehen", positional, opts.lang) 85 } 86 }) 87 88 t.Run("Text nach dem Doppelminus kommt vollständig durch (say)", func(t *testing.T) { 89 fs, _ := newWrapFS() 90 positional, err := parseInterspersed(fs, []string{"--", "Bericht", "mit --flag", "fertig"}) 91 if err != nil { 92 t.Fatalf("parse: %v", err) 93 } 94 if strings.Join(positional, " ") != "Bericht mit --flag fertig" { 95 t.Errorf("Positionsargumente = %q, want Wörter unverändert", positional) 96 } 97 }) 98 } 99 cmd/talk/say.go --- 1/2 --- Go 53 } 53 } 54 } 54 } 55 55 56 // Validierung vor jedem Daemon- 56 // Validierung vor jedem Daemon- .. Kontakt: Benutzungsfehler sollen .. Kontakt (auch wrap erbt die say-Flag .. .. s): 57 // schnell und ohne Netzwerk-Umw 57 // Benutzungsfehler sollen schne .. ege auffallen. .. ll und ohne Netzwerk-Umwege auffalle .. .. n. 58 if opts.lang != "" && opts.lang .. .. != "de" && opts.lang != "en" { .. 59 fmt.Fprintf(stderr, "talk: - .. .. -lang: ungültiger Wert %q (erlaubt: .. .. de, en)\n", opts.lang) .. 60 return exitFailure .. 61 } .. 62 if opts.priority != "" && !queue 58 if code, bad := validateSayOptio .. .Priority(opts.priority).Valid() { .. ns(opts, stderr); bad { 63 fmt.Fprintf(stderr, "talk: - .. .. -priority: ungültiger Wert %q (erlau .. .. bt: low, normal, high)\n", opts.prio .. .. rity) .. 64 return exitFailure 59 return code 65 } 60 } 66 61 67 cfg, err := config.Load(config.O 62 cfg, err := config.Load(config.O .. ptions{Getenv: getenv}) .. ptions{Getenv: getenv}) cmd/talk/say.go --- 2/2 --- Go 78 73 return sayDegraded(cfg, text, opts, stdout, stderr) 79 74 } .. 75 .. 76 // validateSayOptions prüft die say-Flags (wrap erbt sie): --lang auf de | .. 77 // en, --priority auf low | normal | high. Rückgabe (code, true) heißt: .. 78 // Benutzungsfehler, bereits auf stderr gemeldet. .. 79 func validateSayOptions(opts *sayOptions, stderr io.Writer) (int, bool) { .. 80 if opts.lang != "" && opts.lang != "de" && opts.lang != "en" { .. 81 fmt.Fprintf(stderr, "talk: --lang: ungültiger Wert %q (erlaubt: de, en)\n", opts.lang) .. 82 return exitFailure, true .. 83 } .. 84 if opts.priority != "" && !queue.Priority(opts.priority).Valid() { .. 85 fmt.Fprintf(stderr, "talk: --priority: ungültiger Wert %q (erlaubt: low, normal, high)\n", opts.priority) .. 86 return exitFailure, true .. 87 } .. 88 return 0, false .. 89 } 80 90 81 91 // pickProvider löst den Provider-Pin auf: CLI-Flag --provider schlägt 82 92 // `provider = "…"` aus der config.toml; leer = komplette Default-Kette. cmd/talk/wrap.go --- Go 1 package main 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "os/exec" 8 "path/filepath" 9 "strings" 10 "time" 11 12 "code.m3ta.dev/m3tam3re/talk/internal/config" 13 "code.m3ta.dev/m3tam3re/talk/internal/langdetect" 14 ) 15 16 // Kind nicht gefunden — Shell-Konvention 127. 17 const exitNotFound = 127 18 19 // runWrap ist der wrap-Eingangspfad (Task 07): `talk wrap -- 20 // [Argumente …]` führt den Befehl aus — stdin/stdout/stderr werden 21 // durchgereicht, stdout gehört allein dem Befehl —, misst die Wandzeit und 22 // sagt danach das Ergebnis an: „