Why Does Bash Behave Differently, When It Is Called as Sh

Why does bash behave differently, when it is called as sh?

When bash is invoked as sh, it (mostly) restricts itself to features found in the POSIX standard. Process substitution is not one of those features, hence the error.

Why does /bin/sh behave differently to /bin/bash even if one points to the other?

bash looks at the value of $argv[0] (bash is implemented in C) to determine how it was invoked.

Its behavior when invoked as sh is documented in the manual:

If Bash is invoked with the name sh, it tries to mimic the startup
behavior of historical versions of sh as closely as possible, while
conforming to the POSIX standard as well.

When invoked as an interactive login shell, or as a non-interactive
shell with the -login option, it first attempts to read and execute
commands from /etc/profile and ~/.profile, in that order. The
--noprofile option may be used to inhibit this behavior. When invoked as an interactive shell with the name sh, Bash looks for the variable
ENV, expands its value if it is defined, and uses the expanded value
as the name of a file to read and execute. Since a shell invoked as sh
does not attempt to read and execute commands from any other startup
files, the --rcfile option has no effect. A non-interactive shell
invoked with the name sh does not attempt to read any other startup
files.

When invoked as sh, Bash enters POSIX mode after the startup files are
read

There's a long list (currently 46 items) of things that change when bash is in POSIX mode, documented here.

(POSIX mode is probably useful mostly as a way to test scripts for portability to non-bash shells.)

Incidentally, programs that change their behavior depending on the name under which they were invoked are fairly common. Some versions of grep, fgrep, and egrep are implemented as a single executable (though GNU grep doesn't do this). view is typically a symbolic link to vi or vim; invoking it as view causes to open in read-only mode. The Busybox system includes a number of individual commands that are all symlinks to the master busybox executable.

Why does sed behave differently in a shell script?

The issue was simply that I had a long-forgotten bash alias changing sed to gsed — the GNU version as installed by Homebrew. That explains why sed --version reported itself as gsed at the command line. I had checked which sed from both the script and the prompt, but I didn't think about type and bash aliases.

$ type sed
sed is aliased to `gsed'

Why does /bin/sh behave differently on Mac and Ubuntu

  1. Why the /bin/sh behaves differently on on Mac and Ubuntu?

because they are both different shells. sh on ubuntu is the dash shell, sh on minix is the ash shell, sh on slackware is the bash shell. And since not too long is sh on OSX also the bash shell

If you want same behaviour, specify your shell... #!/bin/sh is, although strictly the never-in-free-systems-implemented-bourne-shell, generally the system shell which can vary from system to system ...use #!/bin/bash to literally say that you mean bash, use #!/bin/ksh to literally call the korn-shell etc.


  1. If I want to write the cross-platforma sh script, how should I avoid the platform-dependent code?
  1. officially: keep strict to the POSIX rules to run on any POSIX compliant shell

  2. pragmatically: write specific for one type of shell that is available on almost any system (not counting microcontrollers and SoC's or big irons and sparc stations,
    bash is available for most systems, but yes there are system who don't do bash so bash is not the best choice for 100% portability, but it is absolutely the most (ab)used ;-)

  3. best portable: As an Ubuntu user rule of thumb: if it runs on dash (the /bin/sh of ubuntu) it will run on virtually anything (including your router , your toaster and your coffeemachine).

  4. Above all: user2719058 is right, OSX is not Linux but BSD-UNIX so while they can run the same shell, the commands are just different enough to make it very very difficult to write a one-script-fits-all. Choosing the best shell will not change that....so the value of a unified scripting language is hereby proven to be very limited unless the system commands on every system are POSIX compliant as well.

tl;dr:
Unified cross-platform scripting is a pipe dream because differences in binaries across systems prevent this.

Why is Bash handling child processes different compared to Sh

Answered thanks to chepner and Charles Duffy:

bash -c has an implicit optimization where it uses exec to replace itself if possible. sh (dash) does not have this optimization. See also this observation.

To verify:

  • Process tree using bash:
❯ docker run --name test --rm --detach krallin/ubuntu-tini bash -c 'sleep 60'
03194d48a4dcc8225251fe1e5de2dcbb901c8a9cfd0853ae910bfe4d3735608d
❯ docker exec test ps axfo pid,ppid,args
PID PPID COMMAND
1 0 /usr/bin/tini -- bash -c sleep 60
7 1 sleep 60
  • Process tree using sh:
❯ docker run --name test --rm --detach krallin/ubuntu-tini sh -c 'sleep 60'
e56f207509df4b0b57f8e6b2b2760835f6784a147b200d798dffad112bb11d6a
❯ docker exec test ps axfo pid,ppid,args
PID PPID COMMAND
1 0 /usr/bin/tini -- sh -c sleep 60
7 1 sh -c sleep 60
8 7 \_ sleep 60

Why does printf behave differently when called from a Makefile?

@chepner is on the right track in their comment but the details are not quite right:

This is wild speculation, but I suspect there is some sort of
optimization being applied by make that causes the first example, as a
simple command, to be executing a third option, the actual binary
printf (found in /usr/bin, perhaps), rather than a shell. In your
second example, the ; forces make to use a shell to execute the shell
command line.

Make always uses /bin/sh as its shell, regardless of what the user is using as their shell. On some systems, /bin/sh is bash (which has a builtin printf) and on some systems /bin/sh is something different (typically dash which is a lightweight, POSIX-conforming shell) which probably doesn't have a shell built-in.

On your system, /bin/sh is bash. But, when you have a "simple command" that doesn't require a shell (that is, make itself has enough trivial quoting smarts to understand your command) then to be more efficient make will invoke that command directly rather than running the shell.

That's what's happening here: when you run the simple command (no ;) make will invoke the command directly and run /usr/bin/printf. When you run the more complex command (including a ;) make will give up running the command directly and invoke your shell... which is bash, which uses bash's built-in printf.

Basically, your script is not POSIX-conforming (there is no %b in the POSIX standard) and so what it does is not well-defined. If you want the SAME behavior always you should use /usr/bin/printf to force that always to be used. Forcing make to always run a shell and never use its fast path is much trickier; you'll need to include a special character like a trailing ; in each command.



Related Topics



Leave a reply



Submit