Compare commits

..
2 changed files with 90 additions and 7 deletions
+36 -3
View File
@@ -150,10 +150,31 @@ var globalFlags = map[string]bool{
"--timeout": true, "--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 中「抽取」全域選項(可出現在命令之後、與命令 // extractGlobals 從 args 中「抽取」全域選項(可出現在命令之後、與命令
// 旗標交錯),其餘引數依原順序保留給子命令剖析。未知的選項一律留給 // 旗標交錯),其餘引數依原順序保留給子命令剖析。未知的選項一律留給
// 子命令處理(可能是命令旗標,如 --has-work),不在這裡報錯。 // 子命令處理(可能是命令旗標,如 --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)) rest := make([]string, 0, len(args))
applyGlobal := func(name, value string) error { applyGlobal := func(name, value string) error {
switch name { switch name {
@@ -197,6 +218,17 @@ func extractGlobals(g *Globals, args []string) ([]string, error) {
continue continue
} }
name, inline, hasInline := strings.Cut(arg, "=") 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] _, ok := globalFlags[name]
if !ok { if !ok {
// 不是全域選項:留給子命令(可能是 --has-work 這類命令旗標)。 // 不是全域選項:留給子命令(可能是 --has-work 這類命令旗標)。
@@ -293,11 +325,12 @@ func parseGlobals(g *Globals, args []string) ([]string, error) {
// 旗標剖析交由各命令自行處理(不同命令有不同旗標,如 pulls --mine); // 旗標剖析交由各命令自行處理(不同命令有不同旗標,如 pulls --mine);
// 全域選項允許出現在命令之後(README 範例:teai next --output table), // 全域選項允許出現在命令之後(README 範例:teai next --output table),
// dispatch 先從 args 剝離全域選項併入 Globals,其餘(含命令自己的旗標, // dispatch 先從 args 剝離全域選項併入 Globals,其餘(含命令自己的旗標,
// 如 --has-work)原樣交給命令,不會誤判為未知全域選項。 // 如 --has-work)原樣交給命令,不會誤判為未知全域選項。與全域選項
// 同名的命令旗標(登記於 commandReservedFlags)優先留給命令(#18)。
func dispatch(env *Env, name string, args []string) int { func dispatch(env *Env, name string, args []string) int {
cmd := commands[name] cmd := commands[name]
g := env.Globals g := env.Globals
rest, err := extractGlobals(&g, args) rest, err := extractGlobals(&g, args, reservedFlags(name))
if err != nil { if err != nil {
fmt.Fprintf(env.Err, "teai: %v\n\n", err) fmt.Fprintf(env.Err, "teai: %v\n\n", err)
printUsage(env.Err) printUsage(env.Err)
+54 -4
View File
@@ -79,7 +79,7 @@ func TestExtractGlobals(t *testing.T) {
"--output", "table", "--output", "table",
"--timeout", "45s", "--timeout", "45s",
"next", "next",
}) }, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -100,7 +100,7 @@ func TestExtractGlobals(t *testing.T) {
func TestExtractGlobalsAfterCommand(t *testing.T) { func TestExtractGlobalsAfterCommand(t *testing.T) {
// 全域選項出現在命令之後也應抽出(README:teai next --output table)。 // 全域選項出現在命令之後也應抽出(README:teai next --output table)。
g := defaultGlobals() g := defaultGlobals()
rest, err := extractGlobals(&g, []string{"version", "--output", "table"}) rest, err := extractGlobals(&g, []string{"version", "--output", "table"}, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -124,7 +124,7 @@ func TestExtractGlobalsKeepsCommandFlags(t *testing.T) {
} }
for _, args := range cases { for _, args := range cases {
g := defaultGlobals() g := defaultGlobals()
rest, err := extractGlobals(&g, args) rest, err := extractGlobals(&g, args, nil)
if err != nil { if err != nil {
t.Fatalf("extractGlobals(%v) 不應報錯:%v", args, err) t.Fatalf("extractGlobals(%v) 不應報錯:%v", args, err)
} }
@@ -145,7 +145,7 @@ func TestExtractGlobalsKeepsCommandFlags(t *testing.T) {
func TestExtractGlobalsDoubleDashStops(t *testing.T) { func TestExtractGlobalsDoubleDashStops(t *testing.T) {
g := defaultGlobals() g := defaultGlobals()
rest, err := extractGlobals(&g, []string{"--output", "table", "--", "--url", "x"}) rest, err := extractGlobals(&g, []string{"--output", "table", "--", "--url", "x"}, nil)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -158,6 +158,56 @@ func TestExtractGlobalsDoubleDashStops(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(測試輔助)。 // errString 把字串轉成 error(測試輔助)。
type errStr string type errStr string