Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions cmd/internal/exec.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const (

func RegisterExecCmd(ctx *context.Context, rootCmd *cobra.Command) {
subCmd := &cobra.Command{
Use: "exec EXECUTABLE_ID [args...]",
Use: "exec EXECUTABLE_ID [-- args...]",
Aliases: executable.SortedValidVerbs(),
Short: "Execute any executable by reference.",
Long: execDocumentation + fmt.Sprintf(
Expand Down Expand Up @@ -529,8 +529,9 @@ var (
Execute an executable where EXECUTABLE_ID is the target executable's ID in the form of 'ws/ns:name'.
The flow subcommand used should match the target executable's verb or one of its aliases.

If the target executable accept arguments, they can be passed in the form of flag or positional arguments.
Flag arguments are specified with the format 'flag=value' and positional arguments are specified as values without any prefix.
If the target executable accepts arguments, use '--' to separate flow flags from executable arguments.
Flag arguments use standard '--flag=value' or '--flag value' syntax. Boolean flags can omit the value (e.g., '--verbose' implies true).
Positional arguments are specified as values without any prefix.
`
execExamples = `
#### Examples
Expand Down Expand Up @@ -558,6 +559,6 @@ flow exec ws/ns:build

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

flow exec ws/ns:build flag1=value1 flag2=value2 value3 value4
flow exec ws/ns:build -- --flag1=value1 --flag2=value2 value3 value4
`
)
9 changes: 5 additions & 4 deletions docs/cli/flow_exec.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,9 @@ Execute any executable by reference.
Execute an executable where EXECUTABLE_ID is the target executable's ID in the form of 'ws/ns:name'.
The flow subcommand used should match the target executable's verb or one of its aliases.

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


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.
Expand Down Expand Up @@ -40,11 +41,11 @@ flow exec ws/ns:build

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

flow exec ws/ns:build flag1=value1 flag2=value2 value3 value4
flow exec ws/ns:build -- --flag1=value1 --flag2=value2 value3 value4


```
flow exec EXECUTABLE_ID [args...] [flags]
flow exec EXECUTABLE_ID [-- args...] [flags]
```

### Options
Expand Down
2 changes: 1 addition & 1 deletion docs/guides/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,7 +423,7 @@ export VERBOSE=true
export ENVIRONMENT=development

# Command execution
flow deploy app verbose=false --param ENVIRONMENT=production
flow deploy app --param ENVIRONMENT=production -- --verbose=false

# Final environment variables:
# API_KEY=<secret-value> (params wins over shell)
Expand Down
7 changes: 6 additions & 1 deletion docs/guides/executables.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,9 +145,14 @@ executables:

**Run with arguments:**
```shell
flow build container v1.2.3 publish=true registry=my-registry.com
flow build container -- v1.2.3 --publish=true --registry=my-registry.com
```

> [!WARNING]
> **Breaking change:** Executable arguments now use standard `--flag=value` syntax with a `--` separator.
> The previous `flag=value` format (e.g., `flow build container v1.2.3 publish=true`) is no longer supported.
> Use `--` to separate flow flags from executable arguments, and prefix flag names with `--`.

**Argument types:**
- `pos`: Positional argument (by position number, starting from 1)
- `flag`: Named flag argument
Expand Down
4 changes: 2 additions & 2 deletions internal/runner/parallel/parallel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ var _ = Describe("ParallelRunner", func() {

ctx.RunnerMock.EXPECT().IsCompatible(gomock.Any()).Return(true).Times(1)
ctx.RunnerMock.EXPECT().
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"var=test_value"}).
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"--var=test_value"}).
DoAndReturn(func(
_ *context.Context,
exec *executable.Executable,
Expand All @@ -189,7 +189,7 @@ var _ = Describe("ParallelRunner", func() {
inputArgs []string,
) error {
Expect(inputEnv).To(HaveKeyWithValue("TEST_VAR", "test_value"))
Expect(inputArgs).To(ContainElement("var=test_value"))
Expect(inputArgs).To(ContainElement("--var=test_value"))
return nil
}).Times(1)

Expand Down
4 changes: 2 additions & 2 deletions internal/runner/serial/serial_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ var _ = Describe("SerialRunner", func() {

ctx.RunnerMock.EXPECT().IsCompatible(gomock.Any()).Return(true).Times(1)
ctx.RunnerMock.EXPECT().
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"var=test_value"}).
Exec(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), []string{"--var=test_value"}).
DoAndReturn(func(
_ *context.Context,
exec *executable.Executable,
Expand All @@ -186,7 +186,7 @@ var _ = Describe("SerialRunner", func() {
inputArgs []string,
) error {
Expect(inputEnv).To(HaveKeyWithValue("TEST_VAR", "test_value"))
Expect(inputArgs).To(ContainElement("var=test_value"))
Expect(inputArgs).To(ContainElement("--var=test_value"))
return nil
}).Times(1)

Expand Down
33 changes: 28 additions & 5 deletions internal/utils/env/args.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"os"
"slices"
"sort"
"strconv"
"strings"

"github.com/flowexec/flow/types/executable"
Expand All @@ -24,13 +25,35 @@ func BuildArgsEnvMap(
func parseArgs(args executable.ArgumentList, execArgs []string) (flagArgs map[string]string, posArgs []string) {
flagArgs = make(map[string]string)
posArgs = make([]string, 0)
knownFlags := args.Flags()
for i := 0; i < len(execArgs); i++ {
split := strings.SplitN(execArgs[i], "=", 2)
if len(split) == 2 && slices.Contains(args.Flags(), split[0]) {
flagArgs[split[0]] = split[1]
arg := execArgs[i]
if !strings.HasPrefix(arg, "--") {
posArgs = append(posArgs, arg)
continue
}
posArgs = append(posArgs, execArgs[i])

// Strip the -- prefix
flagStr := strings.TrimPrefix(arg, "--")

// Handle --flag=value
if name, value, ok := strings.Cut(flagStr, "="); ok {
if slices.Contains(knownFlags, name) {
flagArgs[name] = value
}
continue
}

// Handle --flag (no value)
if !slices.Contains(knownFlags, flagStr) {
continue
}
if args.FlagType(flagStr) == executable.ArgumentTypeBool {
flagArgs[flagStr] = strconv.FormatBool(true)
} else if i+1 < len(execArgs) && !strings.HasPrefix(execArgs[i+1], "--") {
i++
flagArgs[flagStr] = execArgs[i]
}
}
return
}
Expand Down Expand Up @@ -156,7 +179,7 @@ func BuildArgsFromEnv(
}
pos := len(argsWithPositions)
for flag, value := range flagArgs {
result[pos] = flag + "=" + value
result[pos] = "--" + flag + "=" + value
pos++
}

Expand Down
36 changes: 27 additions & 9 deletions internal/utils/env/env_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ TEST_ENV_VAR3=value3`
},
}
promptedEnv := make(map[string]string)
err := env.SetEnv("", exec, []string{"test", "flag=value"}, promptedEnv)
err := env.SetEnv("", exec, []string{"test", "--flag=value"}, promptedEnv)
Expect(err).ToNot(HaveOccurred())
val, exists := os.LookupEnv("TEST_POS")
Expect(exists).To(BeTrue())
Expand All @@ -186,7 +186,7 @@ TEST_ENV_VAR3=value3`
Args: []executable.Argument{{EnvKey: "TEST_KEY", Flag: "flag"}},
}
promptedEnv := map[string]string{"TEST_KEY": "input"}
err := env.SetEnv("", exec, []string{"flag=flag"}, promptedEnv)
err := env.SetEnv("", exec, []string{"--flag=flag"}, promptedEnv)
Expect(err).ToNot(HaveOccurred())
val, exists := os.LookupEnv("TEST_KEY")
Expect(exists).To(BeTrue())
Expand All @@ -199,7 +199,7 @@ TEST_ENV_VAR3=value3`
Args: []executable.Argument{{EnvKey: "TEST_KEY", Flag: "flag"}},
}
promptedEnv := map[string]string{"TEST_KEY": "input"}
err := env.SetEnv("", exec, []string{"flag=flag"}, promptedEnv)
err := env.SetEnv("", exec, []string{"--flag=flag"}, promptedEnv)
Expect(err).ToNot(HaveOccurred())
val, exists := os.LookupEnv("TEST_KEY")
Expect(exists).To(BeTrue())
Expand Down Expand Up @@ -242,14 +242,32 @@ TEST_ENV_VAR3=value3`
})

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

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

It("should correctly parse boolean flags without a value", func() {
args := executable.ArgumentList{
{EnvKey: "verbose", Flag: "verbose", Type: executable.ArgumentTypeBool},
}
inputVals := []string{"--verbose"}
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
Expect(err).ToNot(HaveOccurred())
Expect(envMap).To(Equal(map[string]string{"verbose": "true"}))
})

It("should correctly parse positional arguments", func() {
p1 := 1
p2 := 2
Expand All @@ -263,15 +281,15 @@ TEST_ENV_VAR3=value3`
It("should correctly parse mixed arguments", func() {
p1 := 1
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}, {EnvKey: "pos1", Pos: &p1}}
inputVals := []string{"flag1=value1", "pos1"}
inputVals := []string{"--flag1=value1", "pos1"}
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
Expect(err).ToNot(HaveOccurred())
Expect(envMap).To(Equal(map[string]string{"flag1": "value1", "pos1": "pos1"}))
})

