Ansible shell Module: Run Shell Commands with Pipes & Redirects (Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
How to run shell commands in Ansible with the shell module (ansible.builtin.shell). Use pipes, redirects, environment variables. Difference from command.

What is the difference between command vs shell Ansible modules?
ansible.builtin.command executes a binary directly on the target host — no shell involvement, so pipes, redirects, and environment variable expansion are unavailable, but execution is more predictable and secure. ansible.builtin.shell routes the command through /bin/sh, enabling pipes (|), redirects (>), and shell built-ins at the cost of less predictable behavior. Use command when possible; reach for shell only when you genuinely need shell features.
See also: Ansible win_shell Module: Run PowerShell Commands on Windows (Guide)
command vs shell
command
- execute commands against the target Unix-based hosts
- it bypasses the shell
- always set changed to True
- execute shell commands against the target Unix-based hosts
- redirections and shell's inbuilt functionality
- always set changed to True
command and shell Ansible modules execute commands on the target node.
Generally speaking, is always better to use a specialized Ansible module to execute a task.
However, sometimes the only way is to execute a Linux command via command or shell module.
Let me reinforce again, you should avoid as much as possible the usage of command/shell instead of a better module.
Both modules execute commands on target nodes but in a sensible different way.
The command modules execute commands on the target machine without using the target shell, it simply executes the command. The target shell is for example the popular bash, zsh, or sh. As a side effect user environment, variable expansions, output redirections, stringing two commands together, and other shell features are not available. On the other side, every command executed using shell module has all shell features so it could be expanded in runtime. From the security point of viewcommand module is more robust and has a more predictable outcome because it bypasses the shell.
Both modules returned always changed status because Ansible is not able to predict if the execution has or has not altered the target system.
command module
- Execute commands on targets
command module won't be impacted by local shell variables because it bypasses the shell. At the same time, it may not be able to run "shell" built-in features and redirections.
shell module
- Execute shell commands on targets
shell Ansible module is potentially more dangerous than the command module and should only be used when you actually really need the shell functionality. So if you're not stringing two commands together (using pipes or even just && or ;), you don't really need the shell module. Similarly, expanding shell variables or file global requires the shell module. If you're not using these features, don't use the shell module. Sometimes it's the only way, I know.
See also: Ansible shell Module: Run Commands with Pipes & Redirects (Complete Guide)
Links
## Playbook The command vs shell Ansible modules in Ansible Playbook. Let me show you the difference between command vs shell Ansible modules in an Ansible Playbook.
command code
---
- name: command module Playbook
hosts: all
tasks:
- name: check uptime
ansible.builtin.command: uptime
register: command_output
- name: command output
ansible.builtin.debug:
var: command_output.stdout_linescommand execution
ansible-pilot $ ansible-playbook -i virtualmachines/demo/inventory commmand_shell/uptime.yml
PLAY [command module Playbook] ************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [demo.example.com]
TASK [check uptime] *******************************************************************************
changed: [demo.example.com]
TASK [command output] *****************************************************************************
ok: [demo.example.com] => {
"command_output.stdout_lines": [
" 12:51:34 up 8 min, 1 user, load average: 0.00, 0.05, 0.06"
]
}
PLAY RECAP ****************************************************************************************
demo.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-pilot $
shell code
---
- name: shell module Playbook
hosts: all
tasks:
- name: list file(s) and folder(s)
ansible.builtin.shell: 'ls -l *'
register: command_output
- name: command output
ansible.builtin.debug:
var: command_output.stdout_linesshell execution
ansible-pilot $ ansible-playbook -i virtualmachines/demo/inventory commmand_shell/list_files.yml
PLAY [shell module Playbook] **************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [demo.example.com]
TASK [list file(s) and folder(s)] *****************************************************************
changed: [demo.example.com]
TASK [command output] *****************************************************************************
ok: [demo.example.com] => {
"command_output.stdout_lines": [
"-rwxr-xr-x. 1 devops wheel 31 Mar 30 13:39 example.sh"
]
}
PLAY RECAP ****************************************************************************************
demo.example.com : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
ansible-pilot $wrong module code
---
- name: shell module Playbook
hosts: all
tasks:
- name: list file(s) and folder(s)
ansible.builtin.command: 'ls -l *'
register: command_output
- name: command output
ansible.builtin.debug:
var: command_output.stdout_lineswrong module execution
ansible-pilot $ ansible-playbook -i virtualmachines/demo/inventory commmand_shell/list_files_command.yaml
PLAY [shell module Playbook] **************************************************************************
TASK [Gathering Facts] ****************************************************************************
ok: [demo.example.com]
TASK [list file(s) and folder(s)] *****************************************************************
fatal: [demo.example.com]: FAILED! => {"changed": true, "cmd": ["ls", "-l", "*"], "delta": "0:00:00.003038", "end": "2022-04-06 13:01:54.498403", "msg": "non-zero return code", "rc": 2, "start": "2022-04-06 13:01:54.495365", "stderr": "ls: cannot access '*': No such file or directory", "stderr_lines": ["ls: cannot access '*': No such file or directory"], "stdout": "", "stdout_lines": []}
PLAY RECAP ****************************************************************************************
demo.example.com : ok=1 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
ansible-pilot $Conclusion
Now you know the difference between command vs shell Ansible modules and their use case.
You know how to use it based on your use case.
See also: Three options to Safely Limit Ansible Playbooks Execution to a Single Machine
Key Difference
# command — runs without shell (no pipes, redirects, wildcards)
- ansible.builtin.command: ls /opt/myapp
# shell — runs through /bin/sh (pipes, redirects, wildcards work)
- ansible.builtin.shell: ps aux | grep nginx | wc -lWhen to Use command
# Simple commands without shell features
- command: systemctl status nginx
register: result
changed_when: false
- command: /opt/myapp/migrate.sh
args:
chdir: /opt/myapp
- command: python3 setup.py install
args:
creates: /usr/local/lib/python3/dist-packages/mylibWhen to Use shell
# Pipes
- shell: cat /var/log/syslog | grep ERROR | tail -20
register: errors
# Redirects
- shell: /opt/backup.sh > /var/log/backup.log 2>&1
# Wildcards
- shell: rm /tmp/*.tmp
# Environment variables
- shell: echo $HOME
# Complex one-liners
- shell: |
for f in /opt/configs/*.conf; do
grep -l "old_value" "$f" && sed -i 's/old_value/new_value/g' "$f"
doneSecurity Comparison
# command — SAFER (no shell injection)
- command: "echo {{ user_input }}"
# Even if user_input contains "; rm -rf /", it's treated as a literal string
# shell — DANGEROUS with untrusted input
- shell: "echo {{ user_input }}"
# If user_input = "; rm -rf /", the shell executes the injection!Idempotency
# Both are NOT idempotent by default — always report "changed"
# Make them idempotent with creates/removes:
- command: /opt/setup.sh
args:
creates: /opt/myapp/.installed # Skip if exists
- shell: /opt/cleanup.sh
args:
removes: /tmp/cleanup-needed # Skip if doesn't exist
# Or with changed_when:
- command: /opt/check-status.sh
register: result
changed_when: false # Never report changedraw Module (SSH Only)
# For hosts without Python (bootstrap)
- raw: apt-get install -y python3
become: true
# No module_utils, no error handling, just raw SSH commandComparison Table
| Feature | command | shell | raw |
|---|---|---|---|
| Pipes | ❌ | ✅ | ✅ |
| Redirects | ❌ | ✅ | ✅ |
| Wildcards | ❌ | ✅ | ✅ |
| Env vars ($HOME) | ❌ | ✅ | ✅ |
| Shell injection safe | ✅ | ❌ | ❌ |
| Needs Python | ✅ | ✅ | ❌ |
| creates/removes | ✅ | ✅ | ❌ |
| chdir | ✅ | ✅ | ❌ |
| stdin | ✅ | ✅ | ❌ |
Best Practices
# 1. Prefer dedicated modules over command/shell
# WRONG
- shell: apt-get install nginx
# CORRECT
- apt: { name: nginx, state: present }
# 2. Use command over shell when possible
# WRONG
- shell: whoami
# CORRECT
- command: whoami
# 3. Always add changed_when for checks
- command: /opt/check.sh
changed_when: false
register: result
# 4. Use no_log for sensitive commands
- shell: echo "{{ vault_password }}" | /opt/setup.sh
no_log: trueFAQ
Why does command fail with pipes?
command bypasses the shell entirely — it calls the executable directly. Pipes (|) are a shell feature. Use shell for pipes.
Can I specify which shell to use?
- shell: my_command
args:
executable: /bin/bash # Use bash instead of /bin/shscript module vs shell?
script copies a local script to the remote host and executes it. shell runs inline commands. Use script for complex multi-line scripts stored as files.
Related Articles
See also
Category: installation
Watch the video: Ansible shell Module: Run Shell Commands with Pipes & Redirects (Guide) — Video Tutorial