| // Copyright 2026 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package cmd |
| |
| import ( |
| "flag" |
| "io" |
| "strings" |
| ) |
| |
| // normalize scans command-line arguments to find the top-level subcommand, |
| // separating global application flags from subcommand arguments without executing FlagSet.Parse. |
| // |
| // Returns: |
| // - cmd: the resolved target subcommand (or &app.serve if none is specified). |
| // If flag validation errors, cmd is returned populated so callers can display contextual command help. |
| // - globalArgs: flags belonging to global application scope. |
| // - cmdArgs: arguments and flags belonging to cmd. |
| func normalize(app *application, args []string) (cmd command, globalArgs, cmdArgs []string, err error) { |
| silentFlagSet := func(name string, cmds ...command) *flag.FlagSet { |
| fs := flag.NewFlagSet(name, flag.ContinueOnError) |
| fs.SetOutput(io.Discard) |
| for _, c := range cmds { |
| addCommandFlags(fs, c) |
| } |
| return fs |
| } |
| |
| findSubcommand := func(curr command, name string) command { |
| for _, c := range getSubcommands(curr) { |
| if c.Name() == name { |
| return c |
| } |
| } |
| return nil |
| } |
| |
| // Flag sets to be used for flag name/type lookup. |
| appFlagSet := silentFlagSet(app.Name(), app) |
| serveFlagSet := silentFlagSet("serve", &app.serve) |
| |
| var preServeArgs []string |
| |
| i := 0 |
| for i < len(args) { |
| arg := args[i] |
| |
| if arg == "--" { |
| // e.g. gopls -v -- check file.go |
| // gopls vulncheck -- -mode=... |
| if i+1 < len(args) { |
| if matched := findSubcommand(app, args[i+1]); matched != nil { |
| cmd = matched |
| i++ // skip "--" |
| cmdArgs = append(cmdArgs, args[i+1:]...) |
| break |
| } |
| } |
| i++ |
| cmd = &app.serve |
| cmdArgs = append(cmdArgs, args[i:]...) |
| break |
| } |
| |
| // expect a valid subcommand if not a flag. |
| if arg == "-" || !strings.HasPrefix(arg, "-") { |
| if matched := findSubcommand(app, arg); matched != nil { |
| cmd = matched |
| i++ |
| cmdArgs = append(cmdArgs, args[i:]...) |
| break |
| } |
| break |
| } |
| |
| // below: arg is a string that has "-" as the prefix. |
| cleanArg := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-") |
| name, _, hasValue := strings.Cut(cleanArg, "=") |
| |
| if name == "h" || name == "help" { // -h or -help |
| globalArgs = append(globalArgs, arg) |
| i++ |
| continue |
| } |
| |
| if appFlag := appFlagSet.Lookup(name); appFlag != nil { |
| consumed, err := consume(args, i, appFlag, name, hasValue) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| globalArgs = append(globalArgs, consumed...) |
| i += len(consumed) |
| continue |
| } |
| if serveFlag := serveFlagSet.Lookup(name); serveFlag != nil { |
| consumed, err := consume(args, i, serveFlag, name, hasValue) |
| if err != nil { |
| return nil, nil, nil, err |
| } |
| preServeArgs = append(preServeArgs, consumed...) |
| i += len(consumed) |
| continue |
| } |
| return nil, nil, nil, commandLineErrorf("unknown flag: %s", arg) |
| } |
| |
| if cmd == nil { |
| if i < len(args) { |
| return nil, nil, nil, commandLineErrorf("unknown command %q", args[i]) |
| } |
| cmd = &app.serve |
| cmdArgs = preServeArgs |
| } else if cmd.Name() == "serve" { |
| // For backwards compatibility, allow flags to be placed after serve. |
| cmdArgs = append(preServeArgs, cmdArgs...) |
| } else if len(preServeArgs) > 0 { |
| // All explicitly specified subcommands other than serve must |
| // follow strict flag ordering. |
| arg := preServeArgs[0] |
| cleanArg := strings.TrimPrefix(strings.TrimPrefix(arg, "-"), "-") |
| name, _, _ := strings.Cut(cleanArg, "=") |
| if hasFlag(cmd, name) { |
| return cmd, nil, nil, commandLineErrorf("flag -%s must be placed after the command %s", name, cmd.Name()) |
| } |
| if sub := findSubcommandWithFlag(app, name); sub != nil { |
| return cmd, nil, nil, commandLineErrorf("flag -%s belongs to subcommand %s", name, sub.Name()) |
| } |
| |
| return cmd, nil, nil, commandLineErrorf("flag provided but not defined: -%s", name) |
| } |
| |
| return cmd, globalArgs, cmdArgs, nil |
| } |
| |
| // consume returns the token(s) corresponding to flag f from args starting at index i. |
| func consume(args []string, i int, f *flag.Flag, name string, hasValue bool) ([]string, error) { |
| if hasValue || isBoolFlag(f) { |
| return args[i : i+1], nil |
| } |
| if i+1 >= len(args) { |
| return nil, commandLineErrorf("flag needs an argument: -%s", name) |
| } |
| return args[i : i+2], nil |
| } |
| |
| // isBoolFlag reports whether f is a boolean flag. |
| func isBoolFlag(f *flag.Flag) bool { |
| bf, ok := f.Value.(interface{ IsBoolFlag() bool }) |
| return ok && bf.IsBoolFlag() |
| } |