Shell commands and shell scripts

Over the course of the term you have seen a number of examples where we have used the shell to issue commands. There is a wide universe of powerful tools available through the shell in Unix systems, and we have only just begun to tap the power of the shell and the utility programs. Another powerful aspect of the shell is shell scripting, which allows us to write programs in a shell scripting language to automate common tasks.

In these notes I am going to walk you through the development process for a simple shell script. Along the way we are going to learn the basics of shell scripting and get an introduction to some powerful utility programs.

The problem

Suppose you are a system administrator in a Unix system. One of the applications running on your system is the folder watcher system that we have been developing. A common maintenance task that an administrator would have to perform with this system is adding or removing watched folders. Of course, an adminstrator could always do this by editing the /etc/fwd.conf file, stopping the fwd application, and then restarting it. Alternatively, a skilled system administrator would just construct a script that could automate these tasks, making it possible to do all of these things by just issuing a single command.

Getting started

In its simplest form, the problem of adding a new watched folder to the fwd system involves adding a single line of text to the configuration file. We could try to do this from the shell by typing the command

"newTag /path/to/new/folder" > /etc/fwd.conf

There are two things wrong with this. The first is that when you type a line in the shell the shell will try to interpret what you have typed as a command. This will produce an error message:

bash: newTag /path/to/new/folder: No such file or directory

What we need here is a command that will generate the desired text. The > operator will then redirect the output of that command to the conf file. The echo command is what we need to do this:

echo "newTag /path/to/new/folder" > /etc/fwd.conf

This is also not quite right, since the redirection operator > ends up replacing the entire contents of the file in question with the one line we wanted to add. What we need instead here is to use an alternate operator, >>, which appends text to the file instead of wiping out the previous contents.

echo "newTag /path/to/new/folder" >> /etc/fwd.conf

This will do what we want, but may be an incomplete solution. A problem that this may not address is that the tag we are trying to use here may already be in use in the conf file. We should start by searching the existing conf file to see whether or not that is the case. The powerful grep function is our friend here. grep can search a file for a pattern that you supply and then print all of the lines in the file where that pattern appears.

grep newTag /etc/fwd.conf

This will work, but may produce more output than we wanted. This command prints all of the lines in fwd.conf where newTag appears. What we really want to see instead is all of the lines where newTag appears at the start of the line. To do this, we can run the search with a pattern that includes the special symbol ^. Patterns that start with ^ will return only lines where newTag appears at the start of the line:

grep "^newTag " /etc/fwd.conf

The search pattern here also includes a space after newTag, so that we don't accidentally catch lines that start with tags like newTag1 or newTag52.

Our first simple script

Now that we have developed a couple of useful shell commands to help with our task, it is time to start constructing a script that will run these commands for us.

Below is the text for our first simple script, simple.sh.

#!/bin/bash

exists=$(grep "^$1 " /etc/fwd.conf)
if [ -z $exists ]
then
  echo "$1 $2" >> /etc/fwd.conf
  kill -2 $(cat /run/fwd.pid)
  /opt/fwd/fwd
else
  echo "Tag $1 already exists."
fi

Here are some things to note in this code:

1. The first line in the script

#!/bin/bash

specifies what shell program we would like to run this script in. Many Unix systems have several different shell programs installed, and each of these alternative shells has its own syntax for shell scripting. To ensure that our script will run correctly, we have to specify which shell program to run the script in. The bash shell is the one of the most popular and widely used shells, and you can find a lot of documentation and information online on how to use the bash scripting language.

2. At various places in the script you will see the expressions $1 and $2. These are variable evaluations. The variables in question here are command line variables, whose values are command line parameters that the user will supply when they run the script. For example, if the user invokes this script by running the command

./first.sh newTag /path/to/new/folder

Then $1 will evaluate to newTag and $2 will evaluate to /path/to/new/folder.

3. The next line in the script sets up a variable exists. Variables in a script can contain text or integers. In most cases we set up variables to hold text. The value for this particular variable comes from what is called a command substitution. The expression $(<cmd>) runs the command <cmd> and then evaluates to the text that would be printed by that command. The command that we are running in this case is a grep search in the /etc/fwd.conf file that looks for lines that start with the tag that we are interested in. If the search finds a line, the expression will evaluate to the text of that line. If the search finds no lines, the expression will evaluate to an empty string. When the shell evaluates a command substitution expression it will first replace any variable evaluations in the command with their values and then run the command.

4. This script also contains a simple example of an if/else statement. The test in an if/else statement is supplied by a condition. The condition [ -z $exists ] that we used here checks whether the string stored in the exists variable is empty. If it is, this means that the new tag that we want to insert does not already exist and it is safe to add it to the fwd.conf file. If the exists variable is non-empty we instead just want to print an error message. You can read more about conditions in bash scripting on this page.

5. After successfully updating the conf file we will want to restart the fwd server. To do that we need to run the kill command with the pid of the fwd application. To get that pid we use a command substitution that runs the cat command to print the contents of the /run/fwd.pid file.

Running the script

Before we can run the script we just created we have to do one additional step. To run a script file we have to make the file executable. We do this by running the chmod command in a terminal to change the file permissions for the file:

chmod +x simple.sh

