Scripting

If you have a collection of commands you'd like to run together, you can combine them in a script and run them all at once. You can also pass arguments to the script so that it can operate on different files or other input.

Like an actor reading a movie script, the computer will run each command in your shell script, without waiting for you to whisper the next line in its ear. A script is a handy way to:

  • Save yourself typing on a group of commands you often run together.
  • Remember complicated commands, so you don't have to look up, or risk forgetting, the particular syntax each time you use it.
  • Use control structures, like loops and case statements, to allow your scripts to do complex jobs. Writing these structures into a script can make them more convenient to type and easier to read. 

Let's say you often have collections of images (say, from a digital camera) that you would like to make thumbnails of. Instead of opening hundreds of images in your image editor, you choose to do the job quickly from the command line. And because you may need to do this same job in the future, you might write a script. This way, the job of making thumbnails will take you only two commands:

$ cd images/digital_camera/vacation_pictures_March_2009
$ make_thumbnails.sh

The second command, make_thumbnails.sh, is the script that does the job. It might look something like this:

#!/bin/bash
mkdir thumbnails
cp *.jpg thumbnails
cd thumbnails 
mogrify -resize 400x300 *.jpg

The first line is optional. The initial hash mark (#) turns it into a comment that the shell ignores when executing the script. It's a useful line, though, because when you run the program, the line indicates that the /bin/bash program (in other words, the Bash shell) should be invoked to run the script. The line is also useful documentation for anyone who comes along and wonders what's in the file.

Each of the following lines is a command. We've seen three of them before: mkdir, cp, and cd. The last command, mogrify, is a program that can resize images (and do a lot of other things besides). Read its manual page to learn more about it.

Making scripts executable

To write a script like the one we've shown, open your favorite text editor and type in the commands you would like to run. You can put multiple commands on a single line so long as you put a semi-colon after each command so the shell knows a new command is starting.

Save the script. One common convention is to use the .sh extension - for example, make_thumbnails.sh.

There is one more step before you can run the script: it has to be executable. Remember from the section on permissions that executability is one of the permissions a file can have, so you can make your script executable by granting the execute (x) permission. The following command allows any user to execute the script:

 chmod +x make_thumbnails.sh

Because you're probably planning to use the script often, you'll find it worthwhile to check your PATH and add the script to one of the directories in it (for instance, /home/jdoe/bin is an easy choice given the PATH shown here).

$ echo $PATH
/usr/bin:/usr/local/bin:/home/jdoe/bin

For simple testing, if you're in the directory that contains the script, you can run it like this:

$ ./make_thumbnails.sh

Why do you need the preceding ./ path? Because most users don't have the current directory in their PATH environment variables. You can add it, but some users consider that a security risk.

Finally, you can also execute a script, even without its execute bit set, by passing it as an argument to the command interpreter, thusly:

 bash make_thumbnails.sh

More control

To provide the flexibility you want, the shell let's you make choices in a script and run things repeatedly on a variety of inputs. In that regard, the shell is actually a programming language, and a nice way to get used to using the powerful features a programming language provides. We'll get you started here and show you the kinds of control the shell provides through compound statements.

if 

This statement was already introduced in the section on checking for errors, but we'll review it here. if is more or less what you'd expect, though its syntax is quite a bit different from its use in most other languages. It follows this form:

if [ test-condition ]
then
  do-something
else
  do-something-else
fi

You read that right: the block must be terminated with the keyword fi.  (It's one of the things that makes using if fun.) The else portion is optional. Make sure to leave spaces around the opening and closing brackets; otherwise if will report a syntax error.

For example, if you need to check to see if you can read a file, you could write a chunk like this:

if [ -r /home/joe/secretdata.txt ]
then
    echo "You can read the file"
else
    echo "You can't read that file!"
fi

if accepts a wide variety of tests. You can put any set of commands as the test-condition, but most if statements use the tests provided by the square bracket syntax. These are actually just a synonym for a  command named test. So the first line of the preceding example could just as well have been written as follows.

if test -r /home/joe/secretdata.txt

 You can find out more about tests such as -r in the manual page for test. All the test operators can be used with square brackets, as we have.

Some useful test operators are:

-r File is readable
-x File is executable
-e File exists
-d File exists and is a directory

There are many, many more of them, and you can even test for multiple conditions at once. See the the manual page for test.

while (and until)

while is a loop control structure. It will keep cycling through until its test condition is no longer true. It takes the following form:

while test-condition
do
  step1
  step2
  ...
done

You can also create loops that run until they are interrupted by the user. For example, this is one way (though not necessarily the best one) to look at who is logged into your system once every 30 seconds:

while true
do
    who
    sleep 30
done

This is inelegant because the user has to press CTRL-C or kill it in some other way. You can write a loop that ends when it encounters a condition by using the break command. For instance the following script uses the read command (quite useful in interactive scripts) to read a line of input from the user. We store the input in a variable named userinput and check it in the next line. The script uses another compound command we've already seen, if, within the while block, which allows us to decide whether to finish the while block. The break command ends the while block and continues with the rest of the script (not shown here). Notice that we use two tests through -o, which means "or". The user can enter Q in either lowercase or uppercase to quit.

while true
do
  echo "Enter input to process (enter Q to quit)"
  read userinput

  if [ $userinput == "q" -o $userinput == "Q" ]
  then
    break
  fi

  process input...

done

until works exactly the same way, except that the loop runs until the test condition becomes true.

case

case is a way for a script to respond to a generally small set of test conditions. It works similarly to case statements in other programming languages, though it has its own peculiar syntax, which is best illustrated through an example.

user=`whoami` # puts the username of the user executing the script
              # into the $user variable.
case $user in
    joe)
        echo "Hello Joe. I know you'd like to know what time it is, so I'll show you below."
        date
        ;;
    amy)
        echo "Good day, Amy. Here's your todo list."
        cat /home/amy/amy-todo.txt
        ;;
    sam|tex)
        echo "Hi fella. Don't forget to watch the system load. The current system load is:"
        uptime
        ;;
    *)
        echo "Welcome, whoever else you are. Get to work now, please."
        ;;
esac

Each case must be followed by the ) character, then a newline, then the list of steps to take, then a double semicolon (;;). The "*)" condition is a catchall, similar to the default keyword in some languages' case structures. If no other cases match, this list of statements will be executed. Finally, the keyword esac ends the case statement. In the example shown, note the case that matches either the string "sam" or "tex". 

for

for is a useful way of iterating through items in a list. It can be any list of strings, but it's particularly useful for iterating through a file list. The following example iterates through all of the files in the directory myfiles and creates a backup file for each one. (It would choke on any directories, but let's keep the example simple and not test for whether the file is a directory.)

for filename in myfiles/*
do
    cp $filename $filename.bak
done

As with any command that sets a variable, the first line of the for block sets the variable called filename without a dollar sign.

There's another variety of for, which is similar to the for construct used in other languages, but which is used less in shell scripting than it's used in other languages, partially because the syntax for incrementing and decrementing variables in the shell is not entirely straightforward.