A shell script prints messages with `echo "$msg"`. Why do portability-conscious authors replace that with `printf '%s\n' "$msg"`, and what actually breaks when the same script runs under a different shell?
answer
- same command, different output per shell
- POSIX left this one implementation-defined
- dash expands escapes, bash does not
- a value of -n vanishes
- format string is the only literal
basics
~20 secho's handling of backslashes and a leading -n differs by shell: dash's echo expands backslash escapes, bash's does not, and a value of -n disappears entirely. printf's format semantics are specified, so printf '%s\n' behaves the same everywhere.
solid answer
~50 sPOSIX deliberately leaves `echo`'s treatment of `-n` and of backslash escapes implementation-defined, and the shells really do differ: dash's builtin `echo` expands `\t` and `\n` with no flag at all, bash's prints them literally unless you pass `-e` or set `shopt -s xpg_echo`, and BSD's `/bin/echo` has no `-e` option. On top of that, `echo "$var"` where `$var` happens to be `-n` prints nothing, because the value is eaten as an option. So the same script emits different bytes depending on what `/bin/sh` is on the box. `printf` is specified: the format string is the only place escapes are interpreted, and it is reused until the arguments run out. The rule I follow is `printf '%s\n' "$value"` for anything I did not write literally myself — and never `printf "$value"`, because a `%` in the data would then be read as a conversion specifier.
code
bash · 8 lines#!/usr/bin/env bash
msg='C:\temp\new'
echo "$msg"
printf '%s\n' "$msg"
flag='-n'
echo "$flag"
printf '%s\n' "$flag"go deeper
Know that printf takes a format string plus arguments, and get into the habit of writing printf '%s\n' "$var" instead of echo "$var" when the value comes from a variable.
Be able to say precisely what differs: -n and backslash handling are implementation-defined, dash expands escapes without a flag, bash needs -e, and a value of -n is consumed as an option.
Show judgment about where it matters — data-carrying output, #!/bin/sh scripts, anything piped into another program — and be able to name the printf format-string trap as a review item rather than a style nit.
Frame it as an interface-contract question: your scripts' stdout is consumed by other programs, so output must be byte-stable across interpreters. Encode that in a shared logging helper rather than relying on each author to remember.
## echo is under-specified on purpose The POSIX specification for `echo` is unusually weak: it says that the handling of a `-n` first operand and of backslash escape sequences is implementation-defined, and an XSI-conformant `echo` is required to *expand* escapes such as `\n`, `\t` and `\c` while treating `-n` as ordinary text. Two conforming shells can therefore produce different output for the same command, and both are right. That is the whole problem: `echo` is not a portable interface, it is a family of similar commands sharing a name. ## What the implementations actually do - **bash's builtin**: prints its arguments literally. `-n` suppresses the trailing newline, `-e` turns escape interpretation on, `-E` turns it off. `shopt -s xpg_echo` flips the default so escapes are expanded without `-e`. - **dash's builtin** (dash is `/bin/sh` on Debian and Ubuntu): always expands backslash escapes, and has no `-e` — passing `-e` prints the literal string `-e`. - **BSD `/bin/echo`** (macOS): supports `-n`, has no `-e`, and does not expand escapes. - **GNU coreutils `/bin/echo`**: escapes only with `-e`. So this script prints two different things depending on the interpreter: ```bash msg='C:\temp\new' echo "$msg" # bash: C:\temp\new dash: C:<tab>emp<newline>ew ``` That is not a cosmetic difference. Windows paths, regular expressions, TeX fragments and JSON snippets all contain backslashes, and a script that mangles them under one `/bin/sh` and not another is a genuinely painful bug to chase. ## The leading-dash trap Even within one shell, `echo` cannot faithfully print arbitrary data: ```bash flag='-n' echo "$flag" # bash prints nothing at all printf '%s\n' "$flag" # prints -n ``` Quoting does not help, because the shell has already finished quote removal by the time `echo` inspects its first argument. There is no portable way to make `echo` print a value that might begin with a dash — the interface simply lacks the escape hatch. ## What printf gives you instead `printf` is specified in detail. It takes a format string and arguments; escape sequences are interpreted **only** in the format, and the format is reused until the arguments are exhausted: ```bash printf '%s\n' one two three # prints three lines printf 'name=%s age=%d\n' bob 42 ``` That reuse property is genuinely useful: `printf '%s\n' "${arr[@]}"` emits one array element per line with no loop. And because the data goes through `%s`, its content is never reinterpreted — dashes, backslashes and newlines all come out exactly as they went in, in every shell. ## The one printf trap `printf` is safe only if the format is a literal you wrote: ```bash user_input='100% done' printf "$user_input\n" # broken: % begins a conversion specifier printf '%s\n' "$user_input" # correct ``` Passing data as the format is the shell's version of a format-string bug. It produces garbage output at best, and if the value comes from outside the script it is a data-driven defect a reviewer should flag on sight. The habit that makes this impossible is: the format string is always a single-quoted literal, and every dynamic value is an argument. ## Being honest about when echo is fine Don't over-claim in an interview. `echo` with a fixed literal that contains no backslashes and no leading dash, in a script you know runs under bash, is fine and universally readable. The rule earns its keep for *variables*, for anything a user or another program supplied, and for `#!/bin/sh` scripts where you do not control which shell interprets them. Writing to standard error follows the same shape: `printf '%s\n' "error: $reason" >&2`.
- Why is `printf "$msg"` a defect even when `$msg` is a message your own script composed?Because the first argument is the format string, and any `%` in it starts a conversion specifier. A message containing `100%` or a filename with `%s` in it produces garbage or consumes arguments that do not exist. Keep the format a single-quoted literal — `printf '%s\n' "$msg"` — so data can never be interpreted as formatting.
- How does printf behave when you give it more arguments than the format consumes?It reuses the format until the arguments run out. `printf '%s\n' a b c` prints three lines, which makes it a clean way to emit each element of a list without a loop. If arguments run out mid-format, the remaining conversions produce an empty string or zero.
- Is there any portable way to print a string without a trailing newline?`printf '%s' "$value"` — no newline, no flag, works in every shell. `echo -n` is the non-portable option: bash and BSD echo honour it, while an XSI-style echo prints the literal `-n`. This is one of the cases where printf is not merely tidier but the only correct answer.
saying these in an interview costs you the question
- Believing echo behaves identically in every shell
- Using echo -e and assuming it is portable
- Passing a variable as printf's format string
- Thinking quoting protects echo from a leading -n
- Claiming printf is slower because it is an external command