Positional arguments are values passed after the command name that aren't flags. The framework supports two approaches: struct-tagged named arguments and the Args type for dynamic access.
Use the arg tag to define named positional arguments:
type CopyCmd struct {
Src string `arg:"src" help:"Source file"`
Dst string `arg:"dst" help:"Destination file"`
}
func (c *CopyCmd) Run(ctx context.Context) error {
return copyFile(c.Src, c.Dst)
}$ copy source.txt dest.txt
Arguments are assigned in struct field order.
If the arg tag value is empty, the name is derived from the field name:
type Cmd struct {
SourceFile string `arg:"" help:"Source file"` // arg name: source-file
Target string `arg:"target"` // arg name: target
}Non-slice arguments are required by default. Use required:"false" to make optional:
type Cmd struct {
Input string `arg:"input" help:"Input file"` // required
Output string `arg:"output" required:"false" help:"Output file"` // optional
}$ cmd input.txt # Output = "" (optional)
$ cmd input.txt output.txt # Output = "output.txt"
Provide defaults for optional arguments:
type Cmd struct {
Input string `arg:"input" help:"Input file"`
Output string `arg:"output" required:"false" default:"out.txt" help:"Output file"`
}$ cmd input.txt # Output = "out.txt"
$ cmd input.txt foo.txt # Output = "foo.txt"
Arguments can fall back to environment variables:
type Cmd struct {
Env string `arg:"env" env:"TARGET_ENV" help:"Target environment"`
}$ export TARGET_ENV=production
$ cmd # Env = "production" (from env)
$ cmd staging # Env = "staging" (positional wins)Priority: positional arg > env var > default > zero value.
Restrict arguments to specific values:
type Cmd struct {
Env string `arg:"env" enum:"dev,staging,prod" help:"Target environment"`
}$ cmd dev # OK
$ cmd other # Error: env must be one of [dev,staging,prod]
A slice field consumes all remaining arguments:
type CatCmd struct {
Files []string `arg:"files" help:"Files to concatenate"`
}$ cat file1.txt file2.txt file3.txt
# Files = ["file1.txt", "file2.txt", "file3.txt"]
Slice arguments are optional by default (empty slice is valid). Use required:"" if at least one value is needed.
Named arguments are consumed first, then the slice gets the rest:
type Cmd struct {
Output string `arg:"output" help:"Output file"`
Inputs []string `arg:"inputs" help:"Input files"`
}$ cmd out.txt a.txt b.txt c.txt
# Output = "out.txt"
# Inputs = ["a.txt", "b.txt", "c.txt"]
For dynamic argument handling, use cli.Args:
type GrepCmd struct {
Pattern string `arg:"pattern" help:"Search pattern"`
Args cli.Args // remaining arguments (no tag needed)
}$ grep "error" file1.txt file2.txt
# Pattern = "error"
# Args = ["file1.txt", "file2.txt"]
cli.Args provides convenience methods:
| Method | Description |
|---|---|
Len() |
Number of arguments |
Empty() |
Returns true if no arguments |
First() |
First argument or empty string |
Last() |
Last argument or empty string |
Get(i) |
Argument at index or empty string |
Contains(s) |
Check if argument exists |
Index(s) |
Index of argument or -1 |
Tail() |
All arguments after the first |
func (g *GrepCmd) Run(ctx context.Context) error {
if g.Args.Empty() {
// read from stdin
}
for _, file := range g.Args {
// process each file
}
return nil
}- Only one
cli.Argsfield is allowed per command - No tag is required on the
Argsfield Argsis populated after named arg fieldsArgsreceives all remaining arguments not consumed by named args
| Tag | Type | Description |
|---|---|---|
arg |
string | Argument name. If empty, derived from field name. |
help |
string | Description shown in help output |
default |
string | Default value when not provided |
required |
presence | Argument must be provided. Non-slice args default to required. |
enum |
string | Comma-separated list of allowed values |
env |
string | Environment variable fallback |
mask |
string | Display instead of default in help (e.g., **** for secrets) |
For custom validation logic, implement ArgsValidator:
type Cmd struct {
Args cli.Args
}
func (c *Cmd) ValidateArgs(args []string) error {
if len(args) == 0 {
return errors.New("at least one file required")
}
for _, arg := range args {
if !strings.HasSuffix(arg, ".txt") {
return fmt.Errorf("invalid file: %s (must be .txt)", arg)
}
}
return nil
}The framework provides common validators:
func (c *Cmd) ValidateArgs(args []string) error {
return cli.ExactArgs(2)(args) // requires exactly 2 args
}| Validator | Description |
|---|---|
ExactArgs(n) |
Exactly n arguments |
MinArgs(n) |
At least n arguments |
MaxArgs(n) |
At most n arguments |
RangeArgs(lo, hi) |
Between lo and hi arguments (inclusive) |
NoArgs |
No arguments allowed |
// Examples
cli.ExactArgs(2) // exactly 2
cli.MinArgs(1) // at least 1
cli.MaxArgs(5) // at most 5
cli.RangeArgs(1, 3) // 1 to 3
cli.NoArgs // none allowed (not a function)For wrapper commands that forward args to child processes, implement Passthrougher:
type ExecCmd struct {
Verbose bool `flag:"verbose" help:"Verbose output"`
Args cli.Args
}
func (e *ExecCmd) Passthrough() bool { return true }In passthrough mode, unknown flags become positional arguments instead of errors:
$ exec --verbose --unknown-flag value
# Verbose = true
# Args = ["--unknown-flag", "value"]
When a command has both positional arguments and embedded subcommands, arguments are consumed before subcommand resolution:
type UserCmd struct {
ID int `arg:"id" help:"User ID"`
Delete DeleteCmd // subcommand
Rename RenameCmd // subcommand
}
type DeleteCmd struct{}
func (d *DeleteCmd) Name() string { return "delete" }
func (d *DeleteCmd) Run(ctx context.Context) error {
userID := cli.Get[int](ctx, "id") // access parent's arg
return deleteUser(userID)
}$ app user 42 delete
# UserCmd.ID = 42
# DeleteCmd runs with ID accessible via context
This enables patterns like:
$ app user 42 delete --force
$ app user 42 rename newname
$ app resource abc-123 get
$ app resource abc-123 update --field value
Arguments are stored in context and accessible from subcommands:
func (d *DeleteCmd) Run(ctx context.Context) error {
userID := cli.Get[int](ctx, "id")
// or with checking
userID, ok := cli.Lookup[int](ctx, "id")
if !ok {
return errors.New("missing user ID")
}
return deleteUser(userID)
}Arguments appear in the usage line and arguments section:
Usage:
copy <src> <dst> [flags]
Arguments:
src Source file
dst Destination file
Optional arguments are shown in brackets:
Usage:
cmd <input> [output] [flags]
Slice arguments are shown with ellipsis:
Usage:
cat [files...] [flags]
Arguments support the same types as flags:
stringint,int64,uint,uint64float64booltime.Durationtime.Time*url.URLnet.IP[]Tfor any scalar type T- Custom types implementing
FlagUnmarshaler
- Subcommands — Command hierarchies
- Lifecycle — Hooks for setup and validation
- Flags — Flag parsing in detail