Writing Bash Scripts that aren't Only Bash: Checking for Bashisms and Using DashJonathan Bowman Created: February 21, 2021 Updated: July 02, 2023 [Linux] #bash #shell #posix
Shells like Bash or Zsh are advanced and user-friendly, and include features beyond what a simpler POSIX-compliant shell might offer. You will do well to utilize the full features of your shell when writing scripts.
There are situations, however, when portability should be a valued feature, allowing the script to run on a variety of shells.
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.
🔗A summary checklist
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 shellcheck
#!/bin/sh if ; then recipient="" else recipient="World" fi
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.
🔗Dash, a POSIX compliant shell
Dash is a derivative of the Ash (Almquist, named for the original creator) shell. It is meant to be POSIX-compliant.
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.
Using Docker or Podman, running dash is easy, as in this example:
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:
See the article for a deeper explanation.
In all of the above,
podman can replace
docker without a problem.
🔗Testing the example
Can you try running the
example-noncompliant.sh script above, but with dash, not Bash?
Or, using Docker or Podman:
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.
🔗Avoid double-bracket tests
[[ 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 when newlines are at issue
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:
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.
🔗Refactoring the example
Given the above concerns, let’s completely rewrite our greeting script:
#!/bin/sh if [ ; then recipient="World" fi
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? Hopefully not; try it out.
🔗Finding the bashisms with
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…
🔗Pursuing best practices with
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.
🔗Measure against the POSIX specification
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.
🔗Consider other languages and configuration tools
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.
This is the reason such tools as Ansible, Saltstack, Chef, and Puppet exist. These, too, can be quite bloated if the needs are simple. But they are unbeatable for flexibility and repeatability.
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 contact me to share your tips, questions, or corrections!
Back to top