Skip to content

Commit df1e3bf

Browse files
feat: Support terminated options like find -exec
Enable option to receive arguments until specified terminator is reached or EOL has been found. This is inspired from ``find -exec [commands..] ;`` where commands.. is treated as arguments to -exec. If for an option ``opt``, ``terminator`` is specified to be ; (semi-colon), in the following $ program [options] --opt v --w=x -- "y z" \; [more-options] --opt will receive {"v", "--w=x", "--", "y z"} as its argument. Note that, the -- inside will also be passed to --opt regardless PassDoubleDash is set or not. However, once the scope of --opt is finished, i.e. terminator ; is reached, -- will act as before if PassDoubleDash is set. Use tag ``terminator`` to specify the terminator for the option related to that field. Please note that, the specified terminator should be a separate token, instead of being jotted with other characters. For example, --opt [arguments..] ; [options..] will be correctly parsed with terminator: ";". However, --opt [arguments..] arg; [options..] will not be correctly parsed. The parser will pass "arg;", and continue to look for the terminator in [options..].
1 parent 3927b71 commit df1e3bf

File tree

5 files changed

+224
-0
lines changed

5 files changed

+224
-0
lines changed

flags.go

+4
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,10 @@ The following is a list of tags for struct fields supported by go-flags:
112112
Repeat this tag once for each allowable value.
113113
e.g. `long:"animal" choice:"cat" choice:"dog"`
114114
hidden: if non-empty, the option is not visible in the help or man page.
115+
terminator: when specified, the option will accept a list of arguments (as a slice)
116+
until the terminator string is found as an argument, or until the end
117+
of the argument list. To allow the same terminated option multiple
118+
times, use a slice of slices.
115119
116120
base: a base (radix) used to convert strings to integer values, the
117121
default base is 10 (i.e. decimal) (optional)

group.go

+9
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,8 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h
285285
choices := mtag.GetMany("choice")
286286
hidden := !isStringFalsy(mtag.Get("hidden"))
287287

288+
terminator := mtag.Get("terminator")
289+
288290
option := &Option{
289291
Description: description,
290292
ShortName: short,
@@ -299,6 +301,7 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h
299301
DefaultMask: defaultMask,
300302
Choices: choices,
301303
Hidden: hidden,
304+
Terminator: terminator,
302305

303306
group: g,
304307

@@ -313,6 +316,12 @@ func (g *Group) scanStruct(realval reflect.Value, sfield *reflect.StructField, h
313316
option.shortAndLongName())
314317
}
315318

319+
if option.isTerminated() && option.value.Kind() != reflect.Slice {
320+
return newErrorf(ErrInvalidTag,
321+
"terminated flag `%s' must be a slice or slice of slices",
322+
option.shortAndLongName())
323+
}
324+
316325
g.options = append(g.options, option)
317326
}
318327

option.go

