macOS `sh` prints `-e` / `-n` literally: `echo -e` in a `#!/bin/sh` script, and `C:\new` turned into a newline — use `printf '%s\n'`
A small script with a #!/bin/sh line prints its own option flags:
$ mkdir demo && cd demo
$ printf '%s\n' '#!/bin/sh' 'echo -e "name\tsize"' 'echo -n "total: "' 'echo 42' > report.sh
$ chmod +x report.sh
$ ./report.sh
-e name size
-n total:
42
The tab is there, so the \t was expanded — but -e was printed instead of
being read as an option, and -n didn’t suppress the newline either. Run the
same file with bash or zsh and it looks fine, which is why it “works on my
machine” when someone tests it with bash report.sh:
$ bash report.sh
name size
total: 42
$ sh report.sh
-e name size
-n total:
42
$ zsh report.sh
name size
total: 42
Why: /bin/sh is bash in POSIX mode
On this Mac /bin/sh looks like a small launcher that runs the shell named by
/private/var/select/sh, which points at bash 3.2:
$ ls -l /bin/sh /private/var/select/sh
-rwxr-xr-x 1 root wheel 101232 Jun 25 11:29 /bin/sh
lrwxr-xr-x 1 root wheel 9 Jun 25 11:29 /private/var/select/sh -> /bin/bash
$ /bin/sh --version | head -n 1
GNU bash, version 3.2.57(1)-release (arm64-apple-darwin25)
Started as sh, bash turns on POSIX mode, and in POSIX mode its echo
behaves the way the POSIX/XSI echo does: no -e, no -n, backslash
escapes always expanded. What I saw here: shopt reports xpg_echo as
off, and turning it off again doesn’t change anything; only leaving POSIX
mode does. For echo, bash --posix behaved the same as sh here.
$ /bin/sh -c 'set -o | grep posix; shopt xpg_echo'
posix on
xpg_echo off
$ /bin/sh -c 'shopt -u xpg_echo; echo -e "a\tb"'
-e a b
$ /bin/sh -c 'set +o posix; echo -e "a\tb"'
a b
$ bash --posix -c 'echo -e "a\tb"'
-e a b
set +o posix is not a fix worth shipping — the script would then depend on
/bin/sh being bash. The dash that macOS also ships (Debian and Ubuntu, by general knowledge, not tested here,
use dash as /bin/sh) prints -e too:
$ /bin/dash -c 'echo -e "a\tb"'
-e a b
The shebang doesn’t protect you if someone types sh script: sh reads the
file itself and the #!/bin/bash line is just a comment.
$ printf '%s\n' '#!/bin/bash' 'echo -e "a\tb"' > b.sh
$ chmod +x b.sh
$ ./b.sh
a b
$ sh b.sh
-e a b
Each shell expands backslashes differently
Without any option, the three shells already disagree about \t:
$ /bin/sh -c 'echo "a\tb"'
a b
$ bash -c 'echo "a\tb"'
a\tb
$ zsh -fc 'echo "a\tb"'
a b
zsh’s echo expands escapes by default (like sh here), bash’s doesn’t.
That matters more for data than for literals. A Windows path read from a
file goes through echo "$p" and comes out with a newline and a tab in it
— in sh and in zsh, the default login shell on macOS:
$ printf '%s\n' 'C:\new\temp' > path.txt
$ /bin/sh -c 'p=$(cat path.txt); echo "$p"'
C:
ew emp
$ bash -c 'p=$(cat path.txt); echo "$p"'
C:\new\temp
$ zsh -fc 'p=$(cat path.txt); echo "$p"'
C:
ew emp
Quoting "$p" doesn’t help: the shell passes the backslashes through
untouched, and it’s echo that expands them.
Fix: printf, with the data in %s
printf does the same thing in all of them. Put the format in the first
argument and the data in %s arguments:
$ for s in /bin/sh bash zsh /bin/dash; do $s -c 'p=$(cat path.txt); printf "%s\n" "$p"'; done
C:\new\temp
C:\new\temp
C:\new\temp
C:\new\temp
Don’t put the data into the format string. A % in the value is read as a
conversion — here % d (space flag, d) ate the space and the d, printed
` 0`, and the exit status was still 0:
$ p='50% done'; printf "$p\n"; echo "exit $?"
50 0one
exit 0
$ printf '%s\n' "$p"
50% done
The script from the top, with printf replacing both echo -e and
echo -n, gives the same bytes in all four shells:
$ printf '%s\n' '#!/bin/sh' 'printf "name\tsize\n"' 'printf "%s" "total: "' 'echo 42' > report2.sh
$ chmod +x report2.sh
$ for s in ./report2.sh "bash report2.sh" "zsh report2.sh" "/bin/dash report2.sh"; do eval "$s" | od -c | head -n 2; done
0000000 n a m e \t s i z e \n t o t a l :
0000020 4 2 \n
0000000 n a m e \t s i z e \n t o t a l :
0000020 4 2 \n
0000000 n a m e \t s i z e \n t o t a l :
0000020 4 2 \n
0000000 n a m e \t s i z e \n t o t a l :
0000020 4 2 \n
(od pads its last line with spaces; trimmed here.) echo 42 is still
fine: plain text without a leading - or a backslash prints the same in the four shells I tried.
If you’re writing zsh only, print -r -- "$p" or echo -E "$p"
printed the path unchanged too:
$ zsh -fc 'p=$(cat path.txt); print -r -- "$p"; echo -E "$p"'
C:\new\temp
C:\new\temp
Checklist
-eor-nshowing up in a script’s output on macOS: the script runs under/bin/sh(shebang#!/bin/sh, or started assh script), which is bash 3.2 in POSIX mode. Itsechotakes no options and expanded the\tand\nI tried backslashes.shopt -u xpg_echodidn’t change that here;set +o posixdid, but don’t rely on/bin/shbeing bash —/bin/dashprints-eas well.sh scriptignores a#!/bin/bashline.- Values with backslashes (like
C:\new) are mangled byecho "$var"inshand in zsh, not in bash. - Use
printf '%s\n' "$var"for data,printf '%s' "..."instead ofecho -n, andprintf 'a\tb\n'instead ofecho -e. - Never
printf "$var": a%in the value is a conversion (50% done→50 0one, exit 0). - zsh-only scripts:
print -r -- "$var"orecho -E "$var".
Tested on macOS 26.5.2 (Apple M1): /bin/sh → GNU bash 3.2.57(1)-release
in POSIX mode, /bin/bash 3.2.57, zsh 5.9 (arm64-apple-darwin25.0),
/bin/dash (dash-16). All console lines replayed from this post in one
zsh -f session in a temporary directory. Linux, newer bash versions, and
GNU coreutils’ /bin/echo and /usr/bin/printf were not tested — only the
shells’ built-in echo and printf.