# Spec: Step Run Command ## Status Implemented. This spec defines command-form `run` behavior. ## Scope This spec defines command-form `run`. Command form is: - A single-line string `run`. - Each non-empty entry in array-form `run`. Shared `run` behavior is defined by [Spec 013: Step Run](013-step-run.md). Script-form `run` is defined by [Spec 015: Step Run Script](015-step-run-script.md). This spec does not define: - Multi-line string `run`. - Direct argv execution through `action: exec`. - Non-local executor behavior. ## Goal Workflow authors can run shell command text with predictable shell handoff, shell-specific behavior, and sequential behavior for array-form `run`. ## Behavior ### Command Text - Command-form `run` is shell command text. - Command-form `run` must not contain line breaks. - Dagu resolves Dagu-owned references before the command starts. - Value resolution must not reclassify command form as script form. - If resolved command-form text contains a line break, the step fails before the selected shell starts. - Dagu treats the resolved command text as the command payload for the selected shell. - Shell-specific rules below define how Dagu gives that payload to the selected shell's command-string carrier. - Dagu must not split command-form `run` into a user-authored argv list. - Any command parsing, normalization, or display metadata produced by Dagu must not change the resolved command text payload. - Shell syntax in the command text is owned by the selected shell. - Dagu starts the selected shell with the step process working directory. - Relative paths in command text are interpreted by the selected shell or operating system from that working directory. - Bare command-name lookup is owned by the selected shell or operating system. - Dagu must not add the step process working directory to command lookup rules. ### Shell Operators - Command-form `run` is passed to the selected shell as one command string, so shell operators inside the command text remain inside that shell invocation. - Dagu accepts shell operator text in command-form `run`, including pipes (`|`), redirects (`>`, `>>`, `<`, `2>`), command chaining (`&&`, `||`, `;`), background execution (`&`), grouping syntax, and shell-specific control syntax. - The selected shell defines which operator forms are valid. For example, `sh`, Bash, PowerShell, and `cmd` do not share one operator grammar. - Dagu must not split command-form `run` at shell operators. - Dagu must not convert shell command chaining inside one command-form entry into array-form `run` entries. - Array-form sequencing is Dagu-owned; shell operators inside an array-form entry are shell-owned for that entry. - Dagu determines command-form success or failure from the selected shell process result, not from individual shell operators inside the command text. - Pipeline, list, conditional, grouping, and subshell exit behavior is owned by the selected shell and its configured options. - Background execution syntax is owned by the selected shell. - Dagu determines the command-form result from the selected shell process, not from detached background work that outlives that shell process. - A command that starts background work must wait for that work itself when the step result should include that work. - A command must also wait for background work when stdout, stderr, `DAGU_OUTPUT_FILE` records, artifacts, or filesystem side effects from that work must be included deterministically in the step attempt. - A workflow must not rely on detached background work that outlives the selected shell process to contribute deterministic stdout, stderr, `DAGU_OUTPUT_FILE` records, artifacts, or side effects. ### Array Form - Array-form `run` is an ordered list of command-form entries. - Each non-empty array entry runs as one command-form invocation. - Array-form entries must not contain line breaks. - If a resolved array-form entry contains a line break, the step fails before the selected shell starts. - Entries run sequentially. - Each started entry uses its own selected-shell invocation. - Shell process state does not carry from one array entry to the next. - State created outside the shell process, such as files written in the working directory, may be observed by later entries according to the operating system and selected shell behavior. - Each entry starts with the same step process working directory selected for the step, unless external state changes make that directory unusable before a later entry starts. - Each entry receives the step attempt environment assembled before command execution starts. - Environment changes made inside one entry do not change the environment Dagu supplies to later entries. - A workflow that needs shared shell state across multiple lines must use script-form `run`. - Dagu captures stdout and stderr from started entries as the step attempt's stdout and stderr streams in entry order. - A runtime feature that consumes captured stdout or stderr for the step attempt observes the aggregate captured stream from all started entries. - The aggregate captured stream is deterministic only for work completed before each entry's selected shell process exits. - All entries in one step attempt share the same `DAGU_OUTPUT_FILE` for that attempt. - Dagu parses `DAGU_OUTPUT_FILE` only after all array entries complete successfully. - `DAGU_OUTPUT_FILE` records are deterministic only for records written before the selected shell process for the final successful entry exits. - If any array entry fails, aborts, or times out, the step attempt publishes no outputs from `DAGU_OUTPUT_FILE`. - Execution stops at the first failed entry. - Later entries must not start after an earlier entry fails. - The step attempt result is the first failed entry result, or success when every non-empty entry succeeds. ### Diagnostics And Secrecy - Dagu-generated diagnostics in normal step errors, run logs, status records, history records, workflow events, and DAG-run detail responses must not expose secret-backed values inserted into resolved command text by Dagu value resolution. - Dagu-generated diagnostics may include authored command text, command text with secret-backed values masked, or a summarized command description. - An explicit inspection or debug surface may show resolved command text only when workflow authors opt in to that surface. - Any explicit inspection or debug surface that shows resolved command text must apply secret masking owned by the environment and secret specs. - User command stdout and stderr are user-controlled output. Dagu captures them as defined by the shared `run` spec. ### Shell Construction #### Common Rules - Dagu constructs the shell invocation from the selected shell, configured shell arguments, command payload, and configured shell packages. - Dagu supplies one command-string payload to the selected shell's command-string carrier. - Unless a shell-specific rule below defines generated shell-owned wrapping, the command-string payload is the resolved command text. - Generated shell-owned wrapping must preserve the resolved command text as the user-authored command payload and must not split it into a user-authored argv list. - Configured shell arguments may provide shell options. - Configured shell arguments must not provide a separate command-string operand. - If configured shell arguments include the selected shell's command-string flag, that flag must be the final configured shell argument. - If a configured command-string flag is followed by another configured shell argument, the step fails before the selected shell starts. - Command-form shell invocation order is: 1. Selected shell. 2. Configured shell arguments before any configured command-string flag. 3. Dagu-added shell options and shell package arguments. 4. Command-string flag. 5. One command-string payload. - If configured shell arguments include the command-string flag, Dagu must insert Dagu-added shell options and shell package arguments before that configured flag. - Configured shell packages are valid only when the selected shell family defines package behavior. - If configured shell packages are present and the selected shell family does not define package behavior, the step fails before the selected shell starts. - Shell-specific defaults are part of the command-form contract and must be covered by black-box tests before being changed. - Shell family matching uses the normalized name of the selected shell. - The normalized shell name is the selected shell executable basename after path components and an optional `.exe` suffix are removed. - Shell family matching is ASCII case-insensitive. #### Unix-Like Shells - For this spec, Unix-like shells have normalized names `sh`, `bash`, `zsh`, `ksh`, `ash`, or `dash`. - Unix-like shell execution includes `-c`; if configured shell arguments already include `-c`, that existing flag identifies where Dagu supplies the command-string operand. - The only valid Unix-like command-string carrier token is exactly `-c`. - A Unix-like configured shell argument that embeds a command string in the carrier token, such as `-cecho hello`, is invalid. - A Unix-like configured shell argument that combines `c` with other options, such as `-lc`, is invalid and the step fails before the selected shell starts. - Use separate shell arguments for other shell options. For example, use `[-l]` when login-shell behavior is needed; Dagu adds or uses `-c` separately. - Unix-like shell execution includes `-e` when the selected shell is Unix-like, no step-level shell is explicitly specified, and configured shell arguments do not already include `-e`. #### PowerShell - PowerShell rules apply to normalized shell names `powershell` and `pwsh`. - PowerShell execution includes `-NoProfile` and `-NonInteractive` unless configured shell arguments already include them. - PowerShell execution includes `-Command` unless configured shell arguments already include `-Command` or `-C`; an existing command-string flag identifies where Dagu supplies the command-string operand. - PowerShell command-string carrier tokens are `-Command` and `-C`. - PowerShell command-string flags are matched ASCII case-insensitively. - A configured PowerShell command-string carrier with an inline value, such as `-Command:Write-Host ok`, is invalid. - PowerShell `-EncodedCommand` is invalid for command-form `run`. - PowerShell command execution normalizes PowerShell error handling and UTF-8 text encoding before user command text runs. - Under PowerShell normalization, a PowerShell error written by `Write-Error` fails the step unless the command handles the error. - Dagu determines PowerShell command success from the PowerShell process exit code. - Native executable error semantics inside the command remain PowerShell-owned unless Dagu's PowerShell normalization explicitly changes them. #### Windows cmd - Windows `cmd` rules apply to normalized shell name `cmd`. - `cmd` execution includes `/c` unless configured shell arguments already include `/c` or `/C`; an existing command flag identifies where Dagu supplies the command-string operand. - `cmd` command-string carrier tokens are `/c` and `/C`. - A configured `cmd` command-string carrier with an inline command body, such as `/cecho hello`, is invalid. - `cmd` resolves the configured `COMSPEC` path when the selected shell is `cmd` or `cmd.exe` and `COMSPEC` points to an existing path. #### Nix Shell - Nix shell rules apply to normalized shell name `nix-shell`. - Nix shell execution adds `-p ` for each configured shell package. - Nix shell execution includes `--pure` unless configured shell arguments already include `--pure` or `--impure`. - Nix shell execution includes `--run` unless configured shell arguments already include it; an existing `--run` identifies where Dagu supplies the command-string operand. - The only valid Nix shell command-string carrier token is exactly `--run`. - A configured Nix shell command-string carrier with an inline command body, such as `--run=echo hello`, is invalid. - Nix shell command execution includes fail-fast behavior when no step-level `with.shell` is explicitly specified. - Under Nix shell fail-fast behavior, `echo before; false; echo after` must not execute `echo after` unless the command handles the failing command. #### Other Shells - Shells not covered by Unix-like, PowerShell, Windows `cmd`, or Nix shell rules use `-c` as the command-string flag. - For other shells, the only valid command-string carrier token is exactly `-c`. - For other shells, a configured command-string carrier with an inline command body, such as `-cecho hello`, is invalid. - Dagu does not add shell-specific default options for other shells. - If another shell does not support `-c` command-string execution, the step fails according to the normal runtime error rules. ## Errors Runtime must fail when: - The selected shell cannot be started. - The command cannot be started by the selected shell. - The selected shell cannot find the command path. - The resolved command text contains a line break. - The command exits with a non-zero code. - An array-form entry fails. - Configured shell arguments provide a separate command-string operand or use an invalid command-string carrier. - Configured shell packages are present for a shell family that does not define package behavior. ## Examples Single command: ```yaml steps: - id: hello run: echo hello ``` Shell command syntax remains shell-owned: ```yaml steps: - id: list run: find . -name "*.go" | sort ``` Array form: ```yaml steps: - id: checks run: - go test ./internal/spec/... - go test ./internal/runtime/... ``` Array entries do not share shell state: ```yaml steps: - id: build_ui run: - cd ui && pnpm install - cd ui && pnpm build ``` Use script form when shell state must carry across lines: ```yaml steps: - id: build_ui run: | cd ui pnpm install pnpm build ``` Wait for background work that belongs to the command result: ```yaml steps: - id: background run: './long-task.sh & task_pid=$!; wait "$task_pid"' ``` Custom shell: ```yaml steps: - id: bash_example run: printf "%s\n" "$BASH_VERSION" with: shell: bash shell_args: [-e, -o, pipefail] ```