In this chapter, we ll take a look at the bash scripting language. The following sections will identify the necessary language constructs for application development, including variables, operations on variables , conditionals, looping, and functions. We ll also demonstrate a number of sample applications to illustrate scripting principles.
Any worthwhile language permits the creation of variables. In bash, variables are untyped, which means that all variables are in essence strings. This doesn t mean we can t do arithmetic on bash variables, which we ll explore shortly. We can create a variable and then inspect it very easily as:
$ x=12 $ echo $x 12 $
In this example, we create a variable x and bind the value 12 to it. We then echo the variable x and find our value. Note that the lack of space between the variable name , the equals, and the value is relevant. There can be no spaces, otherwise an error will occur. Note also that to reference a variable, we precede it with the dollar sign. This variable is scoped (exists) for the life of the shell from which this sequence was performed. Had this sequence of commands been performed within a script (such as ./test.sh , the variable x would not exist once the script was completed.
As bash doesn t type its variables, we can create a string variable similarly:
$ b="my string" $ echo $b my string $
Note that single quotes also would have worked here. An interesting exception is the use of the backtick . Consider the following:
$ c='echo $b' $ echo $c my string $
The backticks have the effect of evaluating the contents within the backticks, and in this case the result is assigned to the variable c . When we emit variable c , we find the value of the original variable b .
The bash interpreter defines a set of environment variables that effectively define the environment. These variables exist when the bash shell is started (though others can be created using the export command). Consider the script in Listing 20.2, which makes use of special environment variables to identify the environment of the script.
1 : #!/bin/bash 2 : 3 : echo "Welcome to host $HOSTNAME running $OSTYPE." 4 : echo "You are user $USER and your home directory is $HOME." 5 : echo "The version of bash running on this system is $BASH_VERSION." 6 : sleep 1 7 : echo "This script has been running for $SECONDS second(s)." 8 : exit
Upon executing this script, we see the following on a sample GNU/Linux system:
$ ./env.sh Welcome to host camus running linux-gnu. You are user mtj and your home directory is /home/mtj. The version of bash running on this system is 2.05b.0(1)-release. This script has been running for 1 second(s). $
Note that we added a useless sleep call to the script (to stall it for one second) so that we could see that the SECONDS variable was working. Also note that the SECONDS variable can be emitted from the command line, but this value represents the number of seconds that the shell has been running.
Bash provides a variety of other special variables. Some of the more important ones are shown in Table 20.1. These can be echoed to understand their formats.
Variable | Description |
---|---|
$ PATH | Default path to binaries |
$PWD | Current working directory |
$OLDPWD | Last working directory |
$PPID | Process ID of the interpreter (or script) |
$# | Number of arguments |
$0, $1, $2, ... | Arguments |
$* | All arguments as a single word |
One final word about variables in bash and then we ll move on to some real programming. We can declare variables in bash, providing some form of typing. For example, we can declare a constant variable (cannot be changed after definition) or declare an integer or even a variable whose scope will extend beyond the script. Examples of these variables are shown below interactively:
$ x=1 $ declare -r x $ x=2 -bash: x: readonly variable $ echo $x 1 $ y=2 $ declare -i y $ echo $y 2 $ persist='$PWD' $ declare -x persist $ export grep persist declare -x persist="/home/mtj"
The last item may require a little more discussion. In this example, we create a variable persist and assign the current working subdirectory to it. We declare for exporting outside of the environment, which means if it had been done in a script, the variable would remain once the script had completed. This can be useful to allow scripts to alter the environment or to return variables.
We can perform simple arithmetic on variables, but there are differences from normal assignments that we ve just reviewed. Consider the source in Listing 20.3.
1 : #!/bin/bash 2 : 3 : x=10 4 : y=5 5 : 6 : let sum=$x+$y 7 : diff=$(($x - $y)) 8 : let mul=$x*$y 9 : let div=$x/$y 10 : let mod=$x%$y 11 : let exp=$x**$y 12 : 13 : echo "$x + $y = $sum" 14 : echo "$x - $y = $diff" 15 : echo "$x * $y = $mul" 16 : echo "$x / $y = $div" 17 : echo "$x ** $y = $exp" 18: exit
At lines 3 and 4, we create two local variables called x and y and assign values to them. We then illustrate simple math evaluations using two different forms. The first form uses the let command to assign the evaluated expression to a new variable. No spaces are permitted in this form. The second example uses the $(( < expr > )) form. Note in this case that spaces are permitted, potentially making the expression much easier to read.
Standard bitwise operators are also available in bash. These include bitwise left shift (<<), bitwise right shift (>>), bitwise AND (&), bitwise OR ( ), bitwise negate ( ~ ), bitwise NOT ( ! ), and bitwise XOR ( ^ ). The following interactive session illustrates these operators:
$ a=4 $ let b="$a<<1" $ echo $b 8 $ b=8 $ c=4 $ echo $(($c$d)) 12 $ echo $((0xc^0x3)) 15 $
Traditional logical operators can also be found within bash. These include the logical AND (&&) and logical OR ( ). The following interactive session illustrates these operators:
$ echo $((2 && 0)) 0 $ echo $((4 && 1)) 1 $ echo $((3 0)) 1 $ echo $((0 0)) 0
In the next section, we ll investigate how these can be used in conditionals for decision points.
Bash provides the typical set of conditional constructs. In this section, we ll explore each of these constructs and also investigate some of the other available conditional expressions that can be used.
In this section, we ll look at conditionals. The if/then construct provides a decision point after evaluating a test construct. The test construct returns a value as its result. The result of the test construct is zero for true (test succeeds and subsequent commands are executed) or nonzero for false (test fails else section, if available, is executed). Let s look at a simple example to illustrate (see Listing 20.4).
1 : #!/bin/bash 2 : a=1 3 : b=2 4 : if [[ $a -eq $b ]] 5 : then 6 : echo "equal" 7 : else 8 : echo "unequal" 9 : fi
Note | Note that the result of the test construct is the inverse of what you would expect. The is because the exit status of a command is 0 for success/normal and != 0 to indicate an error. |
After creating two variables, we test them for equality using the -eq comparison operator. If the test construct is true, we perform the commands contained in the then block. Otherwise, if an else block is present, this is executed (the test construct was false). else-if chains can also be constructed , as shown in Listing 20.5.
1 : #!/bin/bash 2 : x=5 3 : y=8 4 : if [[ $x -lt $y ]] 5 : then 6 : echo "$x < $y" 7 : elif [[ $x -gt $y ]] 8 : then 9 : echo "$x > $y" 10 : elif [[ $x -eq $y ]] 11 : then 12 : echo "$x == $y" 13 : fi
In this example, we test the integers for using the -lt operator ( less-than ), -gt (greater-than), and finally -eq (equality). Other operators are shown in Table 20.2.
Test constructs can also utilize strings such as is illustrated in Listing 20.6. In this example, we ll also look at two forms of the if/then/fi construct that provide identical functionality.
Operator | Description |
---|---|
-eq | is equal to |
-ne | is not equal to |
-gt | is greater than |
-ge | is greater than or equal to |
-lt | is less than |
-le | is less than or equal to |
1 : #!/bin/bash 2 : str="ernie" 3 : if [[ $str = "Ernie" ]] 4 : then 5 : echo "Its Ernie" 6 : fi 7 : 8 : 9 : if [[ "$str" == "Ernie" ]]; then echo "Its Ernie"; fi
After creating a string variable at line 2, we test it against a constant string at line 3. The = operator tests for string equality, as does the == operator. At line 9, we look at a visibly different form of the if/then/fi construct. As it s represented on one line, semicolons are used to separate the individual commands.
In Listing 20.5, we saw the use of the string equality operators. In Table 20.3, we see some of the other string comparison operators.
Operator | Description |
---|---|
= | is equal to |
== | is equal to |
!= | is not equal to |
< | is alphabetically less than |
> | is alphabetically greater than |
-z | is null |
-n | is not null |
As a final look at test constructs, let s look some of the more useful file test operators. Consider the script shown in Listing 20.7. In this script, we emit some information about a file (based upon its attributes). We use the file test operators to determine the attributes of the file.
1 : #!/bin/sh 2 : thefile="test.sh" 3 : 4 : if [ -e $thefile ] 5 : then 6 : echo "File Exists" 7 : 8 : if [ -f $thefile ] 9 : then 10 : echo "regular file" 11 : elif [ -d $thefile ] 12 : then 13 : echo "directory" 14 : elif [ -h $thefile ] 15 : then 16 : echo "symbolic link" 17 : fi 18 : 19 : else 20 : echo "File not present" 21 : fi 22 : 23 : exit
A large number of file test operators are provided by bash. Some of the more useful operators are shown in Table 20.4.
Operator | Description |
---|---|
-e | Test for file existence |
-f | Test for regular file |
-s | Test for file with nonzero size |
-d | Test for directory |
-h | Test for symbolic link |
-r | Test for file read permission |
-w | Test for file write permission |
-x | Test for file execute permission |
For the file test operators shown in Table 20.4, a single file argument is provided for each of the tests. Two other useful file test operators compare the dates of two files, illustrated as:
if [ $file1 -nt $file2 ] then echo "$file is newer than $file2" elif [ $file1 -ot $file2 ] then echo "$file1 is older than $file2" fi
The file test operator -nt tests whether the first file is newer than the second file, while -ot tests whether the first file is older than the second file.
If we re more interested on the reverse of a test, for example, whether a file is not a directory, then the ! operator can be used. The following code snippet illustrates this use:
if [ ! -d $file1 ] then echo "File is not a directory" fi
One special case to note is when you have a single command to perform based upon the success of a given test construct. Consider the following:
[ -r myfile.txt ] && echo "the file is readable."
If the test succeeds (the file myfile.txt is readable), then the command that follows is executed. The logical AND operator between the test and command ensures that only if the initial test construct is true will the command that follows be performed. If the test construct is false, the rest of the line is ignored.
This has been a quick introduction to some of the bash test operators. The Resources section at the end of this chapter provides more information to investigate further.
Let s look at another conditional structure that provides some advantages over standard if conditionals when testing a large number of items. The case command permits a sequence of test constructs utilizing integers or strings. Consider the example shown in Listing 20.8.
1 : #!/bin/bash 2 : var=2 3 : 4 : case "$var" in 5 : 0) echo "The value is 0" ;; 6 : 1) echo The value is 1 ;; 7 : 2) echo The value is 2 ;; 8 : *) echo The value is not 0, 1, or 2 9 : esac 10 : 11 : exit
We can also test ranges within the test construct. Consider the script shown in Listing 20.9 that tests against the ranges 0-5 and 6-9 . The special form [0-5] is used to define a range of values between 0 and 5 inclusive.
1 : #!/bin/bash 2 : var=2 3 : 4 : case $var in 5 : [0-5]) echo The value is between 0 and 5 ;; 6 : [6-9]) echo The value is between 6 and 9 ;; 7 : *) echo Its something else... 8 : esac 9 : 10 : exit
The case construct can be used to test characters as well. The script shown in Listing 20.10 illustrates character tests. Also shown is the concatenation of ranges, here [a-zA-z ] tests for all alphabetic characters, both lower- and uppercase.
1 : #!/bin/bash 2 : 3 : char=f 4 : 5 : case $char in 6 : [a-zA-z]) echo An upper or lower case character ;; 7 : [0-9]) echo A number ;; 8 : *) echo Something else ;; 9 : esac 10 : 11 : exit
Finally, strings can also be tested with the case construct. A simple example is shown in Listing 20.11. In this example, a string is checked against four possibilities. Note that at line 7, the test construct is made up of two different tests. If the name is Marc or Tim , then the test is satisfied. We use the logical OR operator in this case, which is legal within the case test construct.
1 : #!/bin/bash 2 : 3 : name=Tim 4 : 5 : case $name in 6 : Dan) echo Its Dan. ;; 7 : Marc Tim) echo Its me. ;; 8 : Ronald) echo Its Ronald. ;; 9 : *) echo I dont know you. ;; 10 : esac 11 : 12 : exit
This has been the tip of the iceberg as far as case test constructs go ”many other types of conditionals are possible. The Resources section at the end of this chapter provides other sources that dig deeper into this area.
Let s now look at how looping constructs are performed within bash. We ll look at the two most commonly used constructs; the while loop and the for loop.
The while loop simply performs the commands within the while loop as long as the conditional expression is true. Let s first look at a simple example that counts from 1 to 5 (shown in Listing 20.12).
1 : #!/bin/bash 2 : 3 : var=1 4 : 5 : while [ $var -le 5 ] 6 : do 7 : echo var is $var 8 : let var=$var+1 9 : done 10 : 11 : exit
In this example, we define our looping conditional at line 5 ( var < = 5 ). While this condition is true, we print out the value and increment var . Once the condition is false, we fall through the loop to done (at line 9) and ultimately exit the script.
Loops may also be nested. The sample script in Listing 20.13 illustrates this. In this example, we generate a multiplication table of sorts using two variables. Lines 4 to 18 define the outer loop, while lines 8 to 14 define the inner . The only difference, as in other high-level languages, is that the inner loop is indented to show the structure of the code.
1 : #!/bin/bash 2 : outer=0 3 : 4 : while [ $outer -lt 5 ] ; do 5 : 6 : inner=0 7 : 8 : while [ $inner -lt 3 ] ; do 9 : 10 : echo $outer * $inner = $(($outer * $inner)) 11 : 12 : inner=$(expr $inner + 1) 13 : 14 : done 15 : 16 : let outer=$outer+1 17 : 18 : done 19 : 20 : exit
Another interesting item to note about the script in Listing 20.13 is the arithmetic expressions used. In the outer loop we find the use of let to assign outer to itself plus one. The inner loop uses the expr command, which is an expression evaluator .
The for/in/do/done construct in bash allows us to loop through a range of variables. This differs from the classical for loop that is available in high-level languages such as C, but bash s perspective is very useful and offers some capabilities not found in C. Listing 20.14 provides a very simple for loop.
1 : #!/bin/bash 2 : 3 : echo Counting from 1 to 5 4 : 5 : for val in 1 2 3 4 5 6 : do 7 : echo -n $val 8 : done 9 : echo 10 : 11 : exit
The result of this script is:
# ./test.sh Counting from 1 to 5 1 2 3 4 5 #
Of course, we can emulate the C for loop mechanism very simply as shown in Listing 20.15.
1 : #!/bin/bash 2 : 3 : for ((var=1 ; var <= 5 ; var++)) 4 : do 5 : echo -n $var 6 : done 7 : echo 8 : 9 : exit
This code in Listing 20.15 is identical to the original for-in loop shown in Listing 20.14.
We can also use strings within our looping range, as illustrated in Listing 20.16. This script simply iterates through the string s provided range.
1 : #!/bin/bash 2 : 3 : echo -n The first four planets are 4 : for planet in mercury venus earth mars ; do 5 : echo -n $planet 6 : done 7 : echo . 8 : 9 : exit
Where bash shines over traditional high-level languages is in the capability to replace ranges with results of commands. Let s look at a more complicated example of the for-in loop that uses the replacement symbol * , which means the files in the current subdirectory (see Listing 20.17).
1 : #!/bin/bash 2 : 3 : # Save the current directory 4 : curwd=$PWD 5 : 6 : # Change the current directory to /home 7 : cd /home 8 : 9 : echo -n Users on the system are: 10 : 11 : # Loop through each file (via the wildcard) 12 : for user in *; do 13 : echo -n $user 14 : done 15 : echo 16 : 17 : # Return to the previous directory 18 : cd $curwd 19 : 20 : exit
We ve seen some examples already of output using the echo command. This command simply emits the provided string to the display. We also saw suppression of the newline character using the -n option. The echo command also provides the means to emit data through a binary interface. For example, to emit horizontal tabs, the \t option can be used, as:
echo -e \t\t\t\tIndented text.
Some of the other options that exist are shown in Table 20.5. To enable interpretation of these strings, the -e option must be specified before the string.
Sequence | Interpretation |
---|---|
\b | Backspace |
\f | Form feed |
\n | Newline |
\r | Carriage return |
\t | Horizontal tab |
\v | Vertical tab |
\\ | Backslash |
\NNN | ASCII code of octal value |
We can accept input from the user using the read command. The read command provides a number of options, a few of which we ll investigate. First, let s look at the basic form of a read command via the interactive bash shell:
# read var test string # echo $var test string # read -s var # echo $var silent input
In the first form, we read a string from the user and store it into variable var . The second form of read we specify the -s flag. The -s flag represents silent input, which means that characters that are entered in response to a read are not echoed back to the screen. In this case, we typed silent input , and we see this after the variable is echoed back.
Some of the other options that exist for the read command are shown in Table 20.6
Option | Description |
---|---|
-a | Input is assigned into an array, starting with index 0. |
-d | Character to use to terminate input (rather than newline). |
-n | Maximum number of characters to read. |
-p | Prompt string displayed to prompt user for input. |
-s | Silent mode (don t echo input characters). |
-t | Timeout in seconds for read. |
-u | File descriptor to read from rather than terminal. |
Bash allows us to break scripts up into more manageable pieces by creating functions. Functions can be very simple, such as:
function <name> () { sequence of command }
As in C, the function must be declared before it can be called. Let s now look at a simple example function that sums together two numbers that are passed in from the caller (see Listing 20.18).
1 : #!/bin/bash 2 : 3 : function sum () 4 : { 5 : 6 : echo $((+)) 7 : 8 : } 9 : 10 : sum 5 10
In this example, we declare a new function called sum (lines 3 “8), which emits the sum of the two parameters passed to it. Recall that $1 represents the first parameter and $2 represents the second. So what does $0 represent? Just as in C, the first argument from the perspective of a main program is the name of the program itself that was called. In this case, the name is the script file itself. What happens if the caller doesn t provide all of the necessary parameters ( passes only one parameter instead of two)? This would be a good time for some error checking, so we can update the script as shown in Listing 20.19.
1 : #!/bin/bash 2 : 3 : function sum () 4 : { 5 : 6 : if [ $# -ne 2 ] ; then 7 : echo usage is sum <param1> <param2> 8 : exit 9 : fi 10 : 11 : echo $((+)) 12 : 13 : } 14 : 15 : sum 5 10
Note in this version (updated from Listing 20.18) that error checking is now performed in lines 6 “9. We test the special variable $# , which represents the number of parameters passed to the constant 2. Since we re expecting two arguments to be passed to us, we echo the use and exit if two parameters are not present.
Note | The parameter variables are dependent upon context. So, in Listing 20.19, the $1 parameter at line 14 will be different from the $1 present at line 6. |
We can also return values from functions. We use the return command to actually return the value from the function, and then we use the special variable $? to access this value from the caller. See the example shown in Listing 20.20.
1 : #!/bin/bash 2 : 3 : function sum () 4 : { 5 : 6 : if [ $# -ne 2 ] ; then 7 : echo usage is sum <param1> <param2> 8 : exit 9 : fi 10 : 11 : return $((+)) 12 : 13 : } 14 : 15 : sum 5 10 16 : ret=$? 17 : 18 : echo $ret
In this version, rather than echo the result of the summation, we return it to the caller at line 11. At line 16, we grab the result of the function using the special $? variable. This variable represents the exit status of the last function called.
Now that we ve covered some of the basic elements of scripting, from variables to conditional and looping structures, let s look at some sample scripts that actually provide some useful functionality.
The goal of the first script is to provide a subdirectory archive tool. The single parameter for the tool is a subdirectory that will be archived using the tar utility, with the resulting archive file stored in the current working subdirectory. The script source can be found in Listing 20.21.
1 : #!/bin/bash 2 : 3 : # First, do some error checking 4 : if [ $# -ne 1 ] ; then 5 : echo Usage is ./archive.sh <directory-name> 6 : exit -1 7 : fi 8 : 9 : if [ ! -e ] ; then 10 : echo Directory does not exist 11 : exit -1 12 : fi 13 : 14 : if [ ! -d ] ; then 15 : echo Target must be a directory. 16 : exit -1 17 : fi 18 : 19 : # Remove the existing archive 20 : archive=.tgz 21 : 22 : if [ -f $archive ] ; then 23 : rm -f $archive 24 : fi 25 : 26 : # Archive the directory 27 : tar czf $archive 28 : 29 : exit
The goal of this script is to recursively search a directory to print any files that have been updated today. This is a relatively simple task that is also simple to express in bash.
The following sample script illustrates some other concepts not yet covered. See Listing 20.22 for the full script. This script is made up of three parts . The first part (lines 61 “63) invokes the script based upon the user s call. The second part (lines 48 “59) does some basic error checking and then starts the recursive process by interrogating the current subdirectory. Finally, the last part is the recursive function that looks at all files within a given subdirectory. Upon finding a new directory, the recurse() function is called again to dig down further into the tree.
When we call the fut.sh script, two functions are declared ( recurse() and main() ). The script ultimately ends up at line 61, where the main function is called using the first argument passed to the script as the argument passed to main().
In function main() (line 48), we begin by storing the current data in the format YYYY-MM-DD . This is performed using the date command, specifying the desired format in double quotes. We store this result into a variable called today . Note that today isn t local to main(); it can be used in other functions afterward.
Note | It is possible to declare a variable as local to a function. This is useful if we wish to store information in a function for recursive uses. To declare a local variable, we simply insert the local keyword before the variable. |
The main() function continues by storing the argument (the directory to recurse ) in variable checkdir (line 52). We then test checkdir to see if it s empty (has zero length) at line 54. If it is empty, we store . to checkdir , which represents the current subdirectory. This entire test was done so that if the user passed no arguments, we d simply use the current subdirectory as the argument default. Finally, we call the recurse() function with the checkdir variable (line 58).
The bulk of the script is found in function recurse() . This function recursively digs into the directory to find any files that have changed today. The first thing we do is cd into the subdirectory passed to us (line 15). Note that when . is passed, we cd into the current directory (in other words, no change takes place). We then iterate through the files in the subdirectory (line 18).
The first thing to check for a given file is to see if it s another directory (line 21). If it is, we simply call the recurse() function (recursively) to dig into this subdirectory (line 22). Otherwise, we check to see if the file is a regular file (line 25). If it is, we perform an ls command on the file, gathering a long time format (line 27). The time style format of this ls command ( long-iso ) happens to match the format that we gathered in main() to represent the date for today.
At line 29, we search the ls line ( longfile ) using our date stored in today . This is done through the grep command. We pipe the contents of longfile to grep to search for the today string. If the string is found in longfile , the line will simply result, otherwise a blank line will result. At line 31, we check to see if the check variable (the result of the grep ) is a nonzero length string. If so, we emit the current file and continue the process at line 18 to get the next file in the directory.
Once we ve exhausted the file list from line 18, we exit our loop at line 39. We check to see if the directory passed to us was not .. If not, we cd up one directory (because we cd d down one directory at line 15). If our directory was identified as . (the current directory), we avoid cd ing up one level.
Listing 20.22: Files Updated/Created Today Script (on the CD-ROM at ./source/_ch20/fut.sh )1 : #!/bin/bash 2 : # 3 : # fut.sh 4 : # 5 : # Find files created/updated today. 6 : # 7 : # Usage is: 8 : # 9 : # fut.sh <dir> 10 : # 11 : 12 : function recurse() 13 : { 14 : #