Skip to content

Named arguments don't get passed in cmd.Args() #2387

Closed

Description

@tvanriel

My urfave/cli version is

3.10.1

Checklist

  • Are you running the latest v3 release? The list of releases is here.
  • Did you check the manual for your release? The v3 manual is here
  • Did you perform a search about this problem? Here's the GitHub guide about searching.

Dependency Management

  • My project is using go modules.

Describe the bug

Subcommands don't seem to get passed their arguments correctly.

To reproduce

package bug_test

import (
	"context"
	"fmt"

	"github.com/urfave/cli/v3"
)

func Example()  {
	_ = Root().Run(context.Background(), []string{"example", "sub", "value"})

	//Output:
	// value
}


func Root() *cli.Command {

	return &cli.Command{
		Name: "example",
		Commands: []*cli.Command {
			Sub(),
		},
	}
}

func Sub() *cli.Command {
	return &cli.Command {
		Name: "sub",
		Arguments: []cli.Argument{
			&cli.StringArg{
				Name: "test",
			},
		},
		Action: func(ctx context.Context, c *cli.Command) error {
			fmt.Print(c.Args().First())
			return nil
		},
	}
}

Observed behavior

--- FAIL: Example (0.00s)
got:

want:
value
FAIL
FAIL    github.com/tvanriel/minimal-example-urfave-cli-subcommand-bug   0.002s
FAIL

Expected behavior

I would have expected the included code to print value, as was passed in the arguments.

Additional context

Want to fix this yourself?

I did some digging and it seems to be related to a shadowed reference to rargs that gets set while parsing args and then cleaned before being saved in cmd.parsedArgs but couldn't get a fix to work.

We'd love to have more contributors on this project! If the fix for
this bug is easily explained and very small, feel free to create a
pull request for it.

Run go version and paste its output here

go version go1.26.4-X:nodwarf5 linux/amd64

Run go env and paste its output here

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/tvanriel/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/tvanriel/.config/go/env'
GOEXE=''
GOEXPERIMENT='nodwarf5'
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3424211875=/tmp/go-build
 -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/tvanriel/Projects/Personal/minimal-example-urfave-cli-subcommand-bug/go.mod'
GOMODCACHE='/home/tvanriel/go/pkg/mod'
GONOPROXY='gitlab.figonet.nl,gitlab.bauwatch.tech'
GONOSUMDB='gitlab.figonet.nl,gitlab.bauwatch.tech'
GOOS='linux'
GOPATH='/home/tvanriel/go'
GOPRIVATE='gitlab.figonet.nl,gitlab.bauwatch.tech'
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/tvanriel/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.26.4-X:nodwarf5'
GOWORK=''
PKG_CONFIG='pkg-config'

