shell vs command at a Glance

Featurecommandshell
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✅ YesWhen 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.