It("should correctly parse flag arguments with equal sign in value", func() {
args := executable.ArgumentList{{EnvKey: "flag1", Flag: "flag1"}}
inputVals := []string{"flag1=value1=value2"}
inputVals := []string{"--flag1=value1=value2"}
envMap, err := env.BuildArgsEnvMap(args, inputVals, nil)
Expect(err).ToNot(HaveOccurred())
Expect(envMap).To(Equal(map[string]string{"flag1": "value1=value2"}))
Expand Down Expand Up @@ -367,7 +385,7 @@ TEST_ENV_VAR3=value3`
}
inputEnv := make(map[string]string)
defaultEnv := make(map[string]string)
envMap, err := env.BuildEnvMap("", exec, []string{"flag=test3"}, inputEnv, defaultEnv)
envMap, err := env.BuildEnvMap("", exec, []string{"--flag=test3"}, inputEnv, defaultEnv)
Expect(err).ToNot(HaveOccurred())
Expect(envMap).To(Equal(map[string]string{"TEST_KEY": "test", "TEST_KEY_2": "test2", "TEST_KEY_3": "test3"}))
})
Expand Down Expand Up @@ -509,7 +527,7 @@ BUILD_ENV_VAR3=build_value3`

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

It("should handle missing parent env values gracefully", func() {
Expand Down
9 changes: 9 additions & 0 deletions types/executable/arguments.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ func (al *ArgumentList) Flags() []string {
return flags
}

func (al *ArgumentList) FlagType(name string) ArgumentType {
for _, arg := range *al {
if arg.Flag == name {
return arg.Type
}
}
return ""
}

func (al *ArgumentList) Validate() error {
var errs []error
for _, arg := range *al {
Expand Down
Loading