CSC 161 Schedule Readings Labs & Projects Homework Deadlines Resources

Bash Shell Scripts

Introduction

The Bash shell allows users to interact with the computer through a terminal window, organizing commands that tie various programs together. First developed by Brian Fox in 1987, the Bash shell seeks to incorporate numerous features from earlier shells for the Unix operating system. In particular, the Bash shell draws upon the Bourne shell (developed by Ken Thompson and then Stephen Bourne around 1977), the Korn shell (developed by David Korn in the early 1980s), and the C shell (developed by Bill Joy, based on Thompson's earlier shells).

Within a terminal window, you already have used several commands within the Bash shell. For example, the Basic Linux Commands reading discussed Bash commands, such as:

In this lab, we consider several additional commands, and we explore how various capabilities can be packaged to accomplish various tasks. The approach is not unlike programming in C (or Scheme), although the programming elements often are entire programs.

Some Additional Commands

Bash includes a huge range of commands. Here are a few more useful capabilities:

Command Description Example
cal display a calendar for a given year cal 2010
date print the current time and date in various formats
many options documented with man date
date +'%I:%M %p on %A, %B %d, %Y'
diff display differences in lines between two files diff file1 file2
echo print specified text echo hello world!
env print the environmental variables currently set env
grep scan a file or other input for a specified text env | grep home
hostname return name of current workstation hostname
sort sort lines of a file sort -n -k 12 (sort by numbers in column 12)
users simple list of users users
who print list of users currently logged in who and who -a
whoami print the username of the person logged in whoami
ypcat password print user information from the password file ypcat password | grep $USERNAME

More information on each of these commands may be found with the man facility (e.g., man date). Beyond this basic documentation included here an in previous labs, many resources are available on line. Here are a few places to begin:

Bash Script Files

A bash script is a file containing bash commands. To make the file executable, we must first take the following two steps.

  1. The script file should contain the following code as its very first line:
    #!/bin/bash
    This specifies which program (bash) should be used to interpret the commands in the script.
  2. The file permissions for the script should be set such that is it executable. Recall that this is typically done as follows:
    chmod 755 scriptname.sh

The script can then be run like any other executable file:

./scriptname.sh

Actually, we can also run a script as follows (even without making the script executable), but this is less frequently done.

bash scriptname.sh

Script variables

Variables can be created and initialized as shown below. Note that there should NOT be a space between the variable name and the assignment operator. (If the variable name has a space after it, bash will recognize it as a token, but since the variable does not yet exist, bash will assume the token is command name. It will then complain about not finding such a command.)

varname=12

To access the value in a variable, prepend a $ to the variable name. For example,

echo "Value in varname is $varname."

Arithmetic Expressions Require (( ))

The variables in a bash script are by default interpreted as text. To get bash to interpret a value as a number, in order to do arithmetic operations including arithmetic comparisons, you must wrap the expression in double-parentheses. For example, to increment a variable i by one, use the expression((i++)) .

Conditionals and Loops

In addition to basic commands, the Bash shell includes capabilities for conditionals (if) and loops (i.e., fora and while).

If statements

One general syntax for an if-statement follows. As you would probably expect, the elif and else clauses are optional, and the conditions and commands should be replaced with meaningful expressions. Note that the spaces that separate the square brackets from the conditions ARE required.

if [ condition ]; then
  command(s)
elif [ condition ]; then
  command(s)
else
  command(s)
fi

The long history of Bash is particularly evident in the syntax allowed for if expressions. Bash allows both, but interprets each syntax properly — but do not try to mix and match the different versions. The following examples illustrate acceptable Bash syntax, following two different ancestors.

Task Bourne shell style Korn shell style
numeric test
v > 0
if [ $v -gt 0 ]; then
    echo positive
fi
if (( $v > 0 )); then
    echo positive
fi
numeric test
v ≤ 0
if [ $v -le 0 ]; then
    echo non-positive
else
    echo positive
fi
if (( $v <= 0 )); then
    echo non-positive
else
    echo positive
fi
numeric test
v < 0 and v == 0
if [ $v -lt 0 ]; then
    echo negative
elif [ $v -eq 0 ]; then
    echo zero
else
    echo positive
fi
if (( $v <= 0 )); then
    echo non-positive
elif (( $v == 0 )); then
    echo zero
else
    echo positive
fi
numeric test
0 ≤ v ≤ 10
if [ 0 -le $v -a $v -le 10 ]; then
    echo between 0 and 10
else
    echo not between 0 and 10
fi
if (( (0 <= $v) && ($v <= 10) )); then
    echo between 0 and 10
else
    echo not between 0 and 10
fi

For reference, all of these code segments are available in the Bash shell bash-example-1.sh

Comparison Operators

Comparison operators are available in both the Bourne and Korn environments, although the syntax is different:

Korn shell:      ==   !=    <   <=    >   >=   &&   ||
Bourne shell:   -eq  -ne  -lt  -le  -gt  -ge   -a   -o

String comparison is possible only with the symbols

 ==  !=  <  <=  >  >=  &&  ||

Conditionals Regarding Files and Directories

Bash scripts are frequently written for tasks involving the creation and maintenance of files and directories because it can be much faster and easier to write a script for these tasks than to write the corresponding C programs.

There are many operators that can be used for testing conditions that involve files: whether they exist, what kind of file they are, etc. Here are a few. Note that you could also place the NOT operator (!) before any of these tests to test whether the stated condition is false.

condition checks whether...
-f file file exists and is a regular file
-d dir dir exists and is a directory
-x dir file exists and is executable

For example, you might write:

clist="classlist.txt"
if [ -f $clist ]; then
  echo File $clist exists.
else
  echo File $clist does not exist.
fi

for loops

The syntax for a for-loop follows.

for varname in value1 value2 ...
  do
    command(s)
 done

The variable varname will take on each value in the list of values in turn, with one iteration of the loop occuring for each value.

while loops

The syntax for a while loop follows.

while test
do
  command(s)
done

Getting Input

Getting Input From stdin

The command read varname will read a value from stdin (i.e., the terminal) and assign it to a variable called varname. If the variable does not yet exist, it will be created.

Note that the option -n can be used with echo to cause it to not output a newline character. (This is nice when printing user prompts, so the user can enter a reponse on the same line as the prompt).

Getting Input From Commands

Sometimes one wishes to execute a bash shell command or other program and store the result in a variable. This can be done by using the tick quote, which is under the tilde key, ~ and looks like this: `. Note now it is different from an apostrophe (near the enter key), which looks like '.

As an example, one might store the number of words in a file to a variable with the following command

numWords=`wc -w`

Getting Input From Command-line Arguments

As we'll see soon, the mechanism for accessing command-line arguments in a bash script differs substantially from C. In bash, there are several variables that are automatically defined and loaded with information from the command-line as shown below.

variable stores...
$# number of arguments given
$* list of all arguments given
$0 the name of the script
$1 the first argument
$2 the second argument
etc

For example, to verify the number of arguments given you might say:

if [ $# == 3 ]

Redirection and Piping

Some of the most powerful aspects of shell use and scripting are the abilities to redirect input or output and chain commands together with pipes. We look at each of these capabilities in turn.

I/O Redirection

Redirection allows your program to get its input from a file, rather than the terminal (called standard input or stdin), and to write its output to a file, rather than the terminal (standard output, or stdout).

To take the list of commands from a file, rather than the terminal, one uses the input redirection operator <. To direct the standard output to a file one uses the output redirection operator >. Here are three examples; the first uses input redirection, the second uses output redirection, and the third uses both.

command < input.txt
command > output.txt
command < other_input.txt > other_output.txt

The file names are determined by your particular circumstances, and could be anything (though of course the input file specified must exist). Note that output redirection will overwrite any existing file of the name you specify. You can append (rather than replace) an existing file by using the output operator with two angle brackets:

command >> appended_output.txt
As an example, one might use this to keep track of the time to run a program on input with an increasing size, as follows:
for ((inputSize=1 ; inputSize<=1024 ; inputSize=2*inputSize))
do
  echo -n "$inputSize " >> data.txt
 ./runMyTest $inputSize >> data.txt           
done

Pipes

The output of one program may be connected as the input of another program via the use of "pipes." This fabulous aspect of the shell scripting environment makes it possible to combine small, modular programs into larger computations that give very specific results from a few general building blocks. As an example, the following command lists all the users running processes on a machine.

ps au | cut -d\  -f 1 | sort | uniq
USER
dan
dols
interiot
pergamon
root
weinman
www-data

In this example, the ps command gives us lots of information on all the processes running on the machine. The output from ps is piped to cut, which (in this example) extracts the first field (the username) as separated by spaces of each line. That output is in turn piped into sort, which rearranges the entries into lexicographic order. Finally, this sorted list is processed by uniq so that all repeated values are removed, leaving us only with a unique list of active users.