Skip to content

Commit 9dad0d4

Browse files
authored
Merge pull request #2215 from oprudkyi/issue/parallel_run
support parallel running of commands
2 parents 495e1d3 + 798146b commit 9dad0d4

File tree

10 files changed

+182
-48
lines changed

10 files changed

+182
-48
lines changed

command.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ type Command struct {
101101
SliceFlagSeparator string `json:"sliceFlagSeparator"`
102102
// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
103103
DisableSliceFlagSeparator bool `json:"disableSliceFlagSeparator"`
104+
// MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "="
105+
MapFlagKeyValueSeparator string `json:"mapFlagKeyValueSeparator"`
104106
// Boolean to enable short-option handling so user can combine several
105107
// single-character bool arguments into one
106108
// i.e. foobar -o -v -> foobar -ov
@@ -155,6 +157,10 @@ type Command struct {
155157
didSetupDefaults bool
156158
// whether in shell completion mode
157159
shellCompletion bool
160+
// whether global help flag was added
161+
globaHelpFlagAdded bool
162+
// whether global version flag was added
163+
globaVersionFlagAdded bool
158164
}
159165

160166
// FullName returns the full name of the command.
@@ -349,6 +355,7 @@ func (cmd *Command) Root() *Command {
349355

350356
func (cmd *Command) set(fName string, f Flag, val string) error {
351357
cmd.setFlags[f] = struct{}{}
358+
cmd.setMultiValueParsingConfig(f)
352359
if err := f.Set(fName, val); err != nil {
353360
return fmt.Errorf("invalid value %q for flag -%s: %v", val, fName, err)
354361
}
@@ -440,9 +447,21 @@ func (cmd *Command) NumFlags() int {
440447
return count // cmd.flagSet.NFlag()
441448
}
442449

450+
func (cmd *Command) setMultiValueParsingConfig(f Flag) {
451+
tracef("setMultiValueParsingConfig %T, %+v", f, f)
452+
if cf, ok := f.(multiValueParsingConfigSetter); ok {
453+
cf.setMultiValueParsingConfig(multiValueParsingConfig{
454+
SliceFlagSeparator: cmd.SliceFlagSeparator,
455+
DisableSliceFlagSeparator: cmd.DisableSliceFlagSeparator,
456+
MapFlagKeyValueSeparator: cmd.MapFlagKeyValueSeparator,
457+
})
458+
}
459+
}
460+
443461
// Set sets a context flag to a value.
444462
func (cmd *Command) Set(name, value string) error {
445463
if f := cmd.lookupFlag(name); f != nil {
464+
cmd.setMultiValueParsingConfig(f)
446465
return f.Set(name, value)
447466
}
448467

command_setup.go

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,18 @@ func (cmd *Command) setupDefaults(osArgs []string) {
8080

8181
if !cmd.HideVersion && isRoot {
8282
tracef("appending version flag (cmd=%[1]q)", cmd.Name)
83-
cmd.appendFlag(VersionFlag)
83+
if !cmd.globaVersionFlagAdded {
84+
var localVersionFlag Flag
85+
if globalVersionFlag, ok := VersionFlag.(*BoolFlag); ok {
86+
flag := *globalVersionFlag
87+
localVersionFlag = &flag
88+
} else {
89+
localVersionFlag = VersionFlag
90+
}
91+
92+
cmd.appendFlag(localVersionFlag)
93+
cmd.globaVersionFlagAdded = true
94+
}
8495
}
8596

8697
if cmd.PrefixMatchCommands && cmd.SuggestCommandFunc == nil {
@@ -130,14 +141,6 @@ func (cmd *Command) setupDefaults(osArgs []string) {
130141
cmd.Metadata = map[string]any{}
131142
}
132143

133-
if len(cmd.SliceFlagSeparator) != 0 {
134-
tracef("setting defaultSliceFlagSeparator from cmd.SliceFlagSeparator (cmd=%[1]q)", cmd.Name)
135-
defaultSliceFlagSeparator = cmd.SliceFlagSeparator
136-
}
137-
138-
tracef("setting disableSliceFlagSeparator from cmd.DisableSliceFlagSeparator (cmd=%[1]q)", cmd.Name)
139-
disableSliceFlagSeparator = cmd.DisableSliceFlagSeparator
140-
141144
cmd.setFlags = map[Flag]struct{}{}
142145
}
143146

@@ -200,15 +203,21 @@ func (cmd *Command) ensureHelp() {
200203
}
201204

202205
if HelpFlag != nil {
203-
// TODO need to remove hack
204-
if hf, ok := HelpFlag.(*BoolFlag); ok {
205-
hf.applied = false
206-
hf.hasBeenSet = false
207-
hf.Value = false
208-
hf.value = nil
206+
if !cmd.globaHelpFlagAdded {
207+
var localHelpFlag Flag
208+
if globalHelpFlag, ok := HelpFlag.(*BoolFlag); ok {
209+
flag := *globalHelpFlag
210+
localHelpFlag = &flag
211+
} else {
212+
localHelpFlag = HelpFlag
213+
}
214+
215+
tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name)
216+
cmd.appendFlag(localHelpFlag)
217+
cmd.globaHelpFlagAdded = true
218+
} else {
219+
tracef("HelpFlag already added, skip (cmd=%[1]q)", cmd.Name)
209220
}
210-
tracef("appending HelpFlag (cmd=%[1]q)", cmd.Name)
211-
cmd.appendFlag(HelpFlag)
212221
}
213222
}
214223
}

command_test.go

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4386,11 +4386,6 @@ func TestCommandCategories(t *testing.T) {
43864386
}
43874387

43884388
func TestCommandSliceFlagSeparator(t *testing.T) {
4389-
oldSep := defaultSliceFlagSeparator
4390-
defer func() {
4391-
defaultSliceFlagSeparator = oldSep
4392-
}()
4393-
43944389
cmd := &Command{
43954390
SliceFlagSeparator: ";",
43964391
Flags: []Flag{
@@ -4405,6 +4400,26 @@ func TestCommandSliceFlagSeparator(t *testing.T) {
44054400
r.Equal([]string{"ff", "dd", "gg", "t,u"}, cmd.Value("foo"))
44064401
}
44074402

4403+
func TestCommandMapKeyValueFlagSeparator(t *testing.T) {
4404+
cmd := &Command{
4405+
MapFlagKeyValueSeparator: ":",
4406+
Flags: []Flag{
4407+
&StringMapFlag{
4408+
Name: "f_string_map",
4409+
},
4410+
},
4411+
}
4412+
4413+
r := require.New(t)
4414+
r.NoError(cmd.Run(buildTestContext(t), []string{"app", "--f_string_map", "s1:s2,s3:", "--f_string_map", "s4:s5"}))
4415+
exp := map[string]string{
4416+
"s1": "s2",
4417+
"s3": "",
4418+
"s4": "s5",
4419+
}
4420+
r.Equal(exp, cmd.Value("f_string_map"))
4421+
}
4422+
44084423
// TestStringFlagTerminator tests the string flag "--flag" with "--" terminator.
44094424
func TestStringFlagTerminator(t *testing.T) {
44104425
tests := []struct {
@@ -4754,6 +4769,7 @@ func TestJSONExportCommand(t *testing.T) {
47544769
"metadata": null,
47554770
"sliceFlagSeparator": "",
47564771
"disableSliceFlagSeparator": false,
4772+
"mapFlagKeyValueSeparator": "",
47574773
"useShortOptionHandling": false,
47584774
"suggest": false,
47594775
"allowExtFlags": false,
@@ -4817,6 +4833,7 @@ func TestJSONExportCommand(t *testing.T) {
48174833
"metadata": null,
48184834
"sliceFlagSeparator": "",
48194835
"disableSliceFlagSeparator": false,
4836+
"mapFlagKeyValueSeparator": "",
48204837
"useShortOptionHandling": false,
48214838
"suggest": false,
48224839
"allowExtFlags": false,
@@ -4851,6 +4868,7 @@ func TestJSONExportCommand(t *testing.T) {
48514868
"metadata": null,
48524869
"sliceFlagSeparator": "",
48534870
"disableSliceFlagSeparator": false,
4871+
"mapFlagKeyValueSeparator": "",
48544872
"useShortOptionHandling": false,
48554873
"suggest": false,
48564874
"allowExtFlags": false,
@@ -4882,6 +4900,7 @@ func TestJSONExportCommand(t *testing.T) {
48824900
"metadata": null,
48834901
"sliceFlagSeparator": "",
48844902
"disableSliceFlagSeparator": false,
4903+
"mapFlagKeyValueSeparator": "",
48854904
"useShortOptionHandling": false,
48864905
"suggest": false,
48874906
"allowExtFlags": false,
@@ -4932,6 +4951,7 @@ func TestJSONExportCommand(t *testing.T) {
49324951
"metadata": null,
49334952
"sliceFlagSeparator": "",
49344953
"disableSliceFlagSeparator": false,
4954+
"mapFlagKeyValueSeparator": "",
49354955
"useShortOptionHandling": false,
49364956
"suggest": false,
49374957
"allowExtFlags": false,
@@ -4999,6 +5019,7 @@ func TestJSONExportCommand(t *testing.T) {
49995019
"metadata": null,
50005020
"sliceFlagSeparator": "",
50015021
"disableSliceFlagSeparator": false,
5022+
"mapFlagKeyValueSeparator": "",
50025023
"useShortOptionHandling": false,
50035024
"suggest": false,
50045025
"allowExtFlags": false,
@@ -5062,6 +5083,7 @@ func TestJSONExportCommand(t *testing.T) {
50625083
"metadata": null,
50635084
"sliceFlagSeparator": "",
50645085
"disableSliceFlagSeparator": false,
5086+
"mapFlagKeyValueSeparator": "",
50655087
"useShortOptionHandling": false,
50665088
"suggest": false,
50675089
"allowExtFlags": false,
@@ -5169,6 +5191,7 @@ func TestJSONExportCommand(t *testing.T) {
51695191
"metadata": null,
51705192
"sliceFlagSeparator": "",
51715193
"disableSliceFlagSeparator": false,
5194+
"mapFlagKeyValueSeparator": "",
51725195
"useShortOptionHandling": false,
51735196
"suggest": false,
51745197
"allowExtFlags": false,
@@ -5284,3 +5307,31 @@ func TestCommand_ExclusiveFlagsWithAfter(t *testing.T) {
52845307
}))
52855308
require.True(t, called)
52865309
}
5310+
5311+
func TestCommand_ParallelRun(t *testing.T) {
5312+
t.Parallel()
5313+
5314+
for i := 0; i < 10; i++ {
5315+
t.Run(fmt.Sprintf("run_%d", i), func(t *testing.T) {
5316+
t.Parallel()
5317+
5318+
defer func() {
5319+
if r := recover(); r != nil {
5320+
t.Errorf("unexpected panic - '%s'", r)
5321+
}
5322+
}()
5323+
5324+
cmd := &Command{
5325+
Name: "debug",
5326+
Usage: "make an explosive entrance",
5327+
Action: func(_ context.Context, cmd *Command) error {
5328+
return nil
5329+
},
5330+
}
5331+
5332+
if err := cmd.Run(context.Background(), nil); err != nil {
5333+
fmt.Printf("%s\n", err)
5334+
}
5335+
})
5336+
}
5337+
}

flag.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010

1111
const defaultPlaceholder = "value"
1212

13-
var (
13+
const (
1414
defaultSliceFlagSeparator = ","
1515
defaultMapFlagKeyValueSeparator = "="
1616
disableSliceFlagSeparator = false
@@ -222,10 +222,13 @@ func hasFlag(flags []Flag, fl Flag) bool {
222222
return false
223223
}
224224

225-
func flagSplitMultiValues(val string) []string {
226-
if disableSliceFlagSeparator {
225+
func flagSplitMultiValues(val string, sliceSeparator string, disableSliceSeparator bool) []string {
226+
if disableSliceSeparator {
227227
return []string{val}
228228
}
229229

230-
return strings.Split(val, defaultSliceFlagSeparator)
230+
if len(sliceSeparator) == 0 {
231+
sliceSeparator = defaultSliceFlagSeparator
232+
}
233+
return strings.Split(val, sliceSeparator)
231234
}

flag_impl.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ type boolFlag interface {
1919
IsBoolFlag() bool
2020
}
2121

22+
type multiValueParsingConfig struct {
23+
// SliceFlagSeparator is used to customize the separator for SliceFlag, the default is ","
24+
SliceFlagSeparator string
25+
// DisableSliceFlagSeparator is used to disable SliceFlagSeparator, the default is false
26+
DisableSliceFlagSeparator bool
27+
// MapFlagKeyValueSeparator is used to customize the separator for MapFlag, the default is "="
28+
MapFlagKeyValueSeparator string
29+
}
30+
31+
type multiValueParsingConfigSetter interface {
32+
// configuration of parsing
33+
setMultiValueParsingConfig(c multiValueParsingConfig)
34+
}
35+
2236
// ValueCreator is responsible for creating a flag.Value emulation
2337
// as well as custom formatting
2438
//
@@ -134,6 +148,14 @@ func (f *FlagBase[T, C, V]) PostParse() error {
134148
return nil
135149
}
136150

151+
// pass configuration of parsing to value
152+
func (f *FlagBase[T, C, V]) setMultiValueParsingConfig(c multiValueParsingConfig) {
153+
tracef("setMultiValueParsingConfig %T, %+v", f.value, f.value)
154+
if cf, ok := f.value.(multiValueParsingConfigSetter); ok {
155+
cf.setMultiValueParsingConfig(c)
156+
}
157+
}
158+
137159
func (f *FlagBase[T, C, V]) PreParse() error {
138160
newVal := f.Value
139161

flag_map_impl.go

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ import (
1010

1111
// MapBase wraps map[string]T to satisfy flag.Value
1212
type MapBase[T any, C any, VC ValueCreator[T, C]] struct {
13-
dict *map[string]T
14-
hasBeenSet bool
15-
value Value
13+
dict *map[string]T
14+
hasBeenSet bool
15+
value Value
16+
multiValueConfig multiValueParsingConfig
1617
}
1718

1819
func (i MapBase[T, C, VC]) Create(val map[string]T, p *map[string]T, c C) Value {
@@ -36,6 +37,18 @@ func NewMapBase[T any, C any, VC ValueCreator[T, C]](defaults map[string]T) *Map
3637
}
3738
}
3839

40+
// configuration of slicing
41+
func (i *MapBase[T, C, VC]) setMultiValueParsingConfig(c multiValueParsingConfig) {
42+
i.multiValueConfig = c
43+
mvc := &i.multiValueConfig
44+
tracef(
45+
"set map parsing config - keyValueSeparator '%s', slice separator '%s', disable separator:%v",
46+
mvc.MapFlagKeyValueSeparator,
47+
mvc.SliceFlagSeparator,
48+
mvc.DisableSliceFlagSeparator,
49+
)
50+
}
51+
3952
// Set parses the value and appends it to the list of values
4053
func (i *MapBase[T, C, VC]) Set(value string) error {
4154
if !i.hasBeenSet {
@@ -50,10 +63,23 @@ func (i *MapBase[T, C, VC]) Set(value string) error {
5063
return nil
5164
}
5265

53-
for _, item := range flagSplitMultiValues(value) {
54-
key, value, ok := strings.Cut(item, defaultMapFlagKeyValueSeparator)
66+
mvc := &i.multiValueConfig
67+
keyValueSeparator := mvc.MapFlagKeyValueSeparator
68+
if len(keyValueSeparator) == 0 {
69+
keyValueSeparator = defaultMapFlagKeyValueSeparator
70+
}
71+
72+
tracef(
73+
"splitting map value '%s', keyValueSeparator '%s', slice separator '%s', disable separator:%v",
74+
value,
75+
keyValueSeparator,
76+
mvc.SliceFlagSeparator,
77+
mvc.DisableSliceFlagSeparator,
78+
)
79+
for _, item := range flagSplitMultiValues(value, mvc.SliceFlagSeparator, mvc.DisableSliceFlagSeparator) {
80+
key, value, ok := strings.Cut(item, keyValueSeparator)
5581
if !ok {
56-
return fmt.Errorf("item %q is missing separator %q", item, defaultMapFlagKeyValueSeparator)
82+
return fmt.Errorf("item %q is missing separator %q", item, keyValueSeparator)
5783
}
5884
if err := i.value.Set(value); err != nil {
5985
return err

0 commit comments

Comments
 (0)