Skip to content

Commit 2b97d2e

Browse files
authored
Merge pull request #1833 from dearchap/issue_1074
Feat:(issue_1074) Add basic support for cmd args
2 parents 962dae8 + 250dbd2 commit 2b97d2e

File tree

9 files changed

+343
-3
lines changed

9 files changed

+343
-3
lines changed

args.go

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
package cli
22

3+
import (
4+
"fmt"
5+
"time"
6+
)
7+
38
type Args interface {
49
// Get returns the nth argument, or else a blank string
510
Get(n int) string
@@ -55,3 +60,87 @@ func (a *stringSliceArgs) Slice() []string {
5560
copy(ret, a.v)
5661
return ret
5762
}
63+
64+
type Argument interface {
65+
Parse([]string) ([]string, error)
66+
Usage() string
67+
}
68+
69+
type ArgumentBase[T any, C any, VC ValueCreator[T, C]] struct {
70+
Name string // the name of this argument
71+
Value T // the default value of this argument
72+
Destination *T // the destination point for this argument
73+
Values *[]T // all the values of this argument, only if multiple are supported
74+
UsageText string // the usage text to show
75+
Min int // the min num of occurrences of this argument
76+
Max int // the max num of occurrences of this argument, set to -1 for unlimited
77+
Config C // config for this argument similar to Flag Config
78+
}
79+
80+
func (a *ArgumentBase[T, C, VC]) Usage() string {
81+
if a.UsageText != "" {
82+
return a.UsageText
83+
}
84+
85+
usageFormat := ""
86+
if a.Min == 0 {
87+
if a.Max == 1 {
88+
usageFormat = "[%[1]s]"
89+
} else {
90+
usageFormat = "[%[1]s ...]"
91+
}
92+
} else {
93+
usageFormat = "%[1]s [%[1]s ...]"
94+
}
95+
return fmt.Sprintf(usageFormat, a.Name)
96+
}
97+
98+
func (a *ArgumentBase[T, C, VC]) Parse(s []string) ([]string, error) {
99+
tracef("calling arg%[1] parse with args %[2]", &a.Name, s)
100+
if a.Max == 0 {
101+
fmt.Printf("WARNING args %s has max 0, not parsing argument", a.Name)
102+
return s, nil
103+
}
104+
if a.Max != -1 && a.Min > a.Max {
105+
fmt.Printf("WARNING args %s has min[%d] > max[%d], not parsing argument", a.Name, a.Min, a.Max)
106+
return s, nil
107+
}
108+
109+
count := 0
110+
var vc VC
111+
var t T
112+
value := vc.Create(a.Value, &t, a.Config)
113+
values := []T{}
114+
115+
for _, arg := range s {
116+
if err := value.Set(arg); err != nil {
117+
return s, err
118+
}
119+
values = append(values, value.Get().(T))
120+
count++
121+
if count >= a.Max {
122+
break
123+
}
124+
}
125+
if count < a.Min {
126+
return s, fmt.Errorf("sufficient count of arg %s not provided, given %d expected %d", a.Name, count, a.Min)
127+
}
128+
129+
if a.Values == nil {
130+
a.Values = &values
131+
} else {
132+
*a.Values = values
133+
}
134+
135+
if a.Max == 1 && a.Destination != nil {
136+
*a.Destination = values[0]
137+
}
138+
return s[count:], nil
139+
}
140+
141+
type FloatArg = ArgumentBase[float64, NoConfig, floatValue]
142+
type IntArg = ArgumentBase[int64, IntegerConfig, intValue]
143+
type StringArg = ArgumentBase[string, StringConfig, stringValue]
144+
type StringMapArg = ArgumentBase[map[string]string, StringConfig, StringMap]
145+
type TimestampArg = ArgumentBase[time.Time, TimestampConfig, timestampValue]
146+
type UintArg = ArgumentBase[uint64, IntegerConfig, uintValue]

args_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestArgumentsRootCommand(t *testing.T) {
13+
14+
cmd := buildMinimalTestCommand()
15+
var ival int64
16+
var fval float64
17+
var fvals []float64
18+
cmd.Arguments = []Argument{
19+
&IntArg{
20+
Name: "ia",
21+
Min: 1,
22+
Max: 1,
23+
Destination: &ival,
24+
},
25+
&FloatArg{
26+
Name: "fa",
27+
Min: 0,
28+
Max: 2,
29+
Destination: &fval,
30+
Values: &fvals,
31+
},
32+
}
33+
34+
require.NoError(t, cmd.Run(context.Background(), []string{"foo", "10"}))
35+
require.Equal(t, int64(10), ival)
36+
37+
require.NoError(t, cmd.Run(context.Background(), []string{"foo", "12", "10.1"}))
38+
require.Equal(t, int64(12), ival)
39+
require.Equal(t, []float64{10.1}, fvals)
40+
41+
require.NoError(t, cmd.Run(context.Background(), []string{"foo", "13", "10.1", "11.09"}))
42+
require.Equal(t, int64(13), ival)
43+
require.Equal(t, []float64{10.1, 11.09}, fvals)
44+
45+
require.Error(t, errors.New("No help topic for '12.1"), cmd.Run(context.Background(), []string{"foo", "13", "10.1", "11.09", "12.1"}))
46+
require.Equal(t, int64(13), ival)
47+
require.Equal(t, []float64{10.1, 11.09}, fvals)
48+
}
49+
50+
func TestArgumentsSubcommand(t *testing.T) {
51+
52+
cmd := buildMinimalTestCommand()
53+
var ifval int64
54+
var svals []string
55+
var tval time.Time
56+
cmd.Commands = []*Command{
57+
{
58+
Name: "subcmd",
59+
Flags: []Flag{
60+
&IntFlag{
61+
Name: "foo",
62+
Value: 10,
63+
Destination: &ifval,
64+
},
65+
},
66+
Arguments: []Argument{
67+
&TimestampArg{
68+
Name: "ta",
69+
Min: 1,
70+
Max: 1,
71+
Destination: &tval,
72+
Config: TimestampConfig{
73+
Layout: time.RFC3339,
74+
},
75+
},
76+
&StringArg{
77+
Name: "sa",
78+
Min: 1,
79+
Max: 3,
80+
Values: &svals,
81+
},
82+
},
83+
},
84+
}
85+
86+
require.Error(t, errors.New("sufficient count of arg sa not provided, given 0 expected 1"), cmd.Run(context.Background(), []string{"foo", "subcmd", "2006-01-02T15:04:05Z"}))
87+
88+
require.NoError(t, cmd.Run(context.Background(), []string{"foo", "subcmd", "2006-01-02T15:04:05Z", "fubar"}))
89+
require.Equal(t, time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), tval)
90+
require.Equal(t, []string{"fubar"}, svals)
91+
92+
require.NoError(t, cmd.Run(context.Background(), []string{"foo", "subcmd", "--foo", "100", "2006-01-02T15:04:05Z", "fubar", "some"}))
93+
require.Equal(t, int64(100), ifval)
94+
require.Equal(t, time.Date(2006, time.January, 2, 15, 4, 5, 0, time.UTC), tval)
95+
require.Equal(t, []string{"fubar", "some"}, svals)
96+
}
97+
98+
func TestArgsUsage(t *testing.T) {
99+
arg := &IntArg{
100+
Name: "ia",
101+
Min: 0,
102+
Max: 1,
103+
}
104+
tests := []struct {
105+
name string
106+
min int
107+
max int
108+
expected string
109+
}{
110+
{
111+
name: "optional",
112+
min: 0,
113+
max: 1,
114+
expected: "[ia]",
115+
},
116+
{
117+
name: "zero or more",
118+
min: 0,
119+
max: 2,
120+
expected: "[ia ...]",
121+
},
122+
{
123+
name: "one",
124+
min: 1,
125+
max: 1,
126+
expected: "ia [ia ...]",
127+
},
128+
{
129+
name: "many",
130+
min: 2,
131+
max: 1,
132+
expected: "ia [ia ...]",
133+
},
134+
{
135+
name: "many2",
136+
min: 2,
137+
max: 0,
138+
expected: "ia [ia ...]",
139+
},
140+
{
141+
name: "unlimited",
142+
min: 2,
143+
max: -1,
144+
expected: "ia [ia ...]",
145+
},
146+
}
147+
for _, test := range tests {
148+
t.Run(test.name, func(t *testing.T) {
149+
arg.Min, arg.Max = test.min, test.max
150+
require.Equal(t, test.expected, arg.Usage())
151+
})
152+
}
153+
}

