diff --git a/cmd/internal/exec.go b/cmd/internal/exec.go index 97b158c5..6a6b3125 100644 --- a/cmd/internal/exec.go +++ b/cmd/internal/exec.go @@ -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( @@ -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 @@ -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 ` ) diff --git a/docs/cli/flow_exec.md b/docs/cli/flow_exec.md index 6bce3ce5..03cd471c 100644 --- a/docs/cli/flow_exec.md +++ b/docs/cli/flow_exec.md @@ -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. @@ -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 diff --git a/docs/guides/advanced.md b/docs/guides/advanced.md index f527a00c..376769f0 100644 --- a/docs/guides/advanced.md +++ b/docs/guides/advanced.md @@ -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= (params wins over shell) diff --git a/docs/guides/executables.md b/docs/guides/executables.md index b43277fc..ffa3068d 100644 --- a/docs/guides/executables.md +++ b/docs/guides/executables.md @@ -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 diff --git a/internal/runner/parallel/parallel_test.go b/internal/runner/parallel/parallel_test.go index 49cfc35e..f9c7ade2 100644 --- a/internal/runner/parallel/parallel_test.go +++ b/internal/runner/parallel/parallel_test.go @@ -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, @@ -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) diff --git a/internal/runner/serial/serial_test.go b/internal/runner/serial/serial_test.go index db2b0114..2d33655f 100644 --- a/internal/runner/serial/serial_test.go +++ b/internal/runner/serial/serial_test.go @@ -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, @@ -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) diff --git a/internal/utils/env/args.go b/internal/utils/env/args.go index 2dfb8a36..0946d3fe 100644 --- a/internal/utils/env/args.go +++ b/internal/utils/env/args.go @@ -4,6 +4,7 @@ import ( "os" "slices" "sort" + "strconv" "strings" "github.com/flowexec/flow/types/executable" @@ -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 } @@ -156,7 +179,7 @@ func BuildArgsFromEnv( } pos := len(argsWithPositions) for flag, value := range flagArgs { - result[pos] = flag + "=" + value + result[pos] = "--" + flag + "=" + value pos++ } diff --git a/internal/utils/env/env_test.go b/internal/utils/env/env_test.go index 90b18baf..2b2a9fdb 100644 --- a/internal/utils/env/env_test.go +++ b/internal/utils/env/env_test.go @@ -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()) @@ -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()) @@ -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()) @@ -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 @@ -263,7 +281,7 @@ 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"})) @@ -271,7 +289,7 @@ TEST_ENV_VAR3=value3` 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"})) @@ -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"})) }) @@ -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() { diff --git a/types/executable/arguments.go b/types/executable/arguments.go index b3038ccd..846b01e8 100644 --- a/types/executable/arguments.go +++ b/types/executable/arguments.go @@ -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 {