Bugs and typos in Linux Bash scripts can do dire things when the script is run. Here are some ways to check the syntax of your scripts before you even run them.
Those Pesky Bugs
Writing code is hard. Or to be more accurate, writing bug-free non-trivial code is hard. And the more lines of code there are in a program or script, the more likely it becomes that there will be bugs in it.
The language you program in has a direct bearing on this. Programming in assembly is much tougher than programming in C, and programming in C is more challenging than programming in Python. The more low-level the language you’re programming in, the more work you have to do yourself. Python might enjoy in-built garbage-collection routines, but C and assembly certainly don’t.
Writing Linux shell scripts poses its own challenges. With a compiled language like C, a program called a compiler reads your source code—the human-readable instructions you type into a text file—and transforms it into a binary executable file. The binary file contains the machine code instructions that the computer can understand and act upon.
The compiler will only generate a binary file if the source code it’s reading and parsing obeys the syntax and other rules of the language. If you spell a reserved word—one of the command words of the language—or a variable name incorrectly, the compiler will throw an error.
For example, some languages insist you declare a variable before you use it, others are not so fussy. If the language you’re working in requires you to declare variables but you forget to do that, the compiler will throw a different error message. As annoying as these compilation-time errors are, they do catch a lot of problems and force you to address them. But even when you’ve got a program that has no syntactical bugs it doesn’t mean there are no bugs in it. Far from it.
Bugs that are due to logical flaws are usually much harder to spot. If you tell your program to add two and three but you really wanted it to add two and two, you won’t get the answer you expected. But the program is doing what it has been written to do. There’s nothing wrong with the composition or syntax of the program. The problem is you. You’ve written a well-formed program that doesn’t do what you wanted.
Testing Is Difficult
Thoroughly testing a program, even a simple one, is time-consuming. Running it a few times isn’t enough; you really need to test all execution paths in your code, so that all parts of the code are verified. If the program asks for input, you need to provide a sufficient range of input values to test all conditions—including unacceptable input.
For higher-level languages, unit tests and automated testing help to make thorough testing a manageable exercise. So the question is, are there any tools that we can use to help us write bug-free Bash shell scripts?
The answer is yes, including the Bash shell itself.
Using Bash To Check Script Syntax
-n (noexec) option tells Bash to read a script and check it for syntactical errors, without running the script. Depending on what your script is intended to do, this can be a lot safer than running it and looking for problems.
Here’s the script we’re going to check. It isn’t complicated, it’s mainly a set of
if statements. It prompts for, and accepts, a number representing a month. The script decides which season the month belongs to. Obviously, this won’t work if the user provides no input at all, or if they provide invalid input like a letter instead of a digit.
#! /bin/bash read -p "Enter a month (1 to 12): " month # did they enter anything? if [ -z "$month" ] then echo "You must enter a number representing a month." exit 1 fi # is it a valid month? if (( "$month" < 1 || "$month" > 12)); then echo "The month must be a number between 1 and 12." exit 0 fi # is it a Spring month? if (( "$month" >= 3 && "$month" < 6)); then echo "That's a Spring month." exit 0 fi # is it a Summer month? if (( "$month" >= 6 && "$month" < 9)); then echo "That's a Summer month." exit 0 fi # is it an Autumn month? if (( "$month" >= 9 && "$month" < 12)); then echo "That's an Autumn month." exit 0 fi # it must be a Winter month echo "That's a Winter month." exit 0
This section checks whether the user has entered anything at all. It tests whether the
$month variable is unset.
if [ -z "$month" ] then echo "You must enter a number representing a month." exit 1 fi
This section checks whether they have entered a number between 1 and 12. It also traps invalid input that isn’t a digit, because letters and punctuation symbols don’t translate into numerical values.
# is it a valid month? if (( "$month" < 1 || "$month" > 12)); then echo "The month must be a number between 1 and 12." exit 0 fi
All of the other If clauses check whether the value in the
$month variable is between two values. If it is, the month belongs to that season. For example, if the month entered by the user is 6, 7, or 8, it is a Summer month.
# is it a Summer month? if (( "$month" >= 6 && "$month" < 9)); then echo "That's a Summer month." exit 0 fi
If you want to work through our examples, copy and paste the text of the script into an editor and save it as “seasons.sh.” Then make the script executable by using the
chmod +x seasons.sh
We can test the script by
- Providing no input at all.
- Providing a non-numeric input.
- Providing a numerical value that is outside the range of 1 to 12.
- Providing numerical values within the range of 1 to 12.
In all cases, we start the script with the same command. The only difference is the input the user provides when promoted by the script.
That seems to work as expected. Let’s have Bash check the syntax of our script. We do this by invoking the
-n (noexec) option and passing in the name of our script.
bash -n ./seasons.sh
This is a case of “no news is good news.” Silently returning us to the command prompt is Bash’s way of saying everything seems OK. Let’s sabotage our script and introduce an error.
We’ll remove the
then from the first
# is it a valid month? if (( "$month" < 1 || "$month" > 12)); # "then" has been removed echo "The month must be a number between 1 and 12." exit 0 fi
Now let’s run the script, first without and then with input from the user.
The first time the script is run the user doesn’t enter a value and so the script terminates. The section that we’ve sabotaged is never reached. The script ends without an error message from Bash.
The second time the script is run, the user provides an input value, and the first if clause is executed to sanity-check the user’s input. That triggers the error message from Bash.
Note that Bash checks the syntax of that clause—and every other line of code—because it doesn’t care about the logic of the script. The user isn’t prompted to enter a number when Bash checks the script, because the script isn’t running.
The different possible execution paths of the script don’t affect how Bash checks the syntax. Bash simply and methodically works its way from the top of the script to the bottom, checking the syntax for every line.
The ShellCheck Utility
A linter—named for a C source code checking tool from the heyday of Unix—is a code analysis tool used to detect programming errors, stylistic errors, and suspicious or questionable use of the language. Linters are available for many programming languages and are renowned for being pedantic. Not everything a linter finds is a bug per se, but anything they do bring to your notice probably deserves attention.
ShellCheck is a code analysis tool for shell scripts. It behaves like a linter for Bash.
Let’s put our missing
then reserved word back into our script, and try something else. We’ll remove the opening bracket “[” from the very first
# did they enter anything? if -z "$month" ] # opening bracket "[" removed then echo "You must enter a number representing a month." exit 1 fi
if we use Bash to check the script it doesn’t find a problem.
bash -n seasons.sh
But when we try to run the script we see an error message. And, despite the error message, the script continues to execute. This is why some bugs are so dangerous. If the actions taken further on in the script rely on valid input from the user, the script’s behavior will be unpredictable. It could potentially put data at risk.
The reason the Bash
-n (noexec) option doesn’t find the error in the script is the opening bracket “[” is an external program called
[. It isn’t part of Bash. It is a shorthand way of using the
Bash doesn’t check the use of external programs when it is validating a script.
ShellCheck requires installation. To install it on Ubuntu, type:
sudo apt install shellcheck
To install ShellCheck on Fedora, use this command. Note that the package name is in mixed case, but when you issue the command in the terminal window it is all in lowercase.
sudo dnf install ShellCheck
On Manjaro and similar Arch-based distros, we use
sudo pacman -S shellcheck
Let’s try running ShellCheck on our script.
ShellCheck finds the issue and reports it to us, and provides a set of links for further information. If you right-click a link and choose “Open Link” from the context menu that appears, the link will open in your browser.
ShellCheck also finds another issue, which isn’t as serious. It is reported in green text. This indicates it is a warning, not an out-and-out error.
Let’s correct our error and replace the missing “[.” One bug-fix strategy is to correct the highest priority issues first and work down to the lower priority issues like warnings later.
We replaced the missing “[” and ran ShellCheck once more.
The only output from ShellCheck refers to our previous warning, so that’s good. We have no high-priority issues needing fixing.
The warning tells us that using the
read command without the
-r (read as-is) option will cause any backslashes in the input to be treated as escape characters. This is a good example of the type of pedantic output a linter can generate. In our case the user shouldn’t be entering a backslash anyway—we need them to enter a number.
Warnings like this require a judgment call on the part of the programmer. Make the effort to fix it, or leave it as it is? It’s a simple two-second fix. And it’ll stop the warning cluttering up ShellCheck’s output, so we might as well take its advice. We’ll add an “r” to option the flags on the
read command, and save the script.
read -pr "Enter a month (1 to 12): " month
Running ShellCheck once more gives us a clean bill of health.
ShellCheck Is Your Friend
ShellCheck can detect, report, and advise on a whole range of issues. Check out their gallery of bad code, which shows how many types of problems it can detect.
It’s free, fast, and takes a lot of the pain out of writing shell scripts. What’s not to like?