Chapter 19
Writing Scripts

While you can enter individual shell commands at the command line, there are times when doing so is not efficient. For example, if you need to enter the same 15 commands to accomplish a certain task, a single shell script can handle the job faster than you can type in those commands. However, shell scripts go beyond just repeating commands. They allow you to modify their behavior through the use of variables and change the course of their actions based on results of conducted tests.

In addition, you can configure shell scripts to run at night unattended, while you're asleep. Even better is that you can write a single set of shell scripts and then deploy them to all the various servers you administer. If written well, these scripts will need little to no modifications for the different systems on which they reside. Due to their effectiveness, flexibility, and efficiency, creating well-written shell scripts is a core function of properly administering a system.

Beginning a Shell Script

When you are first learning, writing shell scripts is often an intimidating task. It's helpful to try some basic scripts in order to get comfortable with the process, before you launch into more complicated and intricate script writing. The basic scripts and methods in this section will help you build a solid base for creating shell scripts.

Creating a Script File

To place shell commands in a text file, first you'll need to use a text editor (covered in Chapter 8, “Working with Text Files”) to create a file. It is considered good form to use the .sh file extension in your script file's name. While it's not required, it is helpful to be able to quickly glance at a directory and see which files are scripts by their filename extension.

When creating a shell script file, you should specify the shell you are using in the first line of the file. The format (also called syntax) for this is as follows:

#!/bin/bash

