diff --git a/cli.go b/cli.go index e685f3191e..142161355f 100644 --- a/cli.go +++ b/cli.go @@ -30,7 +30,8 @@ import ( ) var ( - isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on" + isTracingOn = os.Getenv("URFAVE_CLI_TRACING") == "on" + isArghModeOn = os.Getenv("URFAVE_CLI_ARGH_MODE") == "on" ) func tracef(format string, a ...any) { diff --git a/command.go b/command.go index b032239153..ca6ea3c73e 100644 --- a/command.go +++ b/command.go @@ -2,6 +2,7 @@ package cli import ( "context" + "encoding/json" "flag" "fmt" "io" @@ -10,6 +11,8 @@ import ( "reflect" "sort" "strings" + + "github.com/urfave/argh" ) const ( @@ -139,6 +142,12 @@ type Command struct { isInError bool // track state of defaults didSetupDefaults bool + + // fields used by argh-based parsing {{ + parserCfg *argh.ParserConfig + cfg *argh.CommandConfig + stringValues map[string]string + // }} } // FullName returns the full name of the command. @@ -175,6 +184,16 @@ func (cmd *Command) setupDefaults(osArgs []string) { isRoot := cmd.parent == nil tracef("isRoot? %[1]v (cmd=%[2]q)", isRoot, cmd.Name) + if isArghModeOn { + if isRoot { + cmd.parserCfg = argh.NewParserConfig() + cmd.cfg = cmd.parserCfg.Prog + cmd.cfg.NValue = argh.ZeroOrMoreValue + } + + cmd.stringValues = map[string]string{} + } + if cmd.ShellComplete == nil { tracef("setting default ShellComplete (cmd=%[1]q)", cmd.Name) cmd.ShellComplete = DefaultCompleteWithFlags(cmd) @@ -287,6 +306,49 @@ func (cmd *Command) setupDefaults(osArgs []string) { disableSliceFlagSeparator = cmd.DisableSliceFlagSeparator } +func (cmd *Command) setupParser() { + tracef("setting up parser (cmd=%[1]q)", cmd.Name) + + // TODO: add support for named positional args {{ + cmd.cfg.NValue = 1 + cmd.cfg.ValueNames = []string{} + // }} + + for _, loopFlag := range cmd.Flags { + fl := loopFlag + + // TODO: add support for flags with value counts other than 0 and 1 + flagNValue := argh.NValue(0) + canonicalName := fl.Names()[0] + + if v, ok := fl.(DocGenerationFlag); ok && v.TakesValue() { + flagNValue = argh.NValue(1) + canonicalName = v.CanonicalName() + } + + flCfg := &argh.FlagConfig{ + NValue: flagNValue, + On: func(cf argh.CommandFlag) { + tracef("flag %[1]q set (cmd=%[2]q)", fl.Names(), cmd.Name) + + for k, v := range cf.Values { + cmd.stringValues[fmt.Sprintf("%[1]v.%[2]v", canonicalName, cf.Name, k)] = v + } + }, + } + + for _, name := range fl.Names() { + tracef("setting flag config in parser config name=%[1]q cfg=%+#[2]v flCfg=%+#[3]v (cmd=%[4]q)", name, cmd.cfg, flCfg, cmd.Name) + + cmd.cfg.SetFlagConfig(name, flCfg) + } + } + + for _, subCmd := range cmd.Commands { + subCmd.setupParser() + } +} + func (cmd *Command) setupCommandGraph() { tracef("setting up command graph (cmd=%[1]q)", cmd.Name) @@ -300,6 +362,21 @@ func (cmd *Command) setupCommandGraph() { func (cmd *Command) setupSubcommand() { tracef("setting up self as sub-command (cmd=%[1]q)", cmd.Name) + if isArghModeOn { + cfg, ok := cmd.parent.cfg.GetCommandConfig(cmd.Name) + if !ok { + cfg = argh.CommandConfig{} + } + + cfg.On = func(cf argh.CommandFlag) { + tracef("received command flag=%+#[1]v (cmd=%[2]q)", cmd.Name) + } + + tracef("setting parser config on parent cfg=%+#[1]v (cmd=%[2]q)", &cfg, cmd.Name) + cmd.parent.cfg.SetCommandConfig(cmd.Name, &cfg) + cmd.cfg = &cfg + } + cmd.ensureHelp() tracef("setting command categories (cmd=%[1]q)", cmd.Name) @@ -338,6 +415,15 @@ func (cmd *Command) ensureHelp() { // arguments are parsed according to the Flag and Command // definitions and the matching Action functions are run. func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { + if isArghModeOn { + if err := cmd.runWithArgh(ctx, osArgs); err != nil { + tracef("setting deferErr from argh error: %[1]v", err) + deferErr = err + } + + return + } + tracef("running with arguments %[1]q (cmd=%[2]q)", osArgs, cmd.Name) cmd.setupDefaults(osArgs) @@ -524,6 +610,44 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { return deferErr } +func (cmd *Command) runWithArgh(ctx context.Context, osArgs []string) error { + cmd.setupParser() + + parseTree, parseErr := argh.ParseArgs(osArgs, cmd.Root().parserCfg) + if parseErr != nil { + tracef("argh parse error: %[1]v", parseErr) + + return parseErr + } + + if isTracingOn { + if b, err := json.Marshal(argh.ToAST(parseTree.Nodes)); err == nil { + tracef("argh ast: %[1]s", string(b)) + } + + if v, ok := os.LookupEnv("URFAVE_CLI_ARGH_DUMP_JSON"); ok { + if fd, err := os.Create(v); err == nil { + if err := json.NewEncoder(fd).Encode( + map[string]any{ + "cfg": cmd.cfg, + "string_values": cmd.stringValues, + "ast": argh.ToAST(parseTree.Nodes), + "err": parseErr, + }, + ); err != nil { + tracef("URFAVE_CLI_ARGH_DUMP_JSON err: %[1]v", err) + } + + if err := fd.Close(); err != nil { + tracef("URFAVE_CLI_ARGH_DUMP_JSON err: %[1]v", err) + } + } + } + } + + return nil +} + func (cmd *Command) checkHelp() bool { tracef("checking if help is wanted (cmd=%[1]q)", cmd.Name) diff --git a/flag.go b/flag.go index 5332b22201..a24994732c 100644 --- a/flag.go +++ b/flag.go @@ -123,6 +123,8 @@ type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool + CanonicalName() string + // GetUsage returns the usage string for the flag GetUsage() string diff --git a/flag_impl.go b/flag_impl.go index e4930195b9..3cbc91f7ed 100644 --- a/flag_impl.go +++ b/flag_impl.go @@ -210,6 +210,10 @@ func (f *FlagBase[T, C, V]) IsSet() bool { return f.hasBeenSet } +func (f *FlagBase[T, C, V]) CanonicalName() string { + return f.Name +} + // Names returns the names of the flag func (f *FlagBase[T, C, V]) Names() []string { return FlagNames(f.Name, f.Aliases) diff --git a/go.mod b/go.mod index 34b9ceda5b..1e4f75fcd5 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.18 require ( github.com/stretchr/testify v1.8.4 + github.com/urfave/argh v0.2.1-0.20230702123329-da1ca8be8db5 github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 ) diff --git a/go.sum b/go.sum index b806593b77..b2f14bea86 100644 --- a/go.sum +++ b/go.sum @@ -4,6 +4,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/urfave/argh v0.2.1-0.20230702123329-da1ca8be8db5 h1:Wlo6nZHECi5IysPUZKOq8KN8AoT/6KsmdGBRidH6n1M= +github.com/urfave/argh v0.2.1-0.20230702123329-da1ca8be8db5/go.mod h1:NUm2NMpNFfFIzbpD9qe98ZL7G3TYWiH1bynhvbJw2pY= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/godoc-current.txt b/godoc-current.txt index 5229697ea0..2234a715dd 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -516,6 +516,8 @@ type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool + CanonicalName() string + // GetUsage returns the usage string for the flag GetUsage() string @@ -645,6 +647,8 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment +func (f *FlagBase[T, C, V]) CanonicalName() string + func (f *FlagBase[T, C, V]) Get(cmd *Command) T Get returns the flag’s value in the given Command. diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index 5229697ea0..2234a715dd 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -516,6 +516,8 @@ type DocGenerationFlag interface { // TakesValue returns true if the flag takes a value, otherwise false TakesValue() bool + CanonicalName() string + // GetUsage returns the usage string for the flag GetUsage() string @@ -645,6 +647,8 @@ type FlagBase[T any, C any, VC ValueCreator[T, C]] struct { func (f *FlagBase[T, C, V]) Apply(set *flag.FlagSet) error Apply populates the flag given the flag set and environment +func (f *FlagBase[T, C, V]) CanonicalName() string + func (f *FlagBase[T, C, V]) Get(cmd *Command) T Get returns the flag’s value in the given Command.