5 ways to improve your Bash scripts

Find out how Bash can help you tackle the most challenging tasks.
168 readers like this
168 readers like this
A graduate degree could springboard you into an open source job

Opensource.com

A system admin often writes Bash scripts, some short and some quite lengthy, to accomplish various tasks.

Have you ever looked at an installation script provided by a software vendor? They often add a lot of functions and logic in order to ensure that the installation works properly and doesn’t result in damage to the customer’s system. Over the years, I’ve amassed a collection of various techniques for enhancing my Bash scripts, and I’d like to share some of them in hopes they can help others. Here is a collection of small scripts created to illustrate these simple examples.

Starting out

When I was starting out, my Bash scripts were nothing more than a series of commands, usually meant to save time with standard shell operations like deploying web content. One such task was extracting static content into the home directory of an Apache web server. My script went something like this:

cp january_schedule.tar.gz /usr/apache/home/calendar/
cd /usr/apache/home/calendar/
tar zvxf january_schedule.tar.gz

While this saved me some time and typing, it certainly was not a very interesting or useful script in the long term. Over time, I learned other ways to use Bash scripts to accomplish more challenging tasks, such as creating software packages, installing software, or backing up a file server.

1. The conditional statement

Just as with so many other programming languages, the conditional has been a powerful and common feature. A conditional is what enables logic to be performed by a computer program. Most of my examples are based on conditional logic.

The basic conditional uses an "if" statement. This allows us to test for some condition that we can then use to manipulate how a script performs. For instance, we can check for the existence of a Java bin directory, which would indicate that Java is installed. If found, the executable path can be updated with the location to enable calls by Java applications.

if [ -d "$JAVA_HOME/bin" ] ; then
    PATH="$JAVA_HOME/bin:$PATH"

2. Limit execution

You might want to limit a script to only be run by a specific user. Although Linux has standard permissions for users and groups, as well as SELinux for enabling this type of protection, you could choose to place logic within a script. Perhaps you want to be sure that only the owner of a particular web application can run its startup script. You could even use code to limit a script to the root user. Linux has a couple of environment variables that we can test in this logic. One is $USER, which provides the username. Another is $UID, which provides the user’s identification number (UID) and, in the case of a script, the UID of the executing user.

User

The first example shows how I could limit a script to the user jboss1 in a multi-hosting environment with several application server instances. The conditional "if" statement essentially asks, "Is the executing user not jboss1?" When the condition is found to be true, the first echo statement is called, followed by the exit 1, which terminates the script.

if [ "$USER" != 'jboss1' ]; then
     echo "Sorry, this script must be run as JBOSS1!"
     exit 1
fi
echo "continue script"

Root

This next example script ensures that only the root user can execute it. Because the UID for root is 0, we can use the -gt option in the conditional if statement to prohibit all UIDs greater than zero.

if [ "$UID" -gt 0 ]; then
     echo "Sorry, this script must be run as ROOT!"
     exit 1
fi
echo "continue script"

3. Use arguments

Just like any executable program, Bash scripts can take arguments as input. Below are a few examples. But first, you should understand that good programming means that we don’t just write applications that do what we want; we must write applications that can’t do what we don’t want. I like to ensure that a script doesn’t do anything destructive in the case where there is no argument. Therefore, this is the first check that y. The condition checks the number of arguments, $#, for a value of zero and terminates the script if true.

