bash Is Not sh

As promised, this is a post about shebang usage, shell script portability, and bash vs sh syntax.


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:

The Debian project famously ran into this issue when replacing their /bin/sh with dash as opposed to bash. The same occurred with Ubuntu.

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 and vzvol at

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!


Examples of pure sh code



Writing pure sh

Using env for Portability (Thanks, @nixcraft !)

Written on June 16, 2018