This first line of a shell script file, which is a pound sign (#) followed by the exclamation point () and an absolute directory reference to a shell (/bin/bash), tells the current shell which shell to use for running the script.

After indicating the shell, commands are entered onto each line of the file. The shell will process commands in the order in which they appear in the file. An example shell script file looks like this:

$ cat sampleScript1.sh
#!/bin/bash
# This sample script dislays the date/time
# and who is logged onto the system
#
date
who
$

Notice that three additional lines were included that start with the pound symbol and add a description of the script. Lines that start with the pound symbol (other than the first #! line) aren't interpreted by the shell. This is a great way to leave comments for yourself about what the script does and what's happening in the script, so when you come back to it two years later, you can easily remember what you did.

There are several different methods for running a shell script. We'll just cover three basic ones here.

  • Precede the script name with the bash command. This method is the easiest and is often used by those who are just learning how to write and run shell scripts. Your current working directory needs to be the same directory where the script is located. You type in bash and follow with the script's name:
    
    $ bash sampleScript1.sh
    
  • Precede the script name with ./. This method requires a little more work. Before attempting to run the script for the first time, you must ensure that the script file has the execution privilege bit set prior to running it. Here is an example of this:
    $ chmod u+x sampleScript1.sh
    $ ./sampleScript1.sh
    
  • The example assumes you are the owner of the file, and thus the chmod u+x was used. Keep in mind that you only need to set the execution privilege one time. Once set, you can just enter ./ and follow it with your script's name, but your current working directory needs to be the same directory where the script is located.
  • Precede the script name with nothing. This method requires that the script is located in a directory within the PATH environment variable (covered in Chapter 6, “Working with the Shell”), or you have modified the variable to include the script's directory. The convenience of this method is that you can be located anywhere on the Linux system to run the script, and nothing other than the script name will launch it:
    $ sampleScript1.sh
    
  • The downside of this method is that your script must reside in a PATH directory. Also, you'll need to give the script a unique name so that its name isn't the same as another script on the system or shell command.

When you run a script, you'll see output from the script's commands. Here's an example of using bash to run a sample script:

$ pwd
/home/sysadmin/Scripts
$
$ ls sampleScript1.sh
sampleScript1.sh
$ 
$ bash sampleScript1.sh
Fri Mar  5 19:19:11 UTC 2021
sysadmin tty1         2021-03-05 18:45
sysadmin pts/0        2021-03-05 18:48 (192.168.0.103)
$

Success! Now all of the pieces are in the right places to execute the new shell script file.

Displaying Messages

Most shell commands produce their own output, which is displayed on the terminal where the script is running. Many times, however, you will want to add your own text messages to help the script user know what is happening within the script. You can do this with the echo command. The echo command can display a simple text string if you add the string following the command:

$ echo This is a test
This is a test
$

Notice that, by default, you don't need to use quotes to delineate the string you're displaying. However, sometimes this can get tricky if you are using quotes within your string:

$ echo Let's see if this'll work
Lets see if thisll work
$

The echo command uses either double or single quotes to delineate text strings. If you use them within your string, you need to use one type of quote within the text and the other type to delineate the string:

$ echo "This is a test to see if you're paying attention"
This is a test to see if you're paying attention
$
$ echo 'Rich says "scripting is easy".'
Rich says "scripting is easy".
$

Now all of the needed quotation marks appear properly in the output.

You can add echo statements anywhere in your shell scripts where you need to display additional information:

$ cat sampleScript2.sh
#!/bin/bash
# This sample script dislays the date/time
# and who is logged onto the system
#
echo "The date and time are: "
date
echo "The following users are logged on: "
who
$

When you run this script, it produces the following output:

$ chmod u+x sampleScript2.sh
$ ./sampleScript2.sh
The date and time are:
Fri Mar  5 19:41:16 UTC 2021
The following users are logged on:
sysadmin tty1         2021-03-05 18:45
sysadmin pts/0        2021-03-05 18:48 (192.168.0.103)
$

That's nice, but what if you want to echo a text string on the same line as a command output? You can use the -n parameter for the echo statement to do that. Just change the first echo statement line to this:

echo -n "The time and date are: "

You'll need to use quotes around the string to ensure that there's a space at the end of the echoed string. The command output begins exactly where the string output stops. The output will now look like this:

$ ./sampleScript2.sh
The date and time are: Fri Mar  5 19:42:50 UTC 2021
The following users are logged on:
sysadmin tty1         2021-03-05 18:45
sysadmin pts/0        2021-03-05 18:48 (192.168.0.103)
$

Perfect! The echo command is a crucial piece of shell scripts that interact with users. You'll find yourself using it in many situations, especially when you want to display the values of script variables. Let's look at that next.

Using Variables

Just running individual commands from the shell script is useful, but this has its limitations. Often you'll want to incorporate other data in your shell commands to process information. You can do this by using variables. Variables allow you to temporarily store information within the shell script for use with other commands in the script. This section shows how to use variables in your shell scripts.

Environment Variables

You've already seen one type of Linux variable in action. (Chapter 6 described the environment variables available in the Linux system.) You can access these values from your shell scripts as well.

The shell maintains environment variables that track specific system information, such as the name of the system, the name of the user logged in to the system, the user's system ID (called UID), the default home directory of the user, and the search path used by the shell to find programs.

You can tap into these environment variables from within your scripts by using the environment variable's name preceded by a dollar sign. This is demonstrated in the following script:

$ cat sampleScript3.sh
#!/bin/bash
# Display a user's information
#
echo "User info for userid: $USER"
echo UID: $UID
echo HOME: $HOME
$

The $USER , $UID , and $HOME environment variables are used to display the pertinent information about the logged-in user. The output should look something like this:

$ chmod u+x sampleScript3.sh
$ ./sampleScript3.sh
User info for userid: sysadmin
UID: 1000
HOME: /home/sysadmin
$

Notice that the environment variables in the echo commands are replaced by their current values when the script is run. Also notice that we were able to place the $USER system variable within the double quotation marks in the first string, and the shell script was still able to figure out what we meant.

User Variables

In addition to the environment variables, a shell script allows you to set and use your own variables within the script. Setting variables allows you to temporarily store data and use it throughout the script.

User variables can be any text string of up to 20 letters, digits, or an underscore character. User variables are case sensitive, so the variable Var1 is different from the variable var1 . This little rule often gets novice script writers in trouble.

Values are assigned to user variables using an equal sign. No spaces can appear between the variable, the equal sign, and the value (another trouble spot for novices). Here are a few examples of assigning values to user variables:

var1=10
var2=-57
var3=testing
var4="still more testing"

The shell script stores all variable values as text strings, so it's up to the individual commands in the shell to properly handle the data type used for the variable's value.

Just like system variables, user variables are referenced using the dollar sign:

$ cat sampleScript4.sh
#!/bin/bash
# Trying out user variables
#
days=10
guest="London Radford"
echo "$guest checked in $days days ago."
#
days=5
guest="Takoda Puddle"
echo "$guest checked in $days days ago."
$

Running the script produces the following output:

$ chmod u+x sampleScript4.sh
$ ./sampleScript4.sh
London Radford checked in 10 days ago.
Takoda Puddle checked in 5 days ago.
$

Each time the variable is referenced, it produces the value currently assigned to it. Variables defined within the shell script maintain their values throughout the life of the shell script but are deleted when the shell script completes.

Command Substitution

One of the most useful features of shell scripts is the ability to extract information from the output of a command and assign it to a variable. Once you assign the output to a variable, you can use that value anywhere in your script. This comes in handy when processing data in your scripts.

There are two ways to assign the output of a command to a variable:

  • The backtick character
  • The $() format

Be careful with the backtick character, which is not the normal single quotation mark character you are used to using for strings. Because it is not used often outside of shell scripts, you may not even know where to find it on your keyboard. On a U.S. keyboard, it is usually on the same key as the tilde symbol (~).

Command substitution allows you to assign the output of a shell command to a variable. While this doesn't seem like much, it is a major building block in script programming.

You must surround the entire command-line command with either the backtick characters:

testDate=`date`

or using the $() format:

testDate="$(date)"

The shell runs the command within the command substitution characters and assigns the output to the variable testDate. Notice that there aren't any spaces between the assignment equal sign and the command substitution character. Also notice that we enclosed the $(date) command in quotation marks. This helps to avoid unusual problems that can occur with a command's output and is considered good form.

Here's an example of creating a variable using the output from a normal shell command:

$ cat sampleScript5.sh
#!/bin/bash
# Trying out command subsitution variables
#
testDate="$(date)"
echo "The date and time are: $testDate"
$

The variable testDate receives the output from the date command, and it is used in the echo statement to display it. Notice that this user variable name, testDate, starts with a lowercase letter and has an uppercase letter in its middle. This naming convention is called camel case and allows easier reading of variable names created out of multiple words by shell script writers. It's often used when naming shell scripts too.

Running the shell script produces the following output:

$ chmod u+x sampleScript5.sh
$ ./sampleScript5.sh
The date and time are: Fri Mar  5 20:23:02 UTC 2021
$

That's not all that exciting in this example (you could just as easily put the command in the echo statement), but once you capture the command output in a variable, you can do anything with it.

Exiting the Script with Status

When a shell script ends, it returns an exit status back to the parent shell that launched it. The exit status tells us if the shell script completed successfully.

Linux provides us with the special $? variable, which holds the exit status value from the last command that executed. To check the exit status of a command, you must view the $? variable immediately after the command ends. It changes values according to the exit status of the last command executed by the shell:

$ ls *.sh
sampleScript1.sh  sampleScript3.sh  sampleScript5.sh
sampleScript2.sh  sampleScript4.sh
$ echo $?
0
$
$ ls *.txt
ls: cannot access '*.txt': No such file or directory
$ echo $?
2
$

By convention, the exit status of a command that successfully completes is 0. If a command completes with an error, a positive integer value appears as the exit status.

You can change the exit status of your shell scripts by using the exit command and using a number as an argument. While you don't have to include an exit status with your script's exit command, it is helpful in certain situations, such as debugging a script problem. To use an exit status, just specify the status value you want in the exit command:

$ cat sampleScript6.sh
#!/bin/bash
# Trying out an exit status
#
testDate="$(date)"
echo "The date and time are: $testDate"
exit 42
$
$ chmod u+x sampleScript6.sh
$ ./sampleScript6.sh
The date and time are: Mon Mar  8 15:45:17 UTC 2021
$ echo $?
42
$

In this example script, an exit command was used to exit the script using an exit status code of 42. After the script was run, we displayed the $? variable value to see if it matched what we had set in the exit command, which it did.

As you write more complicated scripts, you can indicate errors by changing the exit status value. That way, by checking the exit status, you can easily debug your shell scripts. If you don't want to include an exit status in your script, it is still considered good form to use the exit command—just don't add an exit status to it.

Passing Parameters

The most basic method of passing data to your shell script is to use command-line parameters. These parameters allow you to add data values to the command line when you execute the script:

$ ./sampleScript7.sh 3 7
[…]
$

This example passes two command-line parameters (3 and 7) to the script sampleScript7.sh. The script handles the command-line parameters using special variables. This section describes how to use command-line parameters in your Bash shell scripts.

The Bash shell assigns special variables, called positional parameters, to all of the command-line parameters entered. This includes the name of the script the shell is executing. The positional parameter variables are standard numbers, with $0 being the script's name, $1 being the first parameter, $2 being the second parameter, and so on, up to $9 for the ninth parameter.

Here's a simple example of using two command-line parameters in a shell script:

$ cat sampleScript7.sh
#!/bin/bash
# Trying out using command-line parameters
#
total=$[ $1 + $2 ]
echo "When you add $1 to $2,"
echo "the result is: $total"
exit
$
$ chmod u+x sampleScript7.sh
$ ./sampleScript7.sh 3 7
When you add 3 to 7,
the result is: 10
$

You can use the $1 and $2 variables just like any other variables in the shell script. The shell script automatically assigns the value from the command-line parameter to the variable, so you don't need to do anything special.

In the preceding example, the command-line parameters used were both numerical values. You can also use text strings as parameters:

$ cat sampleScript8.sh
#!/bin/bash
# Trying out using a string parameter
#
echo Hello $1, glad to meet you.
exit
$
$ chmod u+x sampleScript8.sh
$ ./sampleScript8.sh Kathi
Hello Kathi, glad to meet you.
$

The shell passes the string value entered into the command line to the script. However, you'll have a problem if you try to do this with a text string that contains spaces:

$ ./sampleScript8.sh Kathi Duggan
Hello Kathi, glad to meet you.
$

Remember that each of the parameters is separated by a space, so the shell interpreted the space as just separating the two values. To include a space as a parameter value, you must use quotation marks (either single or double quotation marks), which are not included as part of the data, but just delineate the beginning and the end of the data:

$ ./sampleScript8.sh 'Kathi Duggan'
Hello Kathi Duggan, glad to meet you.
$
$ ./sampleScript8.sh "Kathi Duggan"
Hello Kathi Duggan, glad to meet you.
$

If your script needs more than nine command-line parameters, you can continue, but the variable names change slightly. After the ninth variable, you must use braces around the variable number, such as ${10} . Here's an example of doing that:

$ cat sampleScript9.sh
#!/bin/bash
# Trying out using 12 parameters
#
totalA=$[ $1 + $2 + $3 + $4 ]
totalB=$[ $5 + $6 + $7 + $8 +$9 ]
totalC=$[ ${10} + ${11} + ${12} ]
#
total=$[ $totalA + $totalB + $totalC ]
#
echo
echo "When you add all these numbers together,"
echo "the result is: $total"
echo
echo "When you add ${10}, ${11} and ${12} together,"
echo "the result is: $totalC"
#
exit
$
$ chmod u+x sampleScript9.sh
$ ./sampleScript9.sh 1 2 3 4 5 6 7 8 9 10 11 12
 
When you add all these numbers together,
the result is: 78
 
When you add 10, 11 and 12 together,
the result is: 33
$

This technique allows you to add as many command-line parameters to your scripts as you could possibly need.

Adding Conditional Expressions

So far, all the shell scripts presented process commands in a linear fashion—one command after another. However, not all programming is linear. There are times when you'd like your program to test for certain conditions, such as if a file exists or if a mathematical expression is 0, and perform different commands based on the results of the test. For that, the Bash shell provides logic statements.

Logic statements allow us to test for a specific condition or conditions and then branch to different sections of code based on whether the condition evaluates to a zero (true) or nonzero (false) exit value. There are a couple of different ways to implement logic statements in Bash scripts.

Working with the if-then Statement

The most basic logic statement is the if-then condition statement. The format for the if-then condition statement is as follows:

if [ condition ]
then
    commands
fi

If the condition you specify evaluates to a zero exit value (true), the shell runs the commands in the then section of code. If the condition evaluates to a nonzero exit value (false), the shell script skips the commands in the then section of code.

The if-then condition syntax is typically tricky for those who are new to shell script writing. It is critical to ensure a space is included after the first bracket and before the condition, as well as after the condition and before the last bracket: [ condition ].

The condition expression has quite a few different formats in the Bash shell programming. There are built-in tests for numerical values, string values, and even files and directories. Table 19.1 lists the different built-in tests that are available.

TABLE 19.1 Condition Tests

TEST TYPE DESCRIPTION
n1 -eq n2 Numeric Checks if n1 is equal to n2
n1 -ge n2 Numeric Checks if n1 is greater than or equal to n2
n1 -gt n2 Numeric Checks if n1 is greater than n2
n1 -le n2 Numeric Checks if n1 is less than or equal to n2
n1 -lt n2 Numeric Checks if n1 is less than n2
n1 -ne n2 Numeric Checks if n1 is not equal to n2
str1 = str2 String Checks if str1 is the same as str2
str1 = str2 String Checks if str1 is not the same as str2
str1 < str2 String Checks if str1 is less than str2
str1 > str2 String Checks if str1 is greater than str2
-n str1 String Checks if str1 has a length greater than zero
-z str1 String Checks if str1 has a length of zero
-d file File Checks if file exists and is a directory
-e file File Checks if file exists
-f file File Checks if file exists and is a file
-r file File Checks if file exists and is readable
-s file File Checks if file exists and is not empty
-w file File Checks if file exists and is writable
-x file File Checks if file exists and is executable
-O file File Checks if file exists and is owned by the current user
-G file File Checks if file exists and the default group is the same as the current user
file1 -nt file2 File Checks if file1 is newer than file2
file1 -ot file2 File Checks if file1 is older than file2

Here is an example of using several if-then condition statements in a shell script:

$ cat sampleScript10.sh
#!/bin/bash
# Trying out if-then conditions
#
if [ -z $1 ]
then
    echo "You did not provide any parameters"
    exit
fi
#
if [ -z $2 ]
then
    echo "You did not provide a second parameter"
    exit
fi
#
if [ $1 -eq $2 ]
then
    echo "Both values are equal"
    exit
fi
#
if [ $1 -gt $2 ]
then
    echo "$1 is greater than $2"
    exit
fi
#
if [ $1 -lt $2 ]
then
   echo "$1 is less than $2"
fi
#
exit
$ 

We'll try not providing enough parameters first and see how the if-then statement tests string length using the -z condition test. This is an excellent condition test to include, if command-line parameters are used in a shell script.

$ chmod u+x sampleScript10.sh
$ ./sampleScript10.sh
You did not provide any parameters
$
$ ./sampleScript10.sh 73
You did not provide a second parameter
$

Now we can try providing enough parameters and have the script determine information concerning them:

$ ./sampleScript10.sh 73 42
73 is greater than 42
$
$ ./sampleScript10.sh 42 73
42 is less than 73
$
$ ./sampleScript10.sh 42 42
Both values are equal
$

Whenever the command from an if-then statement in the script evaluated to a zero exit value, the statements between then and fi were processed by the shell script.

Using Compound Tests

You can reduce the number of if-then statements through the use of Boolean logic to combine tests. These operators are available:

  • [ conditionA ] && [ conditionB ]
  • [ conditionA ] || [ conditionB ]

The first operator is an “and” test, where both conditions must return a zero exit status (true). The second operator performs an “or” test. In this case, only one condition must return a zero exit status.

Modifying our if-then sample script in this manner allows us to consolidate the first two tests, as shown snipped here:

$ cat sampleScript11.sh
#!/bin/bash
# Trying out if-then conditions
# and compound tests
#
if [ -z $1 ] || [ -z $2 ]
then
    echo "You didn't provide the correct number of parameters."
    exit
fi
#
if [ $1 -eq $2 ]
[…]
exit
$

The script still works as needed, but with a less code:

$ chmod u+x sampleScript11.sh
$
$ ./sampleScript11.sh
You didn't provide the correct number of parameters.
$
$ ./sampleScript11.sh 42
You didn't provide the correct number of parameters.
$
$ ./sampleScript11.sh 42 73
42 is less than 73
$

Working with the if-then-else Statement

In the if-then statement, you have only one option for whether a command is successful. If the command returns a nonzero exit value (false), the Bash shell just moves on to the next command in the script. In this situation, it would be nice to be able to execute an alternate set of commands. That's exactly what the if-then-else statement is for.

The if-then-else statement provides another group of commands in the statement:

if [ condition ]
then
   commands
else
   commands
fi

When the command in the if statement line returns with a zero exit value, the commands listed in the then section are executed, just as in a normal if-then statement. When the command in the if statement line returns a nonzero exit value, the Bash shell executes the commands in the else section.

Modifying our if-then sample script to include an if-then-else statement allows us to provide more information concerning the parameters, as shown snipped here:

$ cat sampleScript12.sh
#!/bin/bash
# Trying out if-then-else statments
[…]
#
if [ $1 -eq $2 ]
then
    echo "Both values are equal"
    exit
else
    echo "The values are not equal"
fi
#
if [ $1 -gt $2 ]
then
    echo "$1 is greater than $2"
    exit
fi
#
if [ $1 -lt $2 ]
then
   echo "$1 is less than $2"
fi
#
exit
$
$ chmod u+x sampleScript12.sh
$
$ ./sampleScript12.sh 42 42
Both values are equal
$
$ ./sampleScript12.sh 42 73
The values are not equal
42 is less than 73
$

The script still works as needed but allows additional information to be provided through the else section of the if-then-else statement. However, there are additional methods to provide more information and consolidate code. We'll explore one technique next.

Trying the case Statement

Often, you'll find yourself trying to evaluate the value of a variable, looking for a specific value within a set of possible values, similar to what we demonstrated in earlier scripts. Instead of having to write multiple if-then statements testing for all of the possible conditions, you can use a case statement.

The case statement allows you to check multiple values of a single variable in a list-oriented format:

case variable in
pattern1) commands1;;
pattern2 | pattern3) commands2;;
*) default commands;;
esac