+73
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,26 @@ type Option struct {
6868
// If true, the option is not displayed in the help or man page
6969
Hidden bool
7070

71+
// If not "", the option will accept a list of arguments (as a slice)
72+
// until the terminator string is found as an argument, or until the end
73+
// of the argument list. To allow the same terminated option multiple
74+
// times, use a slice of slices.
75+
//
76+
// Inspired by "find -exec" (which uses a ';' terminator), this supports
77+
// additional arguments after the terminator, for example:
78+
//
79+
// $ program [options] --terminated-opt v --w=x -- "y z" \; [more-options]
80+
//
81+
// In this example, --terminated-opt will receive {"v", "--w=x", "--", "y z"}.
82+
// As with "find -exec", when using ';' as a terminator at the shell, it
83+
// must be backslash-escaped to avoid the ';' being treated as a command
84+
// separator by the shell.
85+
//
86+
// As shown, "--" between the option and the terminator won't trigger
87+
// double-dash handling (if PassDoubleDash is set), but after the
88+
// terminator it will.
89+
Terminator string
90+
7191
// The group which the option belongs to
7292
group *Group
7393

@@ -282,6 +302,55 @@ func (option *Option) Set(value *string) error {
282302
return convert("", option.value, option.tag)
283303
}
284304

305+
func (option *Option) setTerminatedOption(value []*string) error {
306+
tp := option.value.Type()
307+
308+
if tp.Kind() != reflect.Slice {
309+
return newErrorf(ErrInvalidTag,
310+
"terminated flag `%s' must be a slice or slice of slices",
311+
option.shortAndLongName())
312+
}
313+
314+
if len(value) == 0 {
315+
return newErrorf(ErrExpectedArgument,
316+
"expected argument for flag `%s'",
317+
option.shortAndLongName())
318+
}
319+
320+
if option.clearReferenceBeforeSet {
321+
option.empty()
322+
}
323+
324+
option.isSet = true
325+
option.preventDefault = true
326+
option.clearReferenceBeforeSet = false
327+
328+
elemtp := tp.Elem()
329+
330+
if elemtp.Kind() == reflect.Slice {
331+
elemvalptr := reflect.New(elemtp)
332+
elemval := reflect.Indirect(elemvalptr)
333+
334+
for _, val := range value {
335+
if err := convert(*val, elemval, option.tag); err != nil {
336+
return err
337+
}
338+
}
339+
340+
option.value.Set(reflect.Append(option.value, elemval))
341+
} else {
342+
option.empty()
343+
344+
for _, val := range value {
345+
if err := convert(*val, option.value, option.tag); err != nil {
346+
return err
347+
}
348+
}
349+
}
350+
351+
return nil
352+
}
353+
285354
func (option *Option) setDefault(value *string) error {
286355
if option.preventDefault {
287356
return nil
@@ -567,3 +636,7 @@ func (option *Option) isValidValue(arg string) error {
567636
}
568637
return nil
569638
}
639+
640+
func (option *Option) isTerminated() bool {
641+
return option.Terminator != ""
642+
}

options_test.go

+124
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package flags
22

33
import (
4+
"reflect"
45
"strings"
56
"testing"
67
)
@@ -141,3 +142,126 @@ func TestPassAfterNonOptionWithPositionalIntFail(t *testing.T) {
141142
assertStringArray(t, ret, test.ret)
142143
}
143144
}
145+
146+
func TestTerminatedOptions(t *testing.T) {
147+
type testOpt struct {
148+
Slice []int `short:"s" long:"slice" terminator:"END"`
149+
MultipleSlice [][]string `short:"m" long:"multiple" terminator:";"`
150+
Bool bool `short:"v"`
151+
}
152+
153+
tests := []struct {
154+
summary string
155+
parserOpts Options
156+
args []string
157+
expectedSlice []int
158+
expectedMultipleSlice [][]string
159+
expectedBool bool
160+
expectedRest []string
161+
shouldErr bool
162+
}{
163+
{
164+
summary: "Terminators usage",
165+
args: []string{
166+
"-s", "1", "2", "3", "END",
167+
"-m", "bin", "-xyz", "--foo", "bar", "-v", "foo bar", ";",
168+
"-v",
169+
"-m", "-xyz", "--foo",
170+
},
171+
expectedSlice: []int{1, 2, 3},
172+
expectedMultipleSlice: [][]string{
173+
{"bin", "-xyz", "--foo", "bar", "-v", "foo bar"},
174+
{"-xyz", "--foo"},
175+
},
176+
expectedBool: true,
177+
}, {
178+
summary: "Slice overwritten",
179+
args: []string{
180+
"-s", "1", "2", "END",
181+
"-s", "3", "4",
182+
},
183+
expectedSlice: []int{3, 4},
184+
}, {
185+
summary: "Terminator omitted for last opt",
186+
args: []string{
187+
"-s", "1", "2", "3",
188+
},
189+
expectedSlice: []int{1, 2, 3},
190+
}, {
191+
summary: "Shortnames jumbled",
192+
args: []string{
193+
"-vm", "--foo", "-v", "bar", ";",
194+
"-s", "1", "2",
195+
},
196+
expectedSlice: []int{1, 2},
197+
expectedMultipleSlice: [][]string{{"--foo", "-v", "bar"}},
198+
expectedBool: true,
199+
}, {
200+
summary: "Terminator as a token",
201+
args: []string{
202+
"-m", "--foo", "-v;",
203+
"-v",
204+
},
205+
expectedMultipleSlice: [][]string{{"--foo", "-v;", "-v"}},
206+
}, {
207+
summary: "DoubleDash",
208+
parserOpts: PassDoubleDash,
209+
args: []string{
210+
"-m", "--foo", "--", "bar", ";",
211+
"-v",
212+
"--", "--foo", "bar",
213+
},
214+
expectedMultipleSlice: [][]string{{"--foo", "--", "bar"}},
215+
expectedBool: true,
216+
expectedRest: []string{"--foo", "bar"},
217+
}, {
218+
summary: "--opt=foo syntax",
219+
args: []string{"-m=foo", "bar"},
220+
shouldErr: true,
221+
}, {
222+
summary: "No args",
223+
args: []string{"-m", ";"},
224+
shouldErr: true,
225+
}, {
226+
summary: "No args, no terminator",
227+
args: []string{"-m"},
228+
shouldErr: true,
229+
}, {
230+
summary: "Nil args",
231+
args: []string{"-m", ""},
232+
expectedMultipleSlice: [][]string{{""}},
233+
},
234+
}
235+
236+
for _, test := range tests {
237+
t.Run(test.summary, func(t *testing.T) {
238+
opts := testOpt{}
239+
p := NewParser(&opts, test.parserOpts)
240+
rest, err := p.ParseArgs(test.args)
241+
242+
if err != nil {
243+
if !test.shouldErr {
244+
t.Errorf("Unexpected error: %v", err)
245+
}
246+
return
247+
}
248+
if test.shouldErr {
249+
t.Errorf("Expected error")
250+
}
251+
252+
if opts.Bool != test.expectedBool {
253+
t.Errorf("Expected Bool to be %v, got %v", test.expectedBool, opts.Bool)
254+
}
255+
256+
if !reflect.DeepEqual(opts.Slice, test.expectedSlice) {
257+
t.Errorf("Expected Slice to be %v, got %v", test.expectedSlice, opts.MultipleSlice)
258+
}
259+
260+
if !reflect.DeepEqual(opts.MultipleSlice, test.expectedMultipleSlice) {
261+
t.Errorf("Expected MultipleSlice to be %v, got %v", test.expectedMultipleSlice, opts.MultipleSlice)
262+
}
263+
264+
assertStringArray(t, rest, test.expectedRest)
265+
})
266+
}
267+
}

parser.go

+14
Original file line numberDiff line numberDiff line change
@@ -526,6 +526,20 @@ func (p *Parser) parseOption(s *parseState, name string, option *Option, canarg
526526
}
527527

528528
err = option.Set(nil)
529+
} else if option.isTerminated() {
530+
var args []*string
531+
532+
if argument != nil {
533+
return newErrorf(ErrInvalidTag, "terminated options' flag `%s' cannot use `='", option)
534+
}
535+
for !s.eof() {
536+
arg := s.pop()
537+
if arg == option.Terminator {
538+
break
539+
}
540+
args = append(args, &arg)
541+
}
542+
err = option.setTerminatedOption(args)
529543
} else if argument != nil || (canarg && !s.eof()) {
530544
var arg string
531545

0 commit comments

Comments
 (0)