How To Validate the Syntax of a Linux Bash Script Before Running It

How To Validate the Syntax of a Linux Bash Script Before Running It

fatmawati achmad zaenuri/Shutterstock

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

The Bash -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 command:

chmod +x seasons.sh

Setting the executable permission on a script

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.

./seasons.sh

Testing a script with a variety of valid and invalid inputs

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

Using Bash to test the syntax of a script

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 if clause.

# 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.

./seasons.sh

Testing a script with invalid and valid inputs

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 if clause.

# 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
./seasons.sh

An error message from a script that passed the syntax checking with no detected issues

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 test command.

Bash doesn’t check the use of external programs when it is validating a script.

Installing ShellCheck

ShellCheck requires installation. To install it on Ubuntu, type:

sudo apt install shellcheck

Installing shellcheck on Ubuntu

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

Installing shellcheck on Fedora

On Manjaro and similar Arch-based distros, we use pacman:

sudo pacman -S shellcheck

Installing shellcheck on Manjaro

Using ShellCheck

Let’s try running ShellCheck on our script.

shellcheck seasons.sh

Checking a script with ShellCheck

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 reporting errors and warnings

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.

shellcheck seasons.sh

Checking a script a second time with ShellCheck

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.

No errors or warnings reported by ShellCheck

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?


Original Link

Leave a Comment

Your email address will not be published.