From 3a17dfe19c1418eed8ceacc94ab5298c997ef74b Mon Sep 17 00:00:00 2001 From: idelchi Date: Mon, 20 Jul 2026 21:46:13 +0200 Subject: [PATCH 1/2] Add Required field to single-value arguments --- args.go | 5 +++ args_test.go | 75 +++++++++++++++++++++++++++++++++++++++++ godoc-current.txt | 1 + testdata/godoc-v3.x.txt | 1 + 4 files changed, 82 insertions(+) diff --git a/args.go b/args.go index b3acfd5ccc..9f77ccfe07 100644 --- a/args.go +++ b/args.go @@ -89,6 +89,7 @@ type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show + Required bool `json:"required"` // whether the argument is required or not Config C `json:"config"` // config for this argument similar to Flag Config value *T @@ -110,6 +111,10 @@ func (a *ArgumentBase[T, C, VC]) Usage() string { func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) { tracef("calling arg%[1] parse with args %[2]", a.Name, s) + if a.Required && len(s) == 0 { + return s, fmt.Errorf("required argument %q not set", a.Name) + } + var vc VC var t T value := vc.Create(a.Value, &t, a.Config) diff --git a/args_test.go b/args_test.go index 7ae337487f..a4f130a3f1 100644 --- a/args_test.go +++ b/args_test.go @@ -552,6 +552,81 @@ func TestSingleOptionalArg(t *testing.T) { } } +func TestSingleRequiredArg(t *testing.T) { + tests := []struct { + name string + args []string + argValue string + exp string + expErr string + }{ + { + name: "no args", + args: []string{"foo"}, + expErr: `required argument "sa" not set`, + }, + { + name: "no arg with def value", + args: []string{"foo"}, + argValue: "bar", + expErr: `required argument "sa" not set`, + }, + { + name: "one arg", + args: []string{"foo", "zbar"}, + exp: "zbar", + }, + { + name: "empty string arg", + args: []string{"foo", ""}, + exp: "", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cmd := buildMinimalTestCommand() + var s1 string + arg := &StringArg{ + Name: "sa", + Value: test.argValue, + Destination: &s1, + Required: true, + } + cmd.Arguments = []Argument{ + arg, + } + + err := cmd.Run(buildTestContext(t), test.args) + r := require.New(t) + if test.expErr != "" { + r.EqualError(err, test.expErr) + return + } + r.NoError(err) + r.Equal(test.exp, s1) + }) + } +} + +func TestChainedRequiredArgs(t *testing.T) { + cmd := buildMinimalTestCommand() + cmd.Arguments = []Argument{ + &StringArg{ + Name: "first", + Required: true, + }, + &StringArg{ + Name: "second", + Required: true, + }, + } + + r := require.New(t) + r.EqualError(cmd.Run(buildTestContext(t), []string{"foo", "one"}), `required argument "second" not set`) + r.NoError(cmd.Run(buildTestContext(t), []string{"foo", "one", "two"})) +} + func TestUnboundedArgs(t *testing.T) { arg := &StringArgs{ Min: 0, diff --git a/godoc-current.txt b/godoc-current.txt index d680c5f544..8f1889452e 100644 --- a/godoc-current.txt +++ b/godoc-current.txt @@ -294,6 +294,7 @@ type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show + Required bool `json:"required"` // whether the argument is required or not Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. diff --git a/testdata/godoc-v3.x.txt b/testdata/godoc-v3.x.txt index d680c5f544..8f1889452e 100644 --- a/testdata/godoc-v3.x.txt +++ b/testdata/godoc-v3.x.txt @@ -294,6 +294,7 @@ type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct { Value T `json:"value"` // the default value of this argument Destination *T `json:"-"` // the destination point for this argument UsageText string `json:"usageText"` // the usage text to show + Required bool `json:"required"` // whether the argument is required or not Config C `json:"config"` // config for this argument similar to Flag Config // Has unexported fields. From 526922f7a17c07a08d0834168a12d8d0ed412948 Mon Sep 17 00:00:00 2001 From: idelchi Date: Mon, 20 Jul 2026 21:49:53 +0200 Subject: [PATCH 2/2] Render optional single-value arguments bracketed in help --- args.go | 5 +++- args_test.go | 8 +++++- docs/v3/examples/arguments/advanced.md | 39 ++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/args.go b/args.go index 9f77ccfe07..dee84091e2 100644 --- a/args.go +++ b/args.go @@ -104,7 +104,10 @@ func (a *ArgumentBase[T, C, VC]) Usage() string { return a.UsageText } - usageFormat := "%[1]s" + usageFormat := "[%[1]s]" + if a.Required { + usageFormat = "%[1]s" + } return fmt.Sprintf(usageFormat, a.Name) } diff --git a/args_test.go b/args_test.go index a4f130a3f1..3f7c647418 100644 --- a/args_test.go +++ b/args_test.go @@ -422,10 +422,16 @@ func TestArgUsage(t *testing.T) { tests := []struct { name string usage string + required bool expected string }{ { name: "default", + expected: "[ia]", + }, + { + name: "required", + required: true, expected: "ia", }, { @@ -436,7 +442,7 @@ func TestArgUsage(t *testing.T) { } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - arg.UsageText = test.usage + arg.UsageText, arg.Required = test.usage, test.required require.Equal(t, test.expected, arg.Usage()) }) } diff --git a/docs/v3/examples/arguments/advanced.md b/docs/v3/examples/arguments/advanced.md index 7ab3da9a55..5eb91341d5 100644 --- a/docs/v3/examples/arguments/advanced.md +++ b/docs/v3/examples/arguments/advanced.md @@ -111,6 +111,45 @@ Some of the basic types arguments supported are This is ok for single value arguments. Any number of these single value arguments can be concatenated in the `Arguments` slice field of `Command`. +Single value arguments are optional by default. If the argument is not provided the default `Value` is used instead. +You can mark a single value argument as *required* by setting the `Required` field to `true`. If a user does not +provide a required argument, they will be shown an error message. + + +```go +package main + +import ( + "fmt" + "log" + "os" + "context" + + "github.com/urfave/cli/v3" +) + +func main() { + cmd := &cli.Command{ + Arguments: []cli.Argument{ + &cli.IntArg{ + Name: "someint", + Required: true, + }, + }, + Action: func(ctx context.Context, cmd *cli.Command) error { + fmt.Printf("We got %d", cmd.IntArg("someint")) + return nil + }, + } + + if err := cmd.Run(context.Background(), os.Args); err != nil { + log.Fatal(err) + } +} +``` + The library also support multi value arguments for e.g