if [ $# -eq 0 ]; then
    echo "No arguments provided"
    exit 1
fi
echo "arguments found: $#"

Multiple arguments

You can pass more than one argument to a script. The internal variables that the script uses to reference each argument are simply incremented, such as $1, $2, $3, and so on. I’ll just expand my example above with the following line to echo the first three arguments. Obviously, additional logic will be needed for proper argument handling based on the total number. This example is simple for the sake of demonstration.

echo $1 $2 $3

While we’re discussing these argument variables, you might have wondered, "Did he skip zero?"

Well, yes, I did, but I have a great reason! There is indeed a $0 variable, and it is very useful. Its value is simply the name of the script being executed.

echo $0

An important reason to reference the name of the script during execution is to generate a log file that includes the script’s name in its own name. The simplest form might just be an echo statement.

echo test >> $0.log

However, you will probably want to add a bit more code to ensure that the log is written to a location with the name and information that you find helpful to your use case.

4. User input

Another useful feature to use in a script is its ability to accept input during execution. The simplest is to offer the user some input.

echo "enter a word please:"
 read word
 echo $word

This also allows you to provide choices to the user.

read -p "Install Software ?? [Y/n]: " answ
 if [ "$answ" == 'n' ]; then
   exit 1
 fi
   echo "Installation starting..."

5. Exit on failure

Some years ago, I wrote a script for installing the latest version of the Java Development Kit (JDK) on my computer. The script extracts the JDK archive to a specific directory, updates a symbolic link, and uses the alternatives utility to make the system aware of the new version. If the extraction of the JDK archive failed, continuing could break Java system-wide. So, I wanted the script to abort in such a situation. I don’t want the script to make the next set of system changes unless the archive was successfully extracted. The following is an excerpt from that script:

tar kxzmf jdk-8u221-linux-x64.tar.gz -C /jdk --checkpoint=.500; ec=$?
if [ $ec -ne 0 ]; then
     echo "Installation failed - exiting."
     exit 1
fi

A quick way for you to demonstrate the usage of the $? variable is with this short one-liner:

ls T; ec=$?; echo $ec

First, run touch T followed by this command. The value of ec will be 0. Then, delete T, rm T, and repeat the command. The value of ec will now be 2 because ls reports an error condition since T was not found.

You can take advantage of this error reporting to include logic, as I have above, to control the behavior of your scripts.

Takeaway

We might assume that we need to employ languages, such as Python, C, or Java, for higher functionality, but that’s not necessarily true. The Bash scripting language is very powerful. There is a lot to learn to maximize its usefulness. I hope these few examples will shed some light on the potential of coding with Bash.

What to read next

Creating a Bash script template

In the second article in this series, create a fairly simple template that you can use as a starting point for other Bash programs, then test it.

Alan Formy-Duval Opensource.com Correspondent
Alan has 20 years of IT experience, mostly in the Government and Financial sectors. He started as a Value Added Reseller before moving into Systems Engineering. Alan's background is in high-availability clustered apps. He wrote the 'Users and Groups' and 'Apache and the Web Stack' chapters in the Oracle Press/McGraw Hill 'Oracle Solaris 11 System Administration' book.

8 Comments

Great article!

The 'if' statement can come in really handy in all Bash scripts. And for very short 'if' statements, the && and || shortcuts are also useful. For example, I often do this when I need to be sure a directory exists:

[ -d "$1" ] || mkdir --parents "$1"

That statement create the directory only if it doesn't exist. You could also write the statement like this:

[ ! -d "$1" ] && mkdir --parents "$1"

The && means "if the test was true, then do the next statement." And the || means "if the test wasn't true, then do the next statement." You can think of them as "AND" and "OR" at the execution level.

i much prefer the updated test command: [[. It has many improvements over [.

Also, I think it would be useful to update your comparison technique to a newer bash idiom. No need to use -eq anymore. This just works: if [[ 1 == 1 ]]; then echo "Yes, 1 equals 1."; fi

Checking for a user using env variables is kind of dangerous, IMHO. I'd rather use: if [[ $( id -nru ) != 'renich' ]]; then exit 1; fi

Just my opinion.

Since this article's title seems to indicate that one would be interested in any and all ways to make a script better, I would suggest that adding lots of comments in the scripts that can be referred to in the future improves their quality and value overall by leaps and bounds. I often will look through old scripts to see if I've already written something that I can re-use, and it isn't always obvious that I have an existing bit of code unless I've documented the script.

To that end, I would suggest not just writing out details about how to script works, what it's intended to do, or possibly how to modify variables in the future, but maybe even just add some general keywords. This would help when doing a general "grep" type search in your scripts directory.

Using $USER or $UID to restrict execution of a script is extremely insecure. Both are ordinary environment variables. They should be initialized correctly when your shell starts, but the user can change them to anything. I can run

USER=root UID=0 your_script

and your script will just assume I'm root.

If you want to know who the user is, rather than who the user claims to be, use "/usr/bin/id -u" to get the numeric user id or "/usr/bin/id -un" to get the user name. Use "/usr/bin/id", not just "id"; the latter lets the user substitute their own "id" command by adjusting $PATH.

To ensure that a script can only be executed by root, use chown and chmod to ensure that no user other than the actual root user can execute it:

chown root:root your_script
chmod 500 your_script

Most of your variable references should be enclosed in double quotes to avoid misbehavior if the variables' values contain any funny characters.

In section 5, saving the value of $? in $ec is unnecessary. Just use $? directly. The value of $? is rather fragile, so for example this:

command_that_might_fail
echo "The status was $?"
if [ $? -ne 0 ] ; then
exit 42
fi

will test the status of the echo command. Just be aware of that issue, and you can use $? directly, which will make your code easier to read.

For that matter, it's even simpler not to refer to $? explicitly. The condition in an "if" or "while" statement is always a command, and the condition is whether the command succeeded (set $? to 0) or failed (set $? to anything other than 0). (Yes "[" is a command, usually built into the shell.) Thus your tar command could be:

if ! tar ... ; then
echo "Installation failed" 1>&2
exit 1
fi

(Note that I printed the error message to file descriptor 1, the standard error stream.) Or you can use the || or && operator:

tar ... || {
echo "Installation failed" 1>&2
exit 1
}

Keith, I have always operated in secure environments where the "user" is an application or system account such as oracle, postgres, tomcat, weblogic and so on. Only trusted administrative personnel are permitted to access the account. However, with that said, your warning warrants attention as anyone who does access the account could change the variables. Thanks for pointing it out.

In reply to by Keith Thompson (not verified)

Users learning Bourne-family shell scripting need to understand the history.

The POSIX shell has it's own standard, and is a much smaller language than provided by bash. This shell is relevant when using Debian dash, and somewhat less so with the Busybox shell. Standards that are prior to POSIX and ksh88 should not be studied in detail. Be sure to consult the correct references - HP-UX actually presents ksh88 for the "man sh-posix" command, which is completely wrong. The POSIX shell does not implement arrays and the [[ conditional structure.

Prior to POSIX was the '88 release of the Korn shell, which introduced [[ - many are surprised by the /bin/[ program and its relation to /bin/test. Korn also deprecates the -gt operator, as part of introducing floating point in the '93 release.

Bash took many Korn innovations, but also went its own way on several subjects (principally coprocesses).

I find that the POSIX standard, and Korn features in mksh (Android's default shell) are the best featureset to use, unless something specific is needed in one of the other shells. The mksh is much smaller than bash, and presents fewer problems when porting from commercial UNIX.

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.