Bash scripts are most portable when "bashisms" are avoided. Let's explore writing POSIX-compliant shell scripts that work on Ash/Dash and other shells.
Bash and Zsh are both advanced and user-friendly. Due to this, a generous and/or adventurous soul will encounter challenges when sharing shell scripts with others, or utilizing such scripts on machines with simpler shells.
Avoiding such unsavory mishaps is quite possible. It simply involves acknowledging the extra features provided by Bash, and either avoiding or being careful around these additions.
This is an ironic article, because in learning to write shell scripts that work beyond Bash, one learns Bash. In other words, learn some Bash tricks, so that you can choose when to avoid them and when you get to use them.
Here is a checklist I use to keep tabs on my own script writing. Does my script:
#!/bin/shas the first ("shebang") line of the script, not
#!/usr/bin/bashor other shell
- Avoid double-bracket tests
[[ ]]and instead use single-brackets
echo -ewhen newlines
'\n'need to be printed
- Use no other
readflag other than
-r, as in
- Avoid Bash's convenience redirects: use
>myfile 2>&1to redirect stdout and stderr to a file rather than
- Test accurately with dash or posh: Policy-compliant Ordinary SHell
- Only use standard flags and options with common utilities such as sed, grep, cut, test, and others
- Avoid issues discovered by checkbashisms
- Avoid issues discovered by shellcheck
#!/bin/sh read -p "Who would you like to greet? " if [[ -z $REPLY ]] ; then recipient="$REPLY" else recipient="World" fi echo -e "Hello\n$recipient\n"
You might save the following in the current working directory of your choice, as
In human language, the above script prompts for a greeting recipient, then sets the recipient to "World" if none was given, then greets the recipient on multiple lines.
The above works on Bash, but has issues on other shells. You may wish to run it once in Bash, just to feel good. Even in Zsh, though, it may raise some complaints.
Debian and Ubuntu come with dash installed. In fact, scripts invoked with
/bin/sh will run with dash by default. On Alpine, Tiny Core Linux, OpenWRT, and other distros that use BusyBox by default, the standard shell is also dash (although labeled as
ash). On Fedora, dash can be installed with
sudo dnf install dash. Other distros may also include dash in their repositories.
docker run -it debian dash
You may also try my POSIX playground container, with a variety of tools, including dash. By default, it uses posh: Policy-compliant Ordinary SHell, which is slightly stricter than dash. It can be launched with:
docker run -it -v "$PWD:/work" docker.io/bowmanjd/posix-playground
See the article for a deeper explanation.
In all of the above,
podman can replace
docker without a problem.
Can you try running the
example-noncompliant.sh script above, but with dash, not Bash?
Or, using Docker or Podman:
docker run -v "$PWD:/work" -it debian dash /work/example-noncompliant.sh
The output is likely something resembling:
dash: 3: read: arg count dash: 5: [[: not found -e Hello
A few learning points can be derived from that output.
[[ construct is a safe one if using Bash or another shell that supports bashisms. It has some convenient features, like regex matching using
=~, and has less risks with string matching.
That said, you will generally not go wrong with the single bracket approach:
[ ] (an alias for
test). Always be sure to quote variables, but that is good advice anyway. If you need regular express matching, use
Bottom line: not all shells support
When using the
read command to get input, here are a few suggestions:
- Always specify the variable, rather than relying on Bash's default
read -rand no other flags. Using
-rprohibits the user from using backslash to escape characters, which can cause issues later. And no other flag is supported by POSIX
- Instead of specifying a prompt with
-p, just use a
printfcall prior to the
readcommand. Again, POSIX
readdoes not support such a flag, plus the
-poption means something different to Zsh's
Given these rules, our script should not use
read -p "Who would you like to greet? " but rather:
printf "Who would you like to greet? " read -r recipient
echo command works great when we know we want to output a simple string, followed by a newline.
However, if we have newlines in a string we want to print, or if printing without a trailing newline is desired, then
echo -e will be our friend.
So, instead of
echo -e "Hello\n$recipient\n" in our code above, this would be better:
printf "Hello\n%s\n\n" "$recipient"
Note the variable substitution going on with
%s in the first string (the format string). Do not put shell variables like
$recipient in the format string. This is the way.
Given the above concerns, let's completely rewrite our greeting script:
#!/bin/sh printf "Who would you like to greet? " read -r recipient if [ -z "$recipient" ] ; then recipient="World" fi printf "Hello\n%s\n\n" "$recipient"
You might save the above with the filename
example-posix.sh or similar.
When you run it, does it behave the same as the noncompliant script?
There is a tool embedded in the Debian devscripts project, called checkbashisms. It is a simple but powerful Perl script that ferrets out any bashisms in a shell script that begins with the
#!/bin/sh shebang line.
On Debian and Ubuntu, it can be installed with
sudo apt install devscripts and on Fedora with
sudo dnf install devscripts-checkbashisms while Alpine is
sudo apk add checkbashisms. Other distros may have something similar. You might also try installing Perl, then downloading and running the checkbashisms Perl script itself.
What happens when you run it on
example-posix.sh? So telling...
My new favorite shell scripting helper is Shellcheck. You can paste your shell script online and check it there, or install shellcheck in the usual way (Debian, Ubuntu, Fedora, Alpine, Archlinux, and others have it readily available in the standard package repositories.)
It does raise the POSIX-compliance flag on any lines that need it, but many other issues are checked as well. Your code might run just fine, but have gotchas that need some attention. Shellcheck will help you there. I integrate it into my editor, so that I can lint while I type.
Thankfully, the POSIX.1-2017 standard is openly documented. Consider this: when discovering and testing options for a given tool like
sed, instead of going to the GNU pages, the distro man pages, or the Bash or Zsh docs, why not go to the POSIX spec itself? The list of utilities and their options is plainly explained.
In instances where you really need an enhancement provided by the extended tools, you can make that choice. With the POSIX spec in hand, it becomes an informed decision.
Often, I find that I don't need
sed -E or
grep -E as badly as I thought. A few extra escape characters, and I am there.
Sometimes, when the extended syntax provided by GNU utilities is warranted, it may be a sign that the right tool for the job isn't the shell and its compatriots at all.
If your system has Python, Ruby, NodeJS, or other favorite language, might that be a more robust, flexible, and consistent option? Even in circumstances (embedded systems) in which those runtimes would be too bulky, perhaps remote scripting from another machine is in order. For instance, one could use Python to SSH to the remote machine, gain the information necessary, perform some logic, then send the appropriate commands back, without Python being necessary on the target machine.
In my research for this article, I encountered some resources you may find at least as interesting as this one:
- The informative and well-written A Brief POSIX Advocacy: Shell Script Portability by Arnaud Tomeï
- The Autoconf portable shell guide
- Sven Mascheck's remarks on various Unix tools
- How to make bash scripts work in dash by Greg Wooledge
Please feel free to share your tips, questions, corrections in the comments!