diff --git a/args.go b/args.go index b3acfd5ccc..dee84091e2 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 @@ -103,13 +104,20 @@ 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) } 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..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()) }) } @@ -552,6 +558,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/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