This adds the execute permission to the file. To run our script we then would invoke the same way we would invoke any other program:

./simple.sh newTag /path/to/folder

An improved script

Now that we have a script that does most of what we need done, we should go ahead and add a few additional features to make it more correct. In particular, we want to add these features:

  1. In the existing version if the user trys to add an entry with a tag that is already in use the script will refuse to do it. A more user-friendly option for this situation is to give the user the option to replace the existing entry with a new one.
  2. Likewise, it might make sense to check to see if the path the user wants to watch is already being watched. If this is the case, we want to ask the user if they want to keep the existing entry or replace it with a new one.

Here is an expanded version of the script that adds these features:

#!/bin/bash

tag=$(grep -w "^$1" /etc/fwd.conf)
path=$(grep -w "$2\$" /etc/fwd.conf)
restart="true"
if [ -n "$tag" ]
then
  read -p "Tag $1 already exists. Replace it?[y/n]" yn
  if [ yn != "n" ]
  then
    rest=$(grep -v "^$1" /etc/fwd.conf)
    echo $rest > /etc/fwd.conf
    echo "$1 $2" >> /etc/fwd.conf
  else
    restart="false"
  fi
else
  if [ -n "$path" ]
  then
    read -p "Path $2 already exists. Replace it?[y/n]" yn
    if [ yn != "n" ]
    then
      rest=$(grep -v " $2\$" /etc/fwd.conf)
      echo $rest > /etc/fwd.conf
      echo "$1 $2" >> /etc/fwd.conf
    else
      restart="false"
  else
    echo "$1 $2" >> /etc/fwd.conf
  fi
fi
if [ $restart == "true" ]
then
  kill -2 $(cat /run/fwd.pid)
  /opt/fwd/fwd
fi

This version of the script starts by doing two separate searches with grep. The first search locates entries that use the same tag as $1, while the second search finds lines that end with the same path as $2. Putting a $ at the end of a grep pattern finds that pattern when it occurs at the end of a line. Since the $ character has a special meaning in shell scripts we have to escape that character as \$ to use it in the grep command.

If we find that the tag the user wants to add exists already we will have to ask the user whether or not they want to replace it. To do this we use the read command to ask the user for input and then put the user's response into the yn variable.

Should the user want to replace the existing entry that uses the same tag, we have to then get every line but that line from the file. To do this we can make use of the -v option in grep, which will return every line in the file but the lines that match the pattern.

A report generating script

Another common use for shell scripts is to generate reports that summarize data from data collections such as log files. In this next example we are going to construct a script that gives a summary of what files had changes recorded in the fwd.log file and how many times those files are modified. For example, if the fwd.log file contains the entries

a one 8980
b five 9002
a two 9022
a two 9025
a one 9067
a one 9077
b three 9234
b three 9344
b five 9546
b five 9607
b five 9714
a two 9898

the reporting script would generate a report that looks like this:

a one 3
a two 3
b five 4
b three 2

In the notes below I am going to be developing a script that can generate this report from the fwd.log file. I will start the process of developing this script, and then leave the rest of the work to you as a homework assignment.

An obvious ingredient in our script is a loop that can iterate through the contents of the fwd.log file. Our first simple example script demonstrates how to write a simple loop that reads lines of input.

#!/bin/bash

while read line
do
  echo "$line" >> output.txt
done

This example demonstrates the use of the bash while loop. The command that controls this loop is

read line

which invokes the bash read command to read a line of input from the standard input and then store that line in a variable named line. In the body of the loop we take each line read and append it to a text file named output.txt.

When you run this script it will read lines of text that you type into the terminal. The while loop will stop when you terminate the read by pressing the control-d key combination. After running the script you can open the output.txt file to see that the text that you typed has been stored in the file.

In the next example we are going to construct a similar while loop. The difference this time is that we are going to use redirection to have the while loop read from a file instead of reading from the standard input.

#!/bin/bash

while read line
do
  echo "$line"
done < fwd.log

The expression < fwd.log after the end of the while loop is a file redirection that redirects the contents of the fwd.log file into the while loop so that the read command will be reading from that file instead of from the standard input. When you run this script it will print the contents of the fwd.log file to the output.

The next example demonstrates a trick that you can do with the read command. In the last example we used the read command to read a line of text from the input file into the line variable. In this example we will instead read the three things that appear on each line of the log file into three separate variables.

#!/bin/bash

while read tag file time
do
  echo "$tag $file"
done < fwd.log

When you run this script it will print just the first two columns of the log file to the console.

Homework assignment

At this point you have a start on building the reporting script we want to construct. I am now going to leave the rest of the script to you as a homework exercise. Here are some additional things your script should do:

  1. Your script should start by using the sort command to sort the contents of the fwd.log file. Use redirection to dump the output of the sort command to a text file.
  2. Use the loop that I set up in the last example to iterate over the lines of the text file you created in step 1.
  3. Add logic to the loop that allows you to count how many times you see the same combination of tag name and file name. As soon as you stop seeing entries with that particular combination of tag name and file name you should print that tag name and file name to the console with the count of how many times it appeared.
  4. To collect the counts you need in the loop you will need to set up a counter variable that stores an integer count. You will need to do some research online to see how to increment an integer counter in a bash script.