Skip to content

Commit 1daca6e

Browse files
authored
fix xflag delimiter and flag value parsing (#25)
1 parent 70f1197 commit 1daca6e

2 files changed

Lines changed: 76 additions & 38 deletions

File tree

xflag/parse.go

Lines changed: 34 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,46 +14,38 @@ import (
1414
//
1515
// This is a bit unfortunate, but most users nowadays consuming CLI tools expect this behavior.
1616
func ParseToEnd(f *flag.FlagSet, arguments []string) error {
17-
if err := f.Parse(arguments); err != nil {
18-
return err
19-
}
20-
if f.NArg() == 0 {
21-
return nil
22-
}
17+
arguments, trailingArgs := splitAtDelimiter(arguments)
2318
var args []string
24-
remainingArgs := f.Args()
25-
for i := 0; i < len(remainingArgs); i++ {
26-
arg := remainingArgs[i]
27-
// If the arg looks like a flag, parses like a flag, and quacks like a flag, then it
28-
// probably is a flag.
29-
//
30-
// Note, there's an edge cases here which we EXPLICITLY do not handle, and quite honestly
31-
// 99.999% of the time you wouldn't build a CLI with this behavior.
19+
parseOnce := true
20+
for parseOnce || len(arguments) > 0 {
21+
parseOnce = false
22+
// If the next argument looks like a flag, parses like a flag, and quacks like a flag,
23+
// then it probably is a flag. Let the standard parser make that determination. When it
24+
// instead stops at a positional argument, preserve that argument and resume parsing after
25+
// it on the next iteration.
3226
//
33-
// If you want to treat an unknown flag as a positional argument. For example:
27+
// There is one edge case here which we EXPLICITLY do not handle, and quite honestly
28+
// 99.999% of the time you wouldn't build a CLI with this behavior: treating an unknown flag
29+
// as a positional argument. For example:
3430
//
3531
// $ ./cmd --valid=true arg1 --unknown-flag=foo arg2
3632
//
37-
// Right now, this will trigger an error. But *some* users might want that unknown flag to
38-
// be treated as a positional argument. It's trivial to add this behavior, by using VisitAll
39-
// to iterate over all defined flags (regardless if they are set), and then checking if the
40-
// flag is in the map of known flags.
41-
if len(arg) > 1 && arg[0] == '-' {
42-
// If we encounter a "--", treat all subsequent arguments as positional. The "--" itself
43-
// is stripped, consistent with the standard library's behavior.
44-
if arg == "--" {
45-
args = append(args, remainingArgs[i+1:]...)
46-
break
47-
}
48-
if err := f.Parse(remainingArgs[i:]); err != nil {
49-
return err
50-
}
51-
remainingArgs = f.Args()
52-
i = -1 // Reset to handle newly parsed arguments.
53-
continue
33+
// This triggers an error. Some users might want the unknown flag to be treated as a
34+
// positional argument instead. That behavior could be added by using VisitAll to collect
35+
// the defined flags before deciding whether to pass a flag-looking argument to Parse.
36+
if err := f.Parse(arguments); err != nil {
37+
return err
38+
}
39+
40+
arguments = f.Args()
41+
if len(arguments) == 0 {
42+
break
5443
}
55-
args = append(args, arg)
44+
45+
args = append(args, arguments[0])
46+
arguments = arguments[1:]
5647
}
48+
args = append(args, trailingArgs...)
5749
if len(args) > 0 {
5850
// Use "--" as a sentinel to set the FlagSet's internal args field without unsafe
5951
// reflection. When flag.Parse encounters "--" it stops processing and stores the remaining
@@ -62,3 +54,12 @@ func ParseToEnd(f *flag.FlagSet, arguments []string) error {
6254
}
6355
return nil
6456
}
57+
58+
func splitAtDelimiter(arguments []string) (before, after []string) {
59+
for i, arg := range arguments {
60+
if arg == "--" {
61+
return arguments[:i], arguments[i+1:]
62+
}
63+
}
64+
return arguments, nil
65+
}

xflag/parse_test.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ func TestParseToEnd(t *testing.T) {
1515
require.NoError(t, ParseToEnd(fs, []string{}))
1616
require.False(t, *debugP)
1717
require.Equal(t, 0, fs.NFlag())
18+
require.True(t, fs.Parsed())
19+
})
20+
t.Run("only double dash terminator", func(t *testing.T) {
21+
fs := flag.NewFlagSet("name", flag.ContinueOnError)
22+
require.NoError(t, ParseToEnd(fs, []string{"--"}))
23+
require.Empty(t, fs.Args())
24+
require.True(t, fs.Parsed())
1825
})
1926
t.Run("no args", func(t *testing.T) {
2027
fs := flag.NewFlagSet("name", flag.ContinueOnError)
@@ -115,6 +122,11 @@ func TestParseToEnd(t *testing.T) {
115122
require.Error(t, err)
116123
require.Equal(t, err.Error(), "flag provided but not defined: -some-unknown-flag")
117124
})
125+
t.Run("missing flag value after positional argument", func(t *testing.T) {
126+
fs, _ := newFlagset()
127+
err := ParseToEnd(fs, []string{"arg1", "--flag1"})
128+
require.EqualError(t, err, "flag needs an argument: -flag1")
129+
})
118130
t.Run("only positional args", func(t *testing.T) {
119131
fs, c := newFlagset()
120132
err := ParseToEnd(fs, []string{"arg1", "arg2", "arg3"})
@@ -152,6 +164,14 @@ func TestParseToEnd(t *testing.T) {
152164
require.Equal(t, "value2", c.flag2)
153165
require.Equal(t, []string{"arg1", "arg2", "arg3"}, fs.Args())
154166
})
167+
t.Run("flag-looking value after positional argument", func(t *testing.T) {
168+
fs, c := newFlagset()
169+
err := ParseToEnd(fs, []string{"arg1", "--flag1", "--flag3", "arg2"})
170+
require.NoError(t, err)
171+
require.Equal(t, "--flag3", c.flag1)
172+
require.False(t, c.flag3)
173+
require.Equal(t, []string{"arg1", "arg2"}, fs.Args())
174+
})
155175
t.Run("standalone dash is positional", func(t *testing.T) {
156176
fs, c := newFlagset()
157177
args := []string{"--flag1=value1", "-", "arg1"}
@@ -162,15 +182,32 @@ func TestParseToEnd(t *testing.T) {
162182
})
163183
t.Run("flags after double dash terminator", func(t *testing.T) {
164184
fs, c := newFlagset()
165-
// The initial f.Parse consumes --flag1 and stops at "--", leaving ["--flag3"] as remaining
166-
// args (the "--" is stripped by std lib). The loop then parses --flag3 as a flag,
167-
// collecting zero positional args.
168185
args := []string{"--flag1=value1", "--", "--flag3"}
169186
err := ParseToEnd(fs, args)
170187
require.NoError(t, err)
171188
require.Equal(t, "value1", c.flag1)
172-
require.True(t, c.flag3)
173-
require.Equal(t, 0, fs.NArg())
189+
require.False(t, c.flag3)
190+
require.Equal(t, []string{"--flag3"}, fs.Args())
191+
})
192+
t.Run("double dash terminator before flags", func(t *testing.T) {
193+
fs, c := newFlagset()
194+
err := ParseToEnd(fs, []string{"--", "--flag3"})
195+
require.NoError(t, err)
196+
require.False(t, c.flag3)
197+
require.Equal(t, []string{"--flag3"}, fs.Args())
198+
})
199+
t.Run("unknown flag after double dash terminator", func(t *testing.T) {
200+
fs, _ := newFlagset()
201+
err := ParseToEnd(fs, []string{"arg1", "--", "--unknown", "arg2"})
202+
require.NoError(t, err)
203+
require.Equal(t, []string{"arg1", "--unknown", "arg2"}, fs.Args())
204+
})
205+
t.Run("second double dash is positional", func(t *testing.T) {
206+
fs, c := newFlagset()
207+
err := ParseToEnd(fs, []string{"--", "--", "--flag3"})
208+
require.NoError(t, err)
209+
require.False(t, c.flag3)
210+
require.Equal(t, []string{"--", "--flag3"}, fs.Args())
174211
})
175212
t.Run("duplicate flags last wins", func(t *testing.T) {
176213
fs, c := newFlagset()

0 commit comments

Comments
 (0)