bash Is Not sh
As promised, this is a post about shebang usage, shell script portability, and bash vs sh syntax.
TL;DR
If you need bashisms, use #!/usr/bin/env bash.
If you want to use #!/bin/sh, make sure your code can run on an sh
implementation other than bash!
What is sh?
sh, or the Bourne Shell, was a shell first introduced in 1977 with
Research UNIX 7, from Bell labs.
sh was written by Stephen Bourne, as a replacement for earlier UNIX shells.
sh takes much of its syntax from ALGOL68, a programming language first
introduced in 1968. While most folks have never written a line of ALGOL68,
they’d likely be very familiar with the syntax! Structures like if fi, and
case esac, come from ALGOL.
For many years, sh was the default UNIX shell, and it inspired successors
like GNU’s bash, and the Almquist shell ash currently in use by FreeBSD,
NetBSD, Debian and Ubuntu (as dash), and BusyBox!
What is bash?
bash, or the Bourne-Again Shell, is a GNU replacement for the Bourne Shell,
which seeks to extend sh’s syntax.
While bash is perfectly capable of running sh scripts, it brings some extra
non-portable features to the table.
bash vs sh
sh is the original implementation of the language.
bash seeks to replace and extend the language, as a GNU developed clone.
This is not inherently bad.
However, as you will see, assuming that everyone has bash can be a bad thing.
Location location location!
By default, there is always an sh at /bin/sh.
This leads to the common usage of #!/bin/sh as the shebang in most shell
scripts.
When writing POSIX sh, or adhering to the common feature set shared by sh,
ash, dash, and bash, this is fine!
However, on Linux, /bin/sh tends to be a symlink to bash, which on Linux
resides usually at /bin/bash.
When using the #!/bin/sh shebang and writing bashisms, you inadvertently
open yourself up to portability issues, as bashisms can only be interpreted by
bash.
For more on bashisms, see:
https://en.wikipedia.org/wiki/Bash_(Unix_shell)#Portability
The Debian project famously ran into this issue when replacing their /bin/sh
with dash as opposed to bash.
The same occurred with Ubuntu.
https://wiki.ubuntu.com/DashAsBinSh
To quote:
“there have been a certain number of shell scripts written specifically for
Linux systems, some of which incorrectly stated that they could run with
/bin/sh when in fact they required bash, and these scripts will have
broken due to this change.”
As you can see, assuming /bin/sh is bash can lead to some problems!
Why not just use /bin/bash then?
Ah! #!/bin/bash, the favorite of many Linux shell programmers who believe
they’ve solved the issue of assuming /bin/sh is bash!
Why not just specify that you want bash?
While the logic is sound, there’s still a problem.
See, many OSes that aren’t Linux (yes, Linux isn’t UNIX and wasn’t even the
first UNIX-like,) either don’t have bash installed at all, or have it
somewhere other than /bin/bash!
For example, in FreeBSD, we use the Almquist Shell, ash, as our /bin/sh.
ash is a very light re-implementation of sh. We don’t have bash as part
of the base system at all!
If one wants to use bash, they can. A simple pkg install bash will do the
trick! However, ports and Base are separate in FreeBSD.
This means bash will install to /usr/local/bin/bash, and not /bin/bash.
I’m sure you can see the problem of using #!/bin/bash now: it won’t work
on anything other than Linux distros that have bash installed to /bin/!
What then, can we do?
env is fucking magic!
As any python programmer will undoubtedly be familiar with, the shebang
#!/usr/bin/env python is the recommended way to call python.
This too holds true for any shell script that doesn’t adhere to POSIX or the
common set of features amongst sh and its replacements and clones!
If you wish to use #!/bin/sh, you should be sticking to these minimal common
features, as a matter of course. It may be a little harder, but it supports
portability between platforms and ensures your code will run on the widest
variety of systems there is!
But I NEEEEEED bash features! (I don’t want to use awk for arrays)
That’s fine! As mentioned before, env is fucking magic!
It will search your $PATH, for the interpreter specified, and use it!
While we’ve established that #!/bin/bash isn’t portable, #!/usr/bin/env bash
is!
Using #!/usr/bin/env bash will result in env searching your $PATH for
any version of bash it can find.
This means whether bash is at /bin/bash, /usr/local/bin/bash, or anywhere
else, you’ll be able to correctly call bash and use them there bashisms to
your heart’s content!
Additionally, it serves as a notice to anyone seeking to use your program
that it requires bash, and not ash, sh, or any other sh re-implementation.
This is especially important for code you want to ensure is run correctly on non-Linux systems.
But I can’t find any resources on pure sh! (How do I know if it’s a bashism?)
Do you want to write pure sh? In my opinion, this is the best way to go,
though I understand many folks like their bashisms, or simply don’t know
any better.
Resources are available all over the internet!
The FreeBSD ash documentation is a great start.
Additionally, using a linter like shellcheck is a great way to find those
sneaky bashisms and learn new ways of doing The Thing!
Can you really write good shell code without bashisms?
Yup!
Perfect examples are iocage Legacy (or the fork iocell), a FreeBSD jail
manager written in pure sh, and vzvol, a ZFS zvol manager written by
yours truly.
You can find iocell at https://github.com/bartekrutkowski/iocell
and vzvol at https://github.com/RainbowHackerHorse/vzvol
vzvol is also a great example of modular shell code, which is a great way
to ensure extensibility and cleanliness of code.
I source functions from files the same way you would import functions
and libraries in C.
Closing Thoughts
bash isn’t bad. This isn’t me bashing bash :)
I just want things Done Right, so we can all benefit from each others’ code
without having to modify or edit!
Resources
Examples of pure sh code
https://github.com/RainbowHackerHorse/vzvol https://github.com/bartekrutkowski/iocell
Linters
https://github.com/koalaman/shellcheck
bashisms
https://mywiki.wooledge.org/Bashism
Writing pure sh
https://en.wikipedia.org/wiki/The_Unix_Programming_Environment https://legacy.gitbook.com/book/freebsdfrau/serious-shell-programming/details
Using env for Portability (Thanks, @nixcraft !)
https://www.cyberciti.biz/tips/finding-bash-perl-python-portably-using-env.html