Activity

  1. added
    kind/bugdescribes or fixes a bug
    status/triagemaintainers still need to look into this
    area/v3relates to / is being considered for v3
    on Jul 8, 2026
  2. changed the title [-]your bug title goes here[/-] [+]subcommands don't get their arguments passed.[/+] on Jul 8, 2026
  3. BrandonIrizarry commented on Jul 8, 2026

    @BrandonIrizarry
    Contributor

    From what I can tell, this has nothing to do with subcommands per se - it seems to affect all commands. For example, the following minimal example works:

    package main
    
    import (
    	"context"
    	"fmt"
    	"os"
    
    	"github.com/urfave/cli/v3"
    )
    
    var cmd = &cli.Command{
    	Action: func(ctx context.Context, c *cli.Command) error {
    		fmt.Println(c.Args().First())
    		return nil
    	},
    }
    
    func main() {
    	if err := cmd.Run(context.Background(), os.Args); err != nil {
    		panic(err)
    	}
    }

    However, as soon as I name the argument (as what your example illustrates), it fails:

    package main
    
    import (
    	"context"
    	"fmt"
    	"os"
    
    	"github.com/urfave/cli/v3"
    )
    
    var cmd = &cli.Command{
            // Adding an Arguments field breaks the example.
    	Arguments: []cli.Argument{
    		&cli.StringArg{
    			Name: "test",
    		},
    	},
    	Action: func(ctx context.Context, c *cli.Command) error {
    		fmt.Println(c.Args().First())
    		return nil
    	},
    }
    
    func main() {
    	if err := cmd.Run(context.Background(), os.Args); err != nil {
    		panic(err)
    	}
    }
  4. changed the title [-]subcommands don't get their arguments passed.[/-] [+]Named arguments don't get passed in cmd.Args()[/+] on Jul 9, 2026
  5. BrandonIrizarry commented on Jul 9, 2026

    @BrandonIrizarry
    Contributor

    From looking at the code (see command_run.go, at around line 363) I'm starting to realize something (correct me if I'm wrong): this behavior is expected. if you take cmd.Args() to refer to the slice of all non-named args.

    Say my command looks like this:

    var cmd = &cli.Command{
    	Arguments: []cli.Argument{
    		&cli.StringArg{
    			Name: "test",
    		},
    	},
    	Action: func(ctx context.Context, cmd *cli.Command) error {
    		fmt.Println("test's value: ", cmd.StringArg("test"))
    		fmt.Println("first non-named arg: ", cmd.Args().First())
    
    		return nil
    	},
    }
    
    func main() {
    	if err := cmd.Run(context.Background(), os.Args); err != nil {
    		panic(err)
    	}
    }

    If I run go run . boo bar, the output will be:

    test's value:  boo
    first non-named arg:  bar
    

    Similarly, if you would like to refer to a range of arguments, you'd use something like this:

    var cmd = &cli.Command{
    	Arguments: []cli.Argument{
    		&cli.StringArgs{
    			Name: "test",
    			Min:  1,
    			Max:  3,
    		},
    	},
    	Action: func(ctx context.Context, cmd *cli.Command) error {
    		fmt.Println("test's value: ", cmd.StringArgs("test"))
    		fmt.Println("first non-named arg: ", cmd.Args().First())
    
    		return nil
    	},
    }
    
    func main() {
    	if err := cmd.Run(context.Background(), os.Args); err != nil {
    		panic(err)
    	}
    }

    Output of go run .:

    panic: sufficient count of arg test not provided, given 0 expected 1
    
    goroutine 1 [running]:
    main.main()
    	/home/bci/github.com/BrandonIrizarry/cli_test/main.go:30 +0x57
    exit status 2
    

    Output of go run . boo:

    test's value:  [boo]
    first non-named arg: 
    

    Output of go run . boo bar:

    test's value:  [boo bar]
    first non-named arg:
    

    Output of go run . boo bar bats:

    test's value:  [boo bar bats]
    first non-named arg: 
    

    Output of go run . boo bar bats fourth:

    test's value:  [boo bar bats]
    first non-named arg:  fourth
    

    Now we've hit cmd.Args() in this case.

    That having been said, I did try to patch the code by changing this line (around line 379 in command_run.go):

    cmd.parsedArgs = &stringSliceArgs{v: rargs}

    to

    cmd.parsedArgs = &stringSliceArgs{v: cmd.Args().Slice()}

    I found that this does, in fact, get the behavior you and I expected (that is, that cmd.Args() should refer to the whole set of args at all times). However, this seems to break several tests. From what I can tell, the tests break because they don't set cmd.Action, which then defaults to a "show help" action (again, I'm a bit fuzzy here, but that's what I observed.)

    PS: there's also an interesting note in the manual that alludes to my observation that the current behavior is the expected one:

    It goes without saying that the chain of arguments set in the Arguments slice need to be consistent. Generally a glob argument(max=-1) should be set for the argument at the end of the slice. To glob args we aren't interested in we could add the following to the end of the Arguments slice and retrieve them as a slice:

    &StringArgs{
        Max: -1,
    },

    Though at this point I'd also vouch for an update to the manual documenting "mixing named arguments with cmd.Args()".

  6. dearchap commented on Aug 16, 2026

    @dearchap
    Contributor

    This is intended behavior, not a bug.

    When a command declares named arguments via the Arguments field, each named argument consumes the positional arguments it needs from the command line. cmd.Args() returns only the arguments that were not consumed by a named argument. To retrieve the value of a named argument, use cmd.{Type}Arg(name), e.g. cmd.StringArg("test") (which returns value in the reproduction from the issue).

    This behavior dates back to the original arguments feature (#1074) and is relied upon by the trailing StringArgs{Max: -1} glob pattern documented in the manual.

    I've added a PR that documents this behavior clearly (#2411): it clarifies the Args() doc comment, adds a "Mixing named arguments with cmd.Args()" section to the advanced arguments docs (with a runnable example), adds a note to the arguments basics page, and includes a test locking in the semantics.

  7. added a commit that references this issue on Aug 16, 2026
    7ffd2b6
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area/v3relates to / is being considered for v3kind/bugdescribes or fixes a bugstatus/triagemaintainers still need to look into this

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions