From c4fb641094d7002404e33a46823fe6b68076f29a Mon Sep 17 00:00:00 2001 From: max Date: Thu, 10 Sep 2026 10:22:39 +0800 Subject: [PATCH] =?UTF-8?q?internal/cli=EF=BC=9A=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E4=BF=9D=E7=95=99=E6=97=97=E6=A8=99=E6=A9=9F=E5=88=B6=EF=BC=8C?= =?UTF-8?q?login=20add=20=E7=9A=84=20--url/--token=20=E4=B8=8D=E5=86=8D?= =?UTF-8?q?=E8=A2=AB=E5=85=A8=E5=9F=9F=E5=89=96=E6=9E=90=E6=90=B6=E8=B5=B0?= =?UTF-8?q?=EF=BC=88#18=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/cli/cli.go | 39 ++++++++++++++++++++++++--- internal/cli/exitcode_test.go | 50 +++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 3 deletions(-) diff --git a/internal/cli/cli.go b/internal/cli/cli.go index fc90bad..7469f5e 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -150,10 +150,31 @@ var globalFlags = map[string]bool{ "--timeout": true, } +// reservedFlags 回傳 dispatch 不得搶走的旗標名稱(含 -- 前綴):這些 +// 旗標屬於目前命令(或其子命令),即使與全域選項同名也必須原樣留給 +// 命令剖析。目前唯一與全域同名的是 login add 的 --url/--token +// (README「teai login add --url … --token …」)。其餘命令的旗標 +// (--repo、--has-work、--mine……)本來就不在 globalFlags 內,不受影響。 +func reservedFlags(name string) map[string]bool { + reserved := make(map[string]bool) + for _, f := range commandReservedFlags[name] { + reserved[f] = true + } + return reserved +} + +// commandReservedFlags 登記每個命令「與全域選項同名」的自有旗標。 +// 命令旗標優先留給命令;新命令有同姓旗標時在這裡登記(#18)。 +var commandReservedFlags = map[string][]string{ + "login": {"--url", "--token"}, +} + // extractGlobals 從 args 中「抽取」全域選項(可出現在命令之後、與命令 // 旗標交錯),其餘引數依原順序保留給子命令剖析。未知的選項一律留給 // 子命令處理(可能是命令旗標,如 --has-work),不在這裡報錯。 -func extractGlobals(g *Globals, args []string) ([]string, error) { +// reserved 是目前命令保留的旗標名稱(含 -- 前綴,見 reservedFlags): +// 列於其中的選項即使與全域同名也原樣留給命令,命令旗標優先(#18)。 +func extractGlobals(g *Globals, args []string, reserved map[string]bool) ([]string, error) { rest := make([]string, 0, len(args)) applyGlobal := func(name, value string) error { switch name { @@ -197,6 +218,17 @@ func extractGlobals(g *Globals, args []string) ([]string, error) { continue } name, inline, hasInline := strings.Cut(arg, "=") + if reserved[name] { + // 命令保留旗標:即使與全域選項同名也留給命令剖析(#18)。 + rest = append(rest, arg) + if !hasInline && i+1 < len(args) { + // 旗標值跟在下一個引數;一併保留,不當成全域值。 + rest = append(rest, args[i+1]) + i++ + } + i++ + continue + } _, ok := globalFlags[name] if !ok { // 不是全域選項:留給子命令(可能是 --has-work 這類命令旗標)。 @@ -293,11 +325,12 @@ func parseGlobals(g *Globals, args []string) ([]string, error) { // 旗標剖析交由各命令自行處理(不同命令有不同旗標,如 pulls --mine); // 全域選項允許出現在命令之後(README 範例:teai next --output table), // dispatch 先從 args 剝離全域選項併入 Globals,其餘(含命令自己的旗標, -// 如 --has-work)原樣交給命令,不會誤判為未知全域選項。 +// 如 --has-work)原樣交給命令,不會誤判為未知全域選項。與全域選項 +// 同名的命令旗標(登記於 commandReservedFlags)優先留給命令(#18)。 func dispatch(env *Env, name string, args []string) int { cmd := commands[name] g := env.Globals - rest, err := extractGlobals(&g, args) + rest, err := extractGlobals(&g, args, reservedFlags(name)) if err != nil { fmt.Fprintf(env.Err, "teai: %v\n\n", err) printUsage(env.Err) diff --git a/internal/cli/exitcode_test.go b/internal/cli/exitcode_test.go index 1e449d0..1130c4b 100644 --- a/internal/cli/exitcode_test.go +++ b/internal/cli/exitcode_test.go @@ -107,6 +107,56 @@ func TestParseGlobalsStopsAtCommand(t *testing.T) { } } +// TestExtractGlobalsReservedFlags 驗證 #18:與全域同名的命令旗標 +// (登記於 commandReservedFlags)必須留給命令,其餘全域選項照常抽取。 +func TestExtractGlobalsReservedFlags(t *testing.T) { + g := defaultGlobals() + rest, err := extractGlobals(&g, []string{ + "add", + "--url", "https://srv.example.com", // login add 自有旗標:保留 + "--token=tok", // 同上(inline 形式):保留 + "--name", "test-site", + "--output", "table", // 非保留:仍是全域 + }, reservedFlags("login")) + if err != nil { + t.Fatal(err) + } + want := []string{"add", "--url", "https://srv.example.com", "--token=tok", "--name", "test-site"} + if len(rest) != len(want) { + t.Fatalf("rest = %v, want %v", rest, want) + } + for i := range want { + if rest[i] != want[i] { + t.Fatalf("rest = %v, want %v", rest, want) + } + } + if g.Output != gitea.FormatTable { + t.Fatalf("output = %v, want table", g.Output) + } + if g.URL != defaultGlobals().URL || g.Token != "" { + t.Fatalf("reserved flags must not leak into globals: %+v", g) + } +} + +// TestExtractGlobalsNonReservedCommand 驗證 #13 行為不回歸:未登記保留 +// 旗標的命令(如 next/members/pulls),命令後的全域選項照常抽取、 +// 命令自己的旗標(--has-work 等)原樣保留。 +func TestExtractGlobalsNonReservedCommand(t *testing.T) { + g := defaultGlobals() + rest, err := extractGlobals(&g, []string{ + "members", "--has-work", "--output", "table", + }, reservedFlags("members")) + if err != nil { + t.Fatal(err) + } + if len(rest) != 2 || rest[0] != "members" || rest[1] != "--has-work" { + t.Fatalf("rest = %v, want [members --has-work]", rest) + } + if g.Output != gitea.FormatTable { + t.Fatalf("output = %v, want table", g.Output) + } +} + // errString 把字串轉成 error(測試輔助)。 type errStr string