Skip to content

Commit c059a01

Browse files
jahvonclaude
andauthored
feat!: use standard --flag syntax for executable arguments (#325) (#387)
Switch from `flag=value` to `--flag=value` / `--flag value` syntax for executable arguments, with `--` separator between flow flags and executable args. Boolean flags can omit the value (e.g. `--verbose` implies true). This aligns with POSIX conventions and improves shell completion support. BREAKING CHANGE: The previous `flag=value` argument format is no longer supported. Use `--` to separate flow flags from executable arguments and prefix flag names with `--`. Before: flow exec build flag1=value1 pos1 After: flow exec build -- --flag1=value1 pos1 --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 46946da commit c059a01

9 files changed

Lines changed: 85 additions & 28 deletions

File tree

cmd/internal/exec.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const (
4444

4545
func RegisterExecCmd(ctx *context.Context, rootCmd *cobra.Command) {
4646
subCmd := &cobra.Command{
47-
Use: "exec EXECUTABLE_ID [args...]",
47+
Use: "exec EXECUTABLE_ID [-- args...]",
4848
Aliases: executable.SortedValidVerbs(),
4949
Short: "Execute any executable by reference.",
5050
Long: execDocumentation + fmt.Sprintf(
@@ -529,8 +529,9 @@ var (
529529
Execute an executable where EXECUTABLE_ID is the target executable's ID in the form of 'ws/ns:name'.
530530
The flow subcommand used should match the target executable's verb or one of its aliases.
531531
532-
If the target executable accept arguments, they can be passed in the form of flag or positional arguments.
533-
Flag arguments are specified with the format 'flag=value' and positional arguments are specified as values without any prefix.
532+
If the target executable accepts arguments, use '--' to separate flow flags from executable arguments.
533+
Flag arguments use standard '--flag=value' or '--flag value' syntax. Boolean flags can omit the value (e.g., '--verbose' implies true).
534+
Positional arguments are specified as values without any prefix.
534535
`
535536
execExamples = `
536537
#### Examples
@@ -558,6 +559,6 @@ flow exec ws/ns:build
558559
559560
**Execute the 'build' flow in the 'ws' workspace and 'ns' namespace with flag and positional arguments**
560561
561-
flow exec ws/ns:build flag1=value1 flag2=value2 value3 value4
562+
flow exec ws/ns:build -- --flag1=value1 --flag2=value2 value3 value4
562563
`
563564
)

docs/cli/flow_exec.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@ Execute any executable by reference.
88
Execute an executable where EXECUTABLE_ID is the target executable's ID in the form of 'ws/ns:name'.
99
The flow subcommand used should match the target executable's verb or one of its aliases.
1010

11-
If the target executable accept arguments, they can be passed in the form of flag or positional arguments.
12-
Flag arguments are specified with the format 'flag=value' and positional arguments are specified as values without any prefix.
11+
If the target executable accepts arguments, use '--' to separate flow flags from executable arguments.
12+
Flag arguments use standard '--flag=value' or '--flag value' syntax. Boolean flags can omit the value (e.g., '--verbose' implies true).
13+
Positional arguments are specified as values without any prefix.
1314

1415

1516
See https://flowexec.io/types/flowfile?id=executableverb for more information on executable verbs and https://flowexec.io/types/flowfile?id=executableref for more information on executable IDs.
@@ -40,11 +41,11 @@ flow exec ws/ns:build
4041

4142
**Execute the 'build' flow in the 'ws' workspace and 'ns' namespace with flag and positional arguments**
4243

43-
flow exec ws/ns:build flag1=value1 flag2=value2 value3 value4
44+
flow exec ws/ns:build -- --flag1=value1 --flag2=value2 value3 value4
4445

4546

4647
```
47-
flow exec EXECUTABLE_ID [args...] [flags]
48+
flow exec EXECUTABLE_ID [-- args...] [flags]
4849
```
4950

5051
### Options

docs/guides/advanced.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,7 +423,7 @@ export VERBOSE=true
423423
export ENVIRONMENT=development
424424
425425
# Command execution
426-
flow deploy app verbose=false --param ENVIRONMENT=production
426+
flow deploy app --param ENVIRONMENT=production -- --verbose=false
427427
428428
# Final environment variables:
429429
# API_KEY=<secret-value> (params wins over shell)

docs/guides/executables.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,9 +145,14 @@ executables:
145145

146146
**Run with arguments:**
147147
```shell
148-
flow build container v1.2.3 publish=true registry=my-registry.com
148+
flow build container -- v1.2.3 --publish=true --registry=my-registry.com
149149
```
150150

151+
> [!WARNING]
152+
> **Breaking change:** Executable arguments now use standard `--flag=value` syntax with a `--` separator.
153+
> The previous `flag=value` format (e.g., `flow build container v1.2.3 publish=true`) is no longer supported.
154+
> Use `--` to separate flow flags from executable arguments, and prefix flag names with `--`.
155+
151156
**Argument types:**
152157
- `pos`: Positional argument (by position number, starting from 1)
153158
- `flag`: Named flag argument

internal/runner/parallel/parallel_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ var _ = Describe("ParallelRunner", func() {
180180

181181
ctx.RunnerMock.EXPECT().IsCompatible(gomock.Any()).Return(true).Times(1)
182182
ctx.RunnerMock.EXPECT().
183-
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"var=test_value"}).
183+
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"--var=test_value"}).
184184
DoAndReturn(func(
185185
_ *context.Context,
186186
exec *executable.Executable,
@@ -189,7 +189,7 @@ var _ = Describe("ParallelRunner", func() {
189189
inputArgs []string,
190190
) error {
191191
Expect(inputEnv).To(HaveKeyWithValue("TEST_VAR", "test_value"))
192-
Expect(inputArgs).To(ContainElement("var=test_value"))
192+
Expect(inputArgs).To(ContainElement("--var=test_value"))
193193
return nil
194194
}).Times(1)
195195

internal/runner/serial/serial_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ var _ = Describe("SerialRunner", func() {
177177

178178
ctx.RunnerMock.EXPECT().IsCompatible(gomock.Any()).Return(true).Times(1)
179179
ctx.RunnerMock.EXPECT().
180-
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"var=test_value"}).
180+
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"--var=test_value"}).
181181
DoAndReturn(func(
182182
_ *context.Context,
183183
exec *executable.Executable,
@@ -186,7 +186,7 @@ var _ = Describe("SerialRunner", func() {
186186
inputArgs []string,
187187
) error {
188188
Expect(inputEnv).To(HaveKeyWithValue("TEST_VAR", "test_value"))
189-
Expect(inputArgs).To(ContainElement("var=test_value"))
189+
Expect(inputArgs).To(ContainElement("--var=test_value"))
190190
return nil
191191
}).Times(1)
192192

internal/utils/env/args.go

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"os"
55
"slices"
66
"sort"
7+
"strconv"
78
"strings"
89

910
"github.com/flowexec/flow/types/executable"
@@ -24,13 +25,35 @@ func BuildArgsEnvMap(
2425
func parseArgs(args executable.ArgumentList, execArgs []string) (flagArgs map[string]string, posArgs []string) {
2526
flagArgs = make(map[string]string)
2627
posArgs = make([]string, 0)
28+
knownFlags := args.Flags()
2729
for i := 0; i < len(execArgs); i++ {
28-
split := strings.SplitN(execArgs[i], "=", 2)
29-
if len(split) == 2 && slices.Contains(args.Flags(), split[0]) {
30-
flagArgs[split[0]] = split[1]
30+
arg := execArgs[i]
31+
if !strings.HasPrefix(arg, "--") {
32+
posArgs = append(posArgs, arg)
3133
continue
3234
}
33-
posArgs = append(posArgs, execArgs[i])
35+
36+
// Strip the -- prefix
37+
flagStr := strings.TrimPrefix(arg, "--")
38+
39+
// Handle --flag=value
40+
if name, value, ok := strings.Cut(flagStr, "="); ok {
41+
if slices.Contains(knownFlags, name) {
42+
flagArgs[name] = value
43+
}
44+
continue
45+
}
46+
47+
// Handle --flag (no value)
48+
if !slices.Contains(knownFlags, flagStr) {
49+
continue
50+
}
51+
if args.FlagType(flagStr) == executable.ArgumentTypeBool {
52+
flagArgs[flagStr] = strconv.FormatBool(true)
53+
} else if i+1 < len(execArgs) && !strings.HasPrefix(execArgs[i+1], "--") {
54+
i++
55+
flagArgs[flagStr] = execArgs[i]
56+
}
3457
}
3558
return
3659
}
@@ -156,7 +179,7 @@ func BuildArgsFromEnv(
156179
}
157180
pos := len(argsWithPositions)
158181
for flag, value := range flagArgs {
159-
result[pos] = flag + "=" + value
182+
result[pos] = "--" + flag + "=" + value
160183
pos++
161184
}
162185

internal/utils/env/env_test.go

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,7 @@ TEST_ENV_VAR3=value3`
170170
},
171171
}
172172
promptedEnv := make(map[string]string)
173-
err := env.SetEnv("", exec, []string{"test", "flag=value"}, promptedEnv)
173+
err := env.SetEnv("", exec, []string{"test", "--flag=value"}, promptedEnv)
174174
Expect(err).ToNot(HaveOccurred())
175175
val, exists := os.LookupEnv("TEST_POS")
176176
Expect(exists).To(BeTrue())
@@ -186,7 +186,7 @@ TEST_ENV_VAR3=value3`
186186
Args: []executable.Argument{{EnvKey: "TEST_KEY", Flag: "flag"}},
187187
}
188188
promptedEnv := map[string]string{"TEST_KEY": "input"}
189-
err := env.SetEnv("", exec, []string{"flag=flag"}, promptedEnv)
189+
err := env.SetEnv("", exec, []string{"--flag=flag"}, promptedEnv)
190190
Expect(err).ToNot(HaveOccurred())
191191
val, exists := os.LookupEnv("TEST_KEY")
192192
Expect(exists).To(BeTrue())
@@ -199,7 +199,7 @@ TEST_ENV_VAR3=value3`
199199
Args: []executable.Argument{{EnvKey: "TEST_KEY", Flag: "flag"}},
200200
}
201201
promptedEnv := map[string]string{"TEST_KEY": "input"}
202-
err := env.SetEnv("", exec, []string{"flag=flag"}, promptedEnv)
202+
err := env.SetEnv("", exec, []string{"--flag=flag"}, promptedEnv)
203203
Expect(err).ToNot(HaveOccurred())
204204
val, exists := os.LookupEnv("TEST_KEY")
205205
Expect(exists).To(BeTrue())
@@ -242,14 +242,32 @@ TEST_ENV_VAR3=value3`
242242
})
243243

244244
Describe("BuildArgsEnvMap", func() {
245-
It("should correctly parse flag arguments", func() {
245+
It("should correctly parse flag arguments with --flag=value syntax", func() {
246246
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}, {EnvKey: "flag2", Flag: "flag2"}}
247-
inputVals := []string{"flag1=value1", "flag2=value2"}
247+
inputVals := []string{"--flag1=value1", "--flag2=value2"}
248248
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
249249
Expect(err).ToNot(HaveOccurred())
250250
Expect(envMap).To(Equal(map[string]string{"flag1": "value1", "flag2": "value2"}))
251251
})
252252

253+
It("should correctly parse flag arguments with --flag value syntax", func() {
254+
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}, {EnvKey: "flag2", Flag: "flag2"}}
255+
inputVals := []string{"--flag1", "value1", "--flag2", "value2"}
256+
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
257+
Expect(err).ToNot(HaveOccurred())
258+
Expect(envMap).To(Equal(map[string]string{"flag1": "value1", "flag2": "value2"}))
259+
})
260+
261+
It("should correctly parse boolean flags without a value", func() {
262+
args := executable.ArgumentList{
263+
{EnvKey: "verbose", Flag: "verbose", Type: executable.ArgumentTypeBool},
264+
}
265+
inputVals := []string{"--verbose"}
266+
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
267+
Expect(err).ToNot(HaveOccurred())
268+
Expect(envMap).To(Equal(map[string]string{"verbose": "true"}))
269+
})
270+
253271
It("should correctly parse positional arguments", func() {
254272
p1 := 1
255273
p2 := 2
@@ -263,15 +281,15 @@ TEST_ENV_VAR3=value3`
263281
It("should correctly parse mixed arguments", func() {
264282
p1 := 1
265283
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}, {EnvKey: "pos1", Pos: &p1}}
266-
inputVals := []string{"flag1=value1", "pos1"}
284+
inputVals := []string{"--flag1=value1", "pos1"}
267285
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
268286
Expect(err).ToNot(HaveOccurred())
269287
Expect(envMap).To(Equal(map[string]string{"flag1": "value1", "pos1": "pos1"}))
270288
})
271289

272290
It("should correctly parse flag arguments with equal sign in value", func() {
273291
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}}
274-
inputVals := []string{"flag1=value1=value2"}
292+
inputVals := []string{"--flag1=value1=value2"}
275293
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
276294
Expect(err).ToNot(HaveOccurred())
277295
Expect(envMap).To(Equal(map[string]string{"flag1": "value1=value2"}))
@@ -367,7 +385,7 @@ TEST_ENV_VAR3=value3`
367385
}
368386
inputEnv := make(map[string]string)
369387
defaultEnv := make(map[string]string)
370-
envMap, err := env.BuildEnvMap("", exec, []string{"flag=test3"}, inputEnv, defaultEnv)
388+
envMap, err := env.BuildEnvMap("", exec, []string{"--flag=test3"}, inputEnv, defaultEnv)
371389
Expect(err).ToNot(HaveOccurred())
372390
Expect(envMap).To(Equal(map[string]string{"TEST_KEY": "test", "TEST_KEY_2": "test2", "TEST_KEY_3": "test3"}))
373391
})
@@ -509,7 +527,7 @@ BUILD_ENV_VAR3=build_value3`
509527

510528
filteredArgs := env.BuildArgsFromEnv(childArgs, parentEnv)
511529
Expect(filteredArgs).
512-
To(Equal([]string{"bitnami", "https://charts.bitnami.com/bitnami", "namespace=my-namespace"}))
530+
To(Equal([]string{"bitnami", "https://charts.bitnami.com/bitnami", "--namespace=my-namespace"}))
513531
})
514532

515533
It("should handle missing parent env values gracefully", func() {

types/executable/arguments.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,15 @@ func (al *ArgumentList) Flags() []string {
8080
return flags
8181
}
8282

83+
func (al *ArgumentList) FlagType(name string) ArgumentType {
84+
for _, arg := range *al {
85+
if arg.Flag == name {
86+
return arg.Type
87+
}
88+
}
89+
return ""
90+
}
91+
8392
func (al *ArgumentList) Validate() error {
8493
var errs []error
8594
for _, arg := range *al {

0 commit comments

Comments
 (0)