blob: 892c3404d357c1fc4ccca5cf3019b53603364ed2 [file]
// 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()
}