skip to content

What is the difference between Ansible's command and shell modules, and which should you reach for by default?

level: juniorimportance: must knowfreq 66%

answer

  1. is there a shell in between?
  2. pipes and redirects need one of them
  3. metacharacters become literal arguments
  4. templated variables and injection
  5. argv avoids string parsing entirely

basics

~20 s

Ansible's command module executes a program directly with no shell, so pipes, redirects, globs and environment-variable expansion do not work. The shell module runs the command line through a shell on the target, so they do. Prefer command unless you need shell features.

solid answer

~50 s

`ansible.builtin.command` passes the program and its arguments straight to the operating system without a shell in between, so shell metacharacters — `|`, `>`, `&&`, `;`, `*`, `$HOME` — are handed to the program as literal text rather than interpreted. `ansible.builtin.shell` wraps the same command line in a shell on the managed host (`/bin/sh` by default, overridable with the `executable` argument), so all of that works. The default choice is `command`, for two reasons: there is no shell to misinterpret a variable that happens to contain a space or a semicolon, which removes a whole class of injection bugs when the command line is templated; and it makes the dependency on shell behaviour explicit when you do switch. Reach for `shell` deliberately, when you actually need a pipeline, a redirect, or expansion. Both share `chdir`, `creates` and `removes`, and neither is idempotent on its own.

code

yaml · 13 lines
yaml
- name: Restore a dump, needs a shell for the redirect
  ansible.builtin.shell:
    cmd: set -o pipefail && gunzip -c /tmp/db.sql.gz | psql app
    executable: /bin/bash
    creates: /var/lib/app/.restored

- name: Run a vendor installer with no shell involved
  ansible.builtin.command:
    argv:
      - /opt/vendor/install
      - --config
      - /etc/vendor/{{ env_name }}.conf
    creates: /opt/vendor/.installed

go deeper

for a junior

Be able to say plainly that command runs the program with no shell while shell runs it through one, and that pipes, redirects and globs therefore only work with shell. Name command as the default.

for a middle

Explain the practical consequences: templated variables become shell syntax under shell, argv removes string parsing altogether, and /bin/sh may be dash rather than bash on the target.

for a senior

Show review judgment — most shell tasks in a real repository should be a purpose-built module. Be ready to discuss pipefail, exit status of a pipeline, and raw as the pre-Python bootstrap escape hatch.

for a principal

Own the convention across the estate: what is allowed to shell out at all, how such tasks are reviewed and guarded, and when a repeated shell-out becomes a supported module instead of copy-paste in every role.

## Two modules, one difference Both modules run a command on the managed host and return its exit status, `stdout` and `stderr`. The single difference is whether a shell sits between Ansible and the program. `ansible.builtin.command` splits the command line into an argument vector and executes the program directly. Nothing interprets metacharacters. `ansible.builtin.shell` hands the whole string to a shell on the target — `/bin/sh` unless you set the `executable` argument — and lets the shell do its usual job: word splitting, glob expansion, variable expansion, pipelines, redirection, command chaining with `&&` and `;`. ```yaml # Fails: the > and the pipe are passed to `cat` as literal arguments - ansible.builtin.command: cat /etc/hosts | grep db > /tmp/db_hosts # Works: the shell parses the pipeline and the redirect - ansible.builtin.shell: cat /etc/hosts | grep db > /tmp/db_hosts ``` The classic interview symptom is a task that "does nothing but reports success": someone wrote a redirect under `command`, the program accepted the extra arguments or ignored them, and the output file never appeared. ## Why command is the default **Injection surface.** Task command lines are usually templated from variables. Under `shell`, a variable containing `; rm -rf /var/lib/app` or even an unexpected space becomes shell syntax. Under `command`, it is one literal argument. When the value comes from inventory, from a `register`ed result, or from a survey field, that difference is the difference between an argument and an incident. For full control you can use `command` with `argv` — an explicit list of arguments, with no string parsing at all. **Explicitness.** Choosing `shell` documents "this task depends on shell features". A codebase where everything is `shell` tells the reader nothing. **Portability.** `/bin/sh` is not the same shell everywhere. Bash-isms written under `shell` on a Debian control host can behave differently on a target where `/bin/sh` is dash. If you need bash specifically, say so with `executable: /bin/bash` rather than assuming. ## What they share Both accept `chdir` (run from a directory), `creates` (skip if a path exists) and `removes` (skip if a path does not exist), and both are non-idempotent by nature — Ansible cannot inspect what your command did, so a successful run reports changed. Both also skip by default under `--check`. Both also require Python on the managed host, because they are ordinary modules shipped to the target and executed there. When the host has no Python — a bare container, a network appliance, a machine you are about to bootstrap — `ansible.builtin.raw` is the escape hatch: it pushes the command straight down the SSH connection with no module payload at all. Use it to install Python, then stop using it. ## When you should be using neither The most common right answer to "command or shell?" in a review is "neither". Reaching for a shell is usually a sign that a purpose-built module exists and was not looked for: `ansible.builtin.uri` for HTTP calls, `ansible.builtin.git` for clones and checkouts, `ansible.builtin.unarchive` for tarballs, `ansible.builtin.file` for permissions and symlinks, `ansible.builtin.service` for daemons, `ansible.builtin.package` for installs. Those modules compare state and report changed honestly; a shell-out cannot. When you genuinely must shell out — a vendor installer, a one-off migration, a CLI with no module — the reviewable form is: `command` rather than `shell` unless a shell feature is required, a `creates` guard so it runs once, and a clear task `name` explaining what the command is for. ## A small trap worth naming Running a long pipeline under `shell` reports the exit status of the *last* command in the pipeline, which is standard shell behaviour and means a failing first stage can pass silently. If that matters, set `executable: /bin/bash` and include `set -o pipefail` at the front of the command, or split the work into separate tasks. This is the sort of detail that separates "I have used shell" from "I have debugged shell in a playbook".

  • A task using ansible.builtin.command reports ok but the output file it should have written is missing. What happened?
    The command line almost certainly contains a redirect. `command` runs the program directly, so `>` and the filename are passed to the program as ordinary arguments rather than opening a file. Many programs ignore or tolerate the extra arguments and exit zero, so Ansible reports success. The fix is to switch that task to `shell`, or better, to write the file with a module such as `copy` or `template`.
  • When would you use ansible.builtin.raw instead of either module?
    When the managed host has no usable Python interpreter — a freshly booted minimal image, a stripped container, or a network device. Ordinary modules are Python payloads shipped to the target, so they cannot run there. `raw` sends the command straight over the connection with no payload. The normal use is a single bootstrap task that installs Python, after which everything else uses real modules.
  • How do you make a command module task run something under bash specifically?
    You cannot — `command` deliberately involves no shell. If you need bash, use `ansible.builtin.shell` with `executable: /bin/bash`, which is also the way to get bash-only constructs such as `set -o pipefail` or process substitution. Relying on `/bin/sh` being bash is a portability bug: on Debian and Ubuntu it is dash.

saying these in an interview costs you the question

  • Says shell is just a newer version of command
  • Uses a redirect or pipe under the command module
  • Believes command is idempotent and shell is not
  • Interpolates untrusted variables into a shell command line
  • Assumes /bin/sh is bash on every target

context