command.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ type Command struct {
123123
SuggestCommandFunc SuggestCommandFunc
124124
// Flag exclusion group
125125
MutuallyExclusiveFlags []MutuallyExclusiveFlags
126+
// Arguments to parse for this command
127+
Arguments []Argument
126128

127129
// categories contains the categorized commands and is populated on app startup
128130
categories CommandCategories
@@ -135,6 +137,8 @@ type Command struct {
135137
parent *Command
136138
// the flag.FlagSet for this command
137139
flagSet *flag.FlagSet
140+
// parsed args
141+
parsedArgs Args
138142
// track state of error handling
139143
isInError bool
140144
// track state of defaults
@@ -518,6 +522,18 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) {
518522

519523
if cmd.Action == nil {
520524
cmd.Action = helpCommandAction
525+
} else if len(cmd.Arguments) > 0 {
526+
rargs := cmd.Args().Slice()
527+
tracef("calling argparse with %[1]v", rargs)
528+
for _, arg := range cmd.Arguments {
529+
var err error
530+
rargs, err = arg.Parse(rargs)
531+
if err != nil {
532+
tracef("calling with %[1]v (cmd=%[2]q)", err, cmd.Name)
533+
return err
534+
}
535+
}
536+
cmd.parsedArgs = &stringSliceArgs{v: rargs}
521537
}
522538

523539
if err := cmd.Action(ctx, cmd); err != nil {
@@ -607,6 +623,7 @@ func (cmd *Command) suggestFlagFromError(err error, commandName string) (string,
607623
func (cmd *Command) parseFlags(args Args) (Args, error) {
608624
tracef("parsing flags from arguments %[1]q (cmd=%[2]q)", args, cmd.Name)
609625

626+
cmd.parsedArgs = nil
610627
if v, err := cmd.newFlagSet(); err != nil {
611628
return args, err
612629
} else {
@@ -996,6 +1013,9 @@ func (cmd *Command) Value(name string) interface{} {
9961013
// Args returns the command line arguments associated with the
9971014
// command.
9981015
func (cmd *Command) Args() Args {
1016+
if cmd.parsedArgs != nil {
1017+
return cmd.parsedArgs
1018+
}
9991019
return &stringSliceArgs{v: cmd.flagSet.Args()}
10001020
}
10011021

command_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2026,8 +2026,9 @@ func TestCommand_Run_SubcommandFullPath(t *testing.T) {
20262026
out := &bytes.Buffer{}
20272027

20282028
subCmd := &Command{
2029-
Name: "bar",
2030-
Usage: "does bar things",
2029+
Name: "bar",
2030+
Usage: "does bar things",
2031+
ArgsUsage: "[arguments...]",
20312032
}
20322033

20332034
cmd := &Command{

examples_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ func ExampleCommand_Run_appHelp() {
104104
Aliases: []string{"d"},
105105
Usage: "use it to see a description",
106106
Description: "This is how we describe describeit the function",
107+
ArgsUsage: "[arguments...]",
107108
Action: func(context.Context, *cli.Command) error {
108109
fmt.Printf("i like to describe things")
109110
return nil
@@ -162,6 +163,7 @@ func ExampleCommand_Run_commandHelp() {
162163
Aliases: []string{"d"},
163164
Usage: "use it to see a description",
164165
Description: "This is how we describe describeit the function",
166+
ArgsUsage: "[arguments...]",
165167
Action: func(context.Context, *cli.Command) error {
166168
fmt.Println("i like to describe things")
167169
return nil
@@ -220,6 +222,7 @@ func ExampleCommand_Run_subcommandNoAction() {
220222
Name: "describeit",
221223
Aliases: []string{"d"},
222224
Usage: "use it to see a description",
225+
ArgsUsage: "[arguments...]",
223226
Description: "This is how we describe describeit the function",
224227
},
225228
},

0 commit comments

Comments
 (0)