Skip to content

fix: add shell mode to run_shell_script for pipe and redirect support#21130

Open
ihabshea wants to merge 3 commits intoPrefectHQ:mainfrom
ihabshea:fix/run-shell-script-pipe-support
Open

fix: add shell mode to run_shell_script for pipe and redirect support#21130
ihabshea wants to merge 3 commits intoPrefectHQ:mainfrom
ihabshea:fix/run-shell-script-pipe-support

Conversation

@ihabshea
Copy link

Currently run_shell_script splits commands with shlex.split and passes the result as a list to the subprocess. This means shell operators like pipes (|), redirects (>), and logical operators (&&, ||) get treated as literal arguments instead of being interpreted by the shell.

This adds a shell parameter (defaults to False) that, when set to True, runs the command through asyncio.create_subprocess_shell instead. This matches the subprocess.run(shell=True) interface that was suggested in #21021.

Example usage in prefect.yaml:

push:
  - prefect.deployments.steps.run_shell_script:
      script: aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin 123456.dkr.ecr.us-east-1.amazonaws.com
      shell: true

Default behavior is unchanged. Existing scripts that don't use shell: true work exactly as before.

Tests added for pipes, redirects, logical operators, env vars, directory support, multiline scripts, error propagation, and backward compatibility.

Fixes #11287

Adds a shell parameter to run_shell_script so that commands using
shell operators (pipes, redirects, logical operators) work as expected.

When shell=True, commands run through asyncio.create_subprocess_shell
instead of being tokenized with shlex.split and passed as a list.
Defaults to False to preserve existing behavior.

Fixes PrefectHQ#11287
@github-actions github-actions bot added bug Something isn't working docs labels Mar 16, 2026
Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 70722058d9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +228 to +234
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=directory,
env=current_env,
)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve subprocess cleanup for shell mode

When shell=True, this path launches a subprocess with asyncio.create_subprocess_shell but does not wrap it in a cleanup context or finally block, so cancellation of run_shell_script (for example, flow/deployment cancellation while a long command is running) can leave the child process running after the coroutine exits. The non-shell branch avoids this via open_process, which is explicitly designed to terminate and close processes on exceptions/cancellation, so shell mode currently regresses process lifecycle handling.

Useful? React with 👍 / 👎.

@codspeed-hq
Copy link

codspeed-hq bot commented Mar 16, 2026

Merging this PR will not alter performance

✅ 2 untouched benchmarks


Comparing ihabshea:fix/run-shell-script-pipe-support (8eb52fb) with main (25b9fc9)

Open in CodSpeed

split_command = shlex.split(command, posix=sys.platform != "win32")
if not split_command:
continue
async with open_process(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this to use asyncio.create_subprocess_exec to be consistent with the asyncio.create_subprocess_shell usage.

…, add process cleanup

Switches the non-shell code path from open_process to
asyncio.create_subprocess_exec for consistency with the shell path.

Both paths now share a try/finally block that kills the child process
on cancellation or unexpected errors.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working docs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

run_shell_script deployment step does not understand the pipe character (|)

2 participants