shell vs command at a Glance
| Feature | command | shell |
|---|---|---|
Pipes (|) | ❌ No | ✅ Yes |
Redirects (>, >>) | ❌ No | ✅ Yes |
Wildcards (*, ?) | ❌ No | ✅ Yes |
Environment variables ($HOME) | ❌ No | ✅ Yes |
Shell builtins (source, export) | ❌ No | ✅ Yes |
| Security | ✅ Safer | ⚠️ Injection risk |
| Preferred | ✅ Yes | When needed |
ansible.builtin.command (Preferred)
Runs commands without a shell. Safer because it avoids shell injection:
- name: Check disk usage
ansible.builtin.command: df -h
- name: Get hostname
ansible.builtin.command: hostname -f
register: result
- name: Show result
ansible.builtin.debug:
msg: "{{ result.stdout }}"
With Arguments
- name: Run with specific args
ansible.builtin.command:
cmd: /opt/app/bin/migrate --env production --verbose
chdir: /opt/app
- name: Only run if file doesn't exist
ansible.builtin.command:
cmd: /opt/app/setup.sh
creates: /opt/app/.setup_complete
ansible.builtin.shell (When You Need Shell Features)
Runs commands through /bin/sh. Use when you need pipes, redirects, or shell features:
# Pipes
- name: Find large files
ansible.builtin.shell: find /var/log -size +100M | sort -rn
# Redirects
- name: Save output to file
ansible.builtin.shell: df -h > /tmp/disk_report.txt
# Wildcards
- name: Remove temp files
ansible.builtin.shell: rm -f /tmp/*.tmp
# Environment variables
- name: Use HOME variable
ansible.builtin.shell: echo $HOME
# Chaining commands
- name: Build and deploy
ansible.builtin.shell: |
cd /opt/app
npm install
npm run build
systemctl restart myapp
When to Use Which
Use command (default choice):
# Simple commands without shell features
- ansible.builtin.command: systemctl restart nginx
- ansible.builtin.command: cat /etc/hostname
- ansible.builtin.command: /opt/scripts/deploy.sh
- ansible.builtin.command: python3 manage.py collectstatic --noinput
Use shell (only when needed):
# Pipes
- ansible.builtin.shell: ps aux | grep nginx | grep -v grep
# Redirects
- ansible.builtin.shell: mysqldump mydb > /backups/mydb.sql
# Glob patterns
- ansible.builtin.shell: cat /var/log/app/*.log | tail -100
# Shell built-ins
- ansible.builtin.shell: source /opt/app/venv/bin/activate && python manage.py migrate
# Complex one-liners
- ansible.builtin.shell: >
curl -s https://api.example.com/health |
python3 -c "import sys,json; d=json.load(sys.stdin); print(d['status'])"
Best Practices
1. Prefer Ansible Modules Over Shell
# ❌ Don't use shell for things modules handle
- ansible.builtin.shell: apt-get install -y nginx
- ansible.builtin.shell: useradd deploy
- ansible.builtin.shell: cp /tmp/config /etc/app/config
# ✅ Use the right module
- ansible.builtin.apt:
name: nginx
state: present
- ansible.builtin.user:
name: deploy
- ansible.builtin.copy:
src: /tmp/config
dest: /etc/app/config
2. Use creates/removes for Idempotency
- name: Run setup (only once)
ansible.builtin.command:
cmd: /opt/app/setup.sh
creates: /opt/app/.initialized
- name: Run cleanup (only if needed)
ansible.builtin.command:
cmd: /opt/app/cleanup.sh
removes: /tmp/cleanup_needed
3. Use changed_when for Accurate Reporting
- name: Check if migration needed
ansible.builtin.command: python3 manage.py showmigrations --plan
register: migrations
changed_when: "'[ ]' in migrations.stdout"
Security Warning
The shell module is vulnerable to injection if you pass untrusted input:
# ❌ DANGEROUS — user_input could contain "; rm -rf /"
- ansible.builtin.shell: "echo {{ user_input }}"
# ✅ SAFE — use command module or quote properly
- ansible.builtin.command:
argv:
- echo
- "{{ user_input }}"
Explore 800+ Ansible tutorials on AnsibleByExample.