The case statement compares the variable specified against the different patterns. If the variable matches the pattern, the shell executes the command(s) specified for the pattern. You can list more than one pattern on a line, using the bar operator to separate each pattern as an “or” operator. The asterisk (*) symbol is the catchall for values that don't match any of the listed patterns.

Here is an example of using the case statement within a script:

$ cat sampleScript13.sh
#!/bin/bash
# Trying out case statements
#
case $PWD in
/home/sysadmin)
      echo "You are in your home directory"
;;
/home/sysadmin/Scripts)
      echo "You are in your Scripts directory"
;;
*)
      echo "You are not in your home directory, and"
      echo "you are not in your Scripts directory"
;;
esac
#
exit
$

In this script, the variable being matched against the patterns is an environment variable ($PWD) that contains the user's current working directory. Notice that you do not have to put the ;; syntax on the same line as your last command within a selected pattern. This flexibility of the case statement allows you to create easy-to-read shell code.

Here's this script in action:

$ echo $PWD
/home/sysadmin/Scripts
$
$ chmod u+x sampleScript13.sh
$
$ ./sampleScript13.sh
You are in your Scripts directory
$
$ cd
$ echo $PWD
/home/sysadmin
$
$ ./Scripts/sampleScript13.sh
You are in your home directory
$

The case statement provides a much cleaner way of specifying the various options for each possible variable value.

Using Loops

Iterating through a series of commands is a common programming practice. Often, you need to repeat a set of commands until a specific condition has been met, such as processing all the files in a directory, all the users on a system, or all the lines in a text file.

The Bash shell provides three structures for controlling loops: for, while, and until. They each have value for iterating through commands, and we cover them next.

Looking at the for Command

The Bash shell provides the for command to allow you to create a loop that iterates through a series of values. Each iteration performs a predefined set of commands using one of the values in the series. Here's the basic format of the Bash shell for command:

for var in list
do
    commands
done

You supply the series of values used in the iterations in the list parameter. The values can be specified in several ways.

In each iteration, the variable var contains the current value in the list. The first iteration uses the first item in the list, the second iteration uses the second item, and so on, until all the items in the list have been used.

The commands entered between the do and done statements can be one or more standard Bash shell commands. Within the commands, the $var variable contains the current list item value for the iteration.

The most basic use of the for command is to iterate through a list of values defined within the for command itself:

$ cat sampleScript14.sh
#!/bin/bash
# Trying out the for command
#
for wordVar in Alpha Bravo Charlie Delta
do
    echo The current word is $wordVar
done
#
exit
$
$ chmod u+x sampleScript14.sh
$
$ ./sampleScript14.sh
The current word is Alpha
The current word is Bravo
The current word is Charlie
The current word is Delta
$

Each time the for command iterates through the list of values provided, it assigns the $wordVar variable the next value in the list. The $wordVar variable can be used just like any other script variable within the for command statements. After the last iteration, $wordVar remains valid throughout the remainder of the shell script. It retains the last iteration value (unless you change its value).

For large series of numbers in your for loops, it's convenient to use the seq command, instead of typing in all the numbers. You just enter seq followed by the starting number and then the last number in the series, but you'll need to employ command substitution to make it work correctly:

$ cat sampleScript15.sh
#!/bin/bash
# Trying out the for with seq
#
for numVar in $(seq 1 10)
do
    echo The current number is $numVar
done
#
exit
$
$ chmod u+x sampleScript15.sh
$
$ ./sampleScript15.sh
The current number is 1
The current number is 2
The current number is 3
The current number is 4
The current number is 5
The current number is 6
The current number is 7
The current number is 8
The current number is 9
The current number is 10
$

Using the $(seq 1 10) syntax within the for commands list allows the Bash shell to handle generating all the numbers in the list automatically. Notice that in this case of command substitution, we did not enclose the $(seq 1 10) command in quotation marks. This is because the list would be treated as a single item, which is not what we want.

Command substitution in the list lets you get rather fancy with your for loops:

$ cat sampleScript16.sh
#!/bin/bash
# Getting fancy with the for loop
#
for fileVar in $(ls *.sh)
do
    echo The current file is $fileVar
done
#
exit
$
$ chmod u+x sampleScript16.sh
$
$ ./sampleScript16.sh
The current file is sampleScript1.sh
The current file is sampleScript10.sh
The current file is sampleScript11.sh
The current file is sampleScript12.sh
The current file is sampleScript13.sh
The current file is sampleScript14.sh
The current file is sampleScript15.sh
The current file is sampleScript16.sh
The current file is sampleScript2.sh
The current file is sampleScript3.sh
The current file is sampleScript4.sh
The current file is sampleScript5.sh
The current file is sampleScript6.sh
The current file is sampleScript7.sh
The current file is sampleScript8.sh
The current file is sampleScript9.sh
$

In this example, all the script files in the current directory are listed using the $(ls *.sh) command. This list of files is then processed through the command that's between do and done. You can use multiple commands here too, if needed.

Working with the while Format

The while command is somewhat of a cross between the if-then statement and the for loop. The while command allows you to define a command to test and then loop through a set of commands for as long as the defined test command returns a zero exit value (true). It tests the test command at the start of each iteration. When the test command returns a zero exit value, the while command stops executing the set of commands.

Here's the format of the while command:

 while test command
 do
  other commands
 done

The test command defined in the while command is the same format as in if-then statements. As in the if-then statement, you can use any normal Bash shell command, or you can use the test command to test for conditions, such as variable values.

The key to the while command is that the exit status of the test command specified must change, based on the commands run during the loop. If the exit status never changes, the while loop will get stuck in an infinite loop.

The most common use of the test command is to use brackets to check a value of a shell variable that's used in the loop commands:

$ cat sampleScript17.sh
#!/bin/bash
# Trying out the while loop
#
numVar=10
#
while [ $numVar -gt 0 ]
do
    echo The current number is $numVar
    numVar=$[ $numVar - 1 ]
done
#
exit
$
$ chmod u+x sampleScript17.sh
$
$ ./sampleScript17.sh
The current number is 10
The current number is 9
The current number is 8
The current number is 7
The current number is 6
The current number is 5
The current number is 4
The current number is 3
The current number is 2
The current number is 1
$

The while command defines the test condition to check for each iteration:

while [ $numVar -gt 0 ]

As long as the test condition is true, the while command continues to loop through the commands defined. Within the commands, the variable used in the test condition must be modified, or you'll have an infinite loop. In this example, we use shell arithmetic to decrease the variable value by one:

numVar=$[ $numVar - 1 ]

The while loop stops when the test condition is no longer true.

Using the until Command

The until command works in exactly the opposite way from the while command. The until command requires that you specify a test command that normally produces a nonzero exit value (false). As long as the exit status of the test command is a nonzero exit value, the Bash shell executes the commands listed in the loop. When the test command returns a zero exit value (true), the loop stops.

As you would expect, the format of the until command is as follows:

until test command
do
   other commands
done

Only the exit status of the last command determines if the Bash shell executes the other commands defined. The following is an example of using the until command:

$ cat sampleScript18.sh
#!/bin/bash
# Trying out the until loop
#
numVar=10
#
until [ $numVar -eq 0 ]
do
    echo The current number is $numVar
    numVar=$[ $numVar - 1 ]
done
#
exit
$
$ chmod u+x sampleScript18.sh
$
$ ./sampleScript18.sh
The current number is 10
The current number is 9
The current number is 8
The current number is 7
The current number is 6
The current number is 5
The current number is 4
The current number is 3
The current number is 2
The current number is 1
$

This example tests the numVar variable to determine when the until loop should stop. As soon as the value of the variable is equal to zero, the until command stops the loop.

Using Functions

Often while writing shell scripts, you'll find yourself using the same code in multiple locations. If it's just a small code snippet, it's usually not that big of a deal. However, rewriting large chunks of code multiple times in your shell script can get tiring and is not good form.

The Bash shell provides a way to help you out by supporting user-defined functions. Functions are blocks of script code that you assign a name to and reuse anywhere in your code. Anytime you need to use that block of code in your script, you simply use the function name you assigned it (referred to as calling the function).

Creating Functions

There are two formats you can use to create functions in Bash shell scripts. The first format uses the keyword function, along with the function name you assign to the block of code:

function name {
    commands
}

The name attribute defines a unique name assigned to the function. Each function you define in your script must be assigned a unique name.

The commands are one or more Bash shell commands that make up your function. When you call the function, the Bash shell executes each of the commands in the order they appear in the function, just as in a normal script.

The second format for defining a function in a Bash shell script more closely follows how functions are defined in other programming languages:

name() {
commands
}

The empty parentheses after the function name indicate that you're defining a function. The same naming rules apply in this format as in the original shell script function format.

Calling Functions

To use a function in your script, specify the function name on a line, just as you would any other shell command:

$ cat sampleScript19.sh
#!/bin/bash
# Trying out functions
#
function func1 {
     echo "This is a function example"
}
numVar=5
#
until [ $numVar -eq 0 ]
do
    func1
    echo The current number is $numVar
    numVar=$[ $numVar - 1 ]
done
#
exit
$
$ chmod u+x  sampleScript19.sh
$
$ ./sampleScript19.sh
This is a function example
The current number is 5
This is a function example
The current number is 4
This is a function example
The current number is 3
This is a function example
The current number is 2
This is a function example
The current number is 1
$

Each time you reference the func1 function name, the Bash shell returns to the function func1 definition and executes any commands you defined there.

The function definition doesn't have to be the first thing in your shell script, but be careful. If you attempt to use a function before it's defined, you'll get an error message:

$ cat sampleScript20.sh
#!/bin/bash
# Trying out two functions
#
echo "This line is before func1 is defined"
function func1 {
     echo "This is a function example"
}
numVar=5
#
until [ $numVar -eq 0 ]
do
    func1
    echo The current number is $numVar
    numVar=$[ $numVar - 1 ]
done
echo "The loop is done"
func2
#
echo "This line is before func2 is defined"
#
function func2 {
     echo "This is the second function"
}
#
exit
$ 
$ chmod u+x sampleScript20.sh
$
$ ./sampleScript20.sh
This line is before func1 is defined
This is a function example
The current number is 5
This is a function example
The current number is 4
This is a function example
The current number is 3
This is a function example
The current number is 2
This is a function example
The current number is 1
The loop is done
./sampleScript20.sh: line 17: func2: command not found
This line is before func2 is defined
$

The first function, func1, was defined after the first statement in the script, which is perfectly fine. When the func1 function was used in the script, the shell knew where to find it.

However, the script attempted to use the func2 function before it was defined. Because the func2 function wasn't defined, when the script reached the place where we used it, it produced an error message.

You also need to be careful about your function names. Remember, each function name must be unique, or you'll have a problem. If you redefine a function, the new definition overrides the original function definition, without producing any error messages:

$ cat sampleScript21.sh
#!/bin/bash
# Trying out redefining a function
#
function myFunc1 {
     echo "This is the first function definition"
}
myFunc1
#
function myFunc1 {
     echo "This overrides the first function definition"
}
#
numVar=3
#
until [ $numVar -eq 0 ]
do
    myFunc1
    echo The current number is $numVar
    numVar=$[ $numVar - 1 ]
done
#
exit
$ 
$ chmod u+x sampleScript21.sh
$
$ ./sampleScript21.sh
This is the first function definition
This overrides the first function definition
The current number is 3
This overrides the first function definition
The current number is 2
This overrides the first function definition
The current number is 1
$

The original definition of the myFunc1 function works fine, but after the second definition of the myFunc1 function, any subsequent uses of the function use the second definition.

Ideally you are now inspired to pursue more shell script writing to make your system admin work a little easier. If you'd like to learn more, one of our favorite resources is the Linux Command Line and Shell Scripting Bible by Richard Blum and Christine Bresnahan (Wiley, 2021).

The Bottom Line

  • Create basic scripts to automatically run commands. Basic Bash shell scripts require a few, but important, items. For example, on the first line of a shell script file, you use special syntax that indicates to the current shell which shell to use for running the script. You also need to include the commands you want to run within the script. In addition, it's helpful for the script users, even if it is only you, to have the script produce messages so that the script user knows what is happening within the script.
    • Master It You have written a basic Bash shell script that assists in viewing who is logged onto the system currently as well as looking at log files that indicate who logged in previously to the system. For some reason, the script is not functioning properly. You are getting error messages as if the Dash shell is running the script, but you want the Bash shell to run the script and are even using the bash command to run it. What is most likely the problem?
  • Use variables in shell scripts. You can integrate data into your shell script commands to process information using variables. Variables allow you to temporarily store information within the shell script for use with other commands in the script. Variables can be environment variables, user-defined variables, or even data passed to the script through the use of parameters.
    • Master It Imagine you are writing a shell script to assist in the deletion of individual user accounts. This script will be used by an admin intern from the local college, so it needs to include the username of the account to delete in a variable. How will you accomplish this?
  • Provide compound conditions to guide scripts. Compound conditions are ones in which two or more conditions need checking so that the script can decide which commands to execute or the course of action to take. You can handle compound conditions through multiple if-then statements or, if there are only two conditions to check, through the use of Boolean logic to combine the tests.
    • Master It You are writing a script that needs to check a compound condition. In this case, the machine0087.log file's existence needs to be checked. In addition, a counter variable, logCount, must be examined as well. If the file exists and the counter has reached 100, the script should exit. What if-then statement(s) should you write to make this test as consolidated as possible?
  • Determine the best loops to use in a script, when needed. There are times in a shell script that you need to repeat a set of commands until a specific condition has been met. This need occurs when processing all the files in a directory, all the users on a system, or all the lines in a text file. The loop types available for Bash shell scripts include for, while, and until.
    • Master It You are writing a shell script that needs to process lines in a text file. One particular line toward the bottom of the file contains only the text stop here. You are reading in the text file lines one at a time, keeping only the line's text, and putting it into the variable fileLine. What type of loop should you use in your script, and what is the syntax of that loop's first line?
  • Create and use functions in shell scripts. User-defined functions are blocks of script code that you assign a name to and reuse anywhere in your shell script. When you need your script to execute that block of code, you call the function using the name you assigned to it. This provides scripts with the ability to keep blocks of code within a single location, so any changes that the code needs are done in only one place in the script.
    • Master It As you are creating a shell script, you recognize that you are writing these four lines of code over and over again:
              echo "Error in processing file, $1."
              echo "Check that the $1 file exists and contains data."
              echo "This script will now exit…"
              exit
      
      Properly write a function named errorMessage that will allow you to turn these four lines of code into a single line (except for the function declaration) throughout the script.