Table of Content

First Script, Shebang, Exit Status, and Comment

Our first Bash shell script to print out a “Hello, World” message, and exit with “0” as the exit status code.

  1. Open a terminal window, use a text editor, such as, nano or vi, to type the following code and save it as a file, e.g., helloworld.sh.

    #!/bin/bash
    	
    #
    # The "Hello, World" Bash shell script
    #
    	
    echo "Hello, World!"  # print out the "Hello, World" message
    	
    exit 0
    
  2. Next, we make the script executable. Before we do, let’s first examine the file mode of the helloworld.sh using the ls command.

    Below is an example. Note that the “$” token is not your input, instead, it is a command prompt. We use this notation throughput the tutorial.

    $ ls -l  helloworld.sh
     -rw-r--r-- 1 cisc3320 cisc3320 42 Feb 18 16:24 helloworld.sh
    

    To make the script executable, we run on the terminal window,

    chmod +x helloworld.sh
    

    We examine the file mode of the helloworld.sh again,

    $ ls -l helloworld.sh
     -rwxr-xr-x 1 cisc3320 cisc3320 42 Feb 18 16:24 helloworld.sh
    

    Comparing this output with the one before we do chmod, you can see that the script we set the script to be executable indicated by the x mode.

  3. We can now run the shell script from the command line on the terminal window. The following is an example,

    $ ./helloworld.sh
    Hello, World!
    

Shebang

The very first line in the script is a “she-bang”, called “sha-bang” or “sh-bang” since it begins with “#” (the “sharp” token) and “!” (the “bang” token).

  • This line informs the command interpreter, in this case, the Bash command interpreter that it is /bin/bash that interprets and executes the script.
  • This line must be the very first line of the script.
  • ”#!” must be the very first two characters in the line”.

Exercises

To understand “she-bang”, let’s do an exercises.

  1. Let’s write a Python script that prints out a “Hello, World!” message. On the terminal window, use a text editor, such as, nano or vi to create a helloworldpy file whose content is as follows,
    #!/usr/bin/python3
       
    import sys
    
    print('Hello, World!')
    
    sys.exit(0)
    
  2. Make the script executable, e.g.,
     $ chmod +x helloworldpy
    
  3. Run the script, e.g.,
     $ ./helloworldpy
    Hello, World!
    
  4. Now use a text edit and replace /usr/bin/python3 by /usr/bin/python4 in the hellowordpy file, and run the script again. You would observe the following,
     $ ./helloworldpy
     -bash: ./helloworldpy: /usr/bin/python4: bad interpreter: No such file or directory
    

    It is clear from the example the command interpreter that the system uses to run the script is the one specified in the “shebang” line. If it isn’t correct, we won’t get the result as we desire.

  5. Run the following command.
     $ file ./helloworld.sh ./helloworldpy
     ./helloworld.sh:  Bourne-Again shell script, ASCII text executable
     ./helloworldpy: Python script, ASCII text executable
    

    What do you observe? You observe that the file utility correctly identifies the helloworld.sh as a “Bourne-Again shell script” or a bash script, and the hellowordpy as a “Python script”. This is because the file utility determines the type of scripts using the “shebang” line.

  6. Now use a text editor to add a blank line before the shebang line in any of the two script, e.g., we add a blank line at the beginning in the helloword.sh file. Next, we run the file utility again, e.g.,
     $ file ./helloworld.sh
     ./helloworld.sh: ASCII text
    

    What do you observe? How does it correlate with what we discussed?

Exit Status

A script returns an exit status code to the calling command interpreter. The command exit 0 in the helloworld.sh is to return 0 as the exit status code explicitly. We typically use this exit status code to inform the calling command interpreter whether we encounter an error in the script or we run it successfully.

Exercises

We can access the exit status code using the predefined variable $?, e.g.,

$ ./helloworld.sh
Hello, World!
$ echo $?
0

Now revise exit 0 to exit 1, and run the commands in the example again,

$ ./helloworld.sh
Hello, World!
$ echo $?
1

Note that this is important for us to write shell scripts since as we shall see, we use other programs to write useful shell scripts, those programs just like the shell script also returns exit status code, and we use this exit status code to determine whether those programs run successfully.

Comment

In a shell script, we begin a comment with a “#” token. The bash command interpreter treats anything after “#” in a line as a comment and ignores it.

Getting Help

In bash, you get help about shell scripts using the help command.

Exercises

For instance, to get help on delcare, we do,

help declare

For instance, to get help on for, we do

help for

To get an idea what bash offers, type,

help

Testing Script Code Snippet

A Bash shell script is a sequence of commands to be interpreted and executed in bash. As such, to run a bash shell script code snippet for testing or learning purpose, we don’t have to create a new script file for it, instead, we can just enter the commands in a terminal running bash (that we call it the bash terminal for convenience).

Perhaps, a simple practice that may sound trivial but has proven useful is to open two bash terminals, one is to write the intended bash shell script, and the other for testing commands we are unclear whether it would work.

Data Types and Variables

Essentially, any value in a bash shell script is a string; however, based on the usage, we can perform arithmetic operations on those values. As such, we call bash shell scripts untyped.

You may not have to declare a variable explicitly in a bash shell script. For instance, we assign string “Hello, World!” to variable msg.

msg="Hello, World!"

where bash does not permit space before or after the assignment operator =. However, to reference the variable, i.e., to retrieve the value of the variable, we need to use a “$” token, e.g.,

echo $msg

which prints out the value of the msg variable, i.e., “Hello, World!”.

Declaring Variables (Explicitly)

You may explicitly declare a variable, or even a read-only variable. Run and observe the following example,

declare m0
echo $m0
m0="Message 0"
echo $m0
declare m1="Message 1"
echo $m1
declare -r m2="Message 2"
echo $m2
m2="Attempt to change m2"

Data Types

Bash supports the following data types,

  • String
  • Integer
  • Indexed Array
  • Associative Array

String

Any value in a Bash shell script is essentially a string. We can use a pair of single quotation marks, or a pair of double quotation marks to define a string. However, there is a subtle difference between the two. We can observe the difference from the example below,

m1='Hello'
m2="World"
declare m3="!"
echo '$m1, $m2$m3'
echo "$m1,$m2$m3"

What do you observe? What is the difference between single-quoted strings and double-quoted strings?

Integer

Although any value in a Bash shell script is essentially a string, we can perform arithmetic operations on the values that looks like an integer value.

  • To perform arithmetic operations for an expression and assign the result to a variable, we can use the let command.
  • To perform arithmetic operations for an expression alone, we can use $((expression))

Observe the following example,

id=0
echo "id is intialized to $id"

let id++
echo "id is now $id"

let id+=2
echo "id is now $id"

inc=3
let id+=$inc
echo "id is now $id"

a1=5
a2=3
let a3=$a1+$a2

echo "a1+a2=$a1+$a2=$a3"
let a3=$a1-$a2
echo "a1-a2=$a1-$a2=$a3"

echo "$(($a1*$a2))"
echo "a1*a2=$a1*$a2=$(($a1*$a2))"
a3=$(($a1*$a2))
echo "a1-a2=$a1-$a2=$a3"

let a3=$a1/$a2
echo  "a1/a2=$a1/$a2=$a3"
echo  "3*a1/a2=3*$a1/$a2=$((3*$a1/$a2))"

To learn what arithmetic operations you may perform and what operators there are, please use the help command as in,

help let

Indexed Array

There are three methods to create an indexed array, implicitly or explicitly.

  • When we assign a value to variable using the syntax variable_name[subscript]=value we create an indexed array where the subscript is an arithmetic expression that should evaluate to a number.
  • We can also assign a list of values to a variable to create and initialize the whole array using variable_name=(value1, value2, ...)
  • We may also explicitly declare an indexed array by using declare -a variable_name.

Observe the following example to see how we use the two methods to create indexed arrays and to reference each element in the arrays.

student[0]="John Doe"
student[1]='Jane Doe'
student[2]="Carla Doe"

echo "$student" 
echo "$student[*]"
echo "$student[1]"
echo "${student[*]}"
echo "${student[2]}"
echo "${student[@]}"

for s in ${student[*]}; do
	echo "$s"
done

for s in ${student[@]}; do
	echo "$s"
done

for s in "${student[*]}"; do
	echo "$s"
done

for s in "${student[@]}"; do
	echo "$s"
done

for ((i=0; i<${#student[@]}; i ++)); do
	echo "${student[i]}"
done

student[2]="Brett Doe"
student[3]="Carla Doe"

for ((i=0; i<${#student[@]}; i ++)); do
	echo "${student[i]}"
done

teacher=("John Doe"  "Jane Doe"  "Brett Doe"  Mistake Doe)
echo "${#teacher[@]}"
echo "${teacher[0]}"
echo "${teacher[1]}"
echo "${teacher[2]}"
echo "${teacher[3]}"
echo "${teacher[4]}"

In the above example, make sure you also observe how we use the double braces (i.e., {}) to reference a variable, and difference between * and @.

Associative Array

In an indexed array, the index to an array element must be a number. We can understand an associative array as an array whose index can be any value, such as, a string. Many programming languages use “map” to reference an associative array.

We must explicitly declare an associative array, such as, by using declare -A variable_name. Alternatively, we can also explicitly declare an indexed array and initialize it using a list of key-value pairs by using declare -A variable_name=([key1]=value1 [key2]=value2 ...)

Observe the example below,

declare -A person
person['firstname']='Jane'
person[lastname]='Doe'
person['gender']='female'
person[birthday]='01/01/1988'

echo "${person['firstname']}"
echo "${person[firstname]}"
echo "${person['lastname']}"
echo "${person[lastname]}"

for f in "${person[@]}"; do
	echo "$f"
done

person[birthday]='12/31/1987'
echo "${person[birthday]}"

declare -A course=( [prefix]='CISC'  [number]='3320'   [section]='MW3'   [credit]=3   [days]='MW'   [time]='3:40-4:55' )
for key in "${!course[@]}"; do
	value=${course[$key]}
	echo "${key}: ${value}"
done

Flow Control

Conditionals

In bash, there are 5 types of conditionals you may write. The conditionals result in an exit status code being set, and the shell uses the exit status code to determine actions to take. We shall give a few examples when we discuss the if command.

  1. command. The exit status code of the command is the value of the conditional. Following UNIX convention, value 0 means success (true), non-zero failure (false).
  2. (commannd). The command runs in a subshell. The exit status code of the command is the value of the conditional. If the command changes the shell’s variables, the changes do not remain after the subshell completes.
  3. [ condition ]. The shell invokes test to evaluate the condition, and test exits with the value of the condition as the exit status code. The exit status code is the value of the conditional.
  4. [[ condition ]]. Similar to [ condition ], The shell invokes test to evaluate the condition, and test exits with the value of the condition as the exit status code. The exit status code is the value of the conditional. It has a few extended features, such as, you can use it to test whether a string matches a regular expression, and you can use logical operator to combine results of multiple conditions.
  5. ((condition)). This performs arithmetic operation. The shell determines the exit status code of this conditional based on the result of the arithmetic operation. It returns an exit status code of 0 (true) if the result of the arithmetic operation is nonzero; 1 if 0.

Branching

Bash has two branching commands, if and case.

The if Command

The if command generally takes the following form,

if COMMANDS_AS_CONDITIONAL
then 
	COMMANDS
elif COMMANDS_AS_CONDITIONAL
then
	COMMANDS
else 
	COMMANDS
fi

or an equivalent but more compact form,

if COMMANDS_AS_CONDITIONAL; then 
	COMMANDS
elif COMMANDS_AS_CONDITIONAL; then
	COMMANDS
else 
	COMMANDS
fi

or an equivalent but even more compact form,

if COMMANDS_AS_CONDITIONAL; then COMMANDS; elif COMMANDS_AS_CONDITIONAL; then COMMANDS; else COMMANDS fi

where you can have any number of elif blocks or none at all, and but a single else block or none at all.

Examples and Exercises

The following example prints a message if it finds user root in the system’s password file,

if grep -q "^root:" /etc/passwd; then 
	echo "Found the root user"
fi

where grep is a UNIX command that searches patterns in files.

The following example prints a message accordingly based on whether the file helloworld.c has a main function.

if grep -q main helloworld.c; then
	echo "The file has a main function"
else
	echo "The file doesn't  have a main function"
fi

The above two examples use the conditional form 1 discussed earlier.

The following example checks if hello.c exists, if it doesn’t, it continues to check if helloworld.c exists.

if [ -e hello.c ]; then
	echo "Found hello.c"
elif [ -e helloworld.c ]; then
	echo "Found helloworld.c"
else
	echo "hello.c and helloworld.c do notexists"
fi

This example use the conditional form 3 discussed earlier.

The following example checks if ‘vi’ is a symbolic link to a regular file, i.e., whether file ‘vi’ is a symbolic link and also a regular file, for which we use a logical conjunction to join the two conditions.

vi=`which vi`
if [ -L ${vi} ] && [ -f ${vi} ]; then
	echo "${vi} is a symoblic link to a regular file"
fi

where the conditional is in form 3 discussed earlier. We can also rewrite this example using a conditional in form 4 discussed earlier.

vi=`which vi`
if [[ -L ${vi}  && -f ${vi} ]]; then
	echo "${vi} is a symoblic link to a regular file"
fi

In the example, take a close look at command vi=`which vi` where we enclose which vi in a pair of the acute token (``` ) (also called backtick, backquote, and a few other names). When the shell sees the backticks, it will evaluate the command enclosed and replaces the pair of backticks and the enclosed by the output of the command before it continues to evaluate the command, e.g., in this particular case, `which vi` outputs `/usr/bin/vi`, and the shell replaces which vi `` by /usr/bin/vi, then the command becomes effectively, vi=/usr/bin/vi, as such, once the shell finishes evaluating the vi=/usr/bin/vi command, the vi variable gets the value of /usr/bin/vi.

The following example checks if 25 is the square of 5, if ((25==5*5)); then echo “25 is the square of 5” fi The above example uses the conditional form 5 discussed eariler.

The test Command

To learn more about writing conditionals, we may take a look at the manual page of the test command, i.e., run,

man test

The case Command

The case command takes the following form,

case WORD in 
PATTERN1)
	COMMANDS
	;;
PATTERN2)
	COMMANDS
	;;
esac

The case command executes commands whose associated pattern matches the word.

level=junior
case ${level} in
freshmen|sophomore)
	echo "The student is a freshman or a sophomore"
	;;
junior)
	echo "The student is a junior"
	;;
senior)
	echo "The student is a senior"
	;;
esac

Let’s take a look at the example below where we use wildcard * in the pattern, and also use it to introduce a “default” branch.

courseno=cisc3320
case ${courseno} in
cisc*)
	echo "${courseno} is a CIS class"
	;;
musi*)
	echo "${courseno} is a MUSIC class"
	;;
*)
	echo "${courseno} is neither a CIS nor a MUSIC class"
	;;
esac

Iteration (Loop)

Bash has two forms of for loops and a while loop.

The for ... in Command

The command takes the following form,

for NAME in WORDS ; do COMMANDS; done

or equivalently,

for NAME in WORDS 
do 
	COMMANDS
done

or equivalently,

for NAME in WORDS; do 
	COMMANDS
done

where WORDS is a list of words.

The following example prints out the number of lines of asm files,

files=`ls *.asm`
for f in ${files[*]}; do
	lines=`wc -l $f | cut -d' ' -f1`
	echo "${f} has ${lines} lines"
done

The for (( exp1; exp2; exp3 )) Command

This for command takes the following form,

 for (( exp1; exp2; exp3 )); do COMMANDS; done

whose equivalent form can be,

 for (( exp1; exp2; exp3 )); do 
 	COMMANDS
 done

or

 for (( exp1; exp2; exp3 ))
 do 
 	COMMANDS
 done

where exp1, exp2, and exp3 must be arithmetic expressions.

The following example computes the sum from 1 to 50,

let s=0
for ((i=1; i<=50; i++)); do
	let s=$s+$i
done
echo "The sum from 1 to 50 (inclusive) is $s"

The while Command

The while command takes the following form,

while COMMANDS; do COMMANDS; done

which is most flexible, but can be more cumbersome to write in some situations than the for loops. The following example comptes the sum from 1 to 50 as well, but using the while loop,

let s=0
let i=1
while [ $i -le 50 ]; do
	let s=$s+$i
	let i++
done
echo "The sum from 1 to 50 (inclusive) is $s"

Input and Output

We discuss a few methods to provide input to a shell script.

Command Line Arguments and Positional Parameters

We use a few internal variables to access command line arguments.

  • Positional parameters. We consider that a command line consists of a lists of parameters separated by one or more space, and we can use the positional parameters, such as, $0, $1, …, $n where n is the position of the parameters on the command line to access each parameter.

  • We use $# to obtain the number of command line arguments, i.e., the value of n

  • In addition, we can use $@ to access the entire command line arguments, i.e., a concatenation $1, $2, … separated by spaces.

Below is an example script that we shall save it as clargs.sh,

#!/bin/bash

echo "The name of the script is $0"
echo "This command has $# command line arguments"
if [ $# -gt 0 ]; then
	echo "These command line arguments are $@"
	echo "Each of the arguments is"
	for ((i=1; i<=$#; i++)); do
		echo "The value of the $i-th argument is \"${!i}\""
    done
fi

We can run this script with any number of command line arguments, e.g.,

$ ./clargs.sh arg1 arg2 "arg 3" "arg 4" arg5

The name of the script is ./clargs.sh
This command has 5 command line arguments
These command line arguments are arg1 arg2 arg 3 arg 4 arg5
Each of the arguments is
The value of the 1-th argument is "arg1"
The value of the 2-th argument is "arg2"
The value of the 3-th argument is "arg 3"
The value of the 4-th argument is "arg 4"
The value of the 5-th argument is "arg5"
$

We should take a note about the following,

  • In this example, ${!i} is an example of variable “indirect reference”. To understand this, consider the “direct” reference to variable i, i.e., ${i}. {$i} gives us the value of the variable i, “directly” since there is only one variable reference to variable i. When we use ${!i}, there are two variables references. First, we get the value of variable i, that is one reference to variable i. We use the value as a variable name, and then retrieve the value of this variable (whose name is the value of i), i.e., two variable references. As such, we “indirectly” obtain the value of ${!i} , hence, indirect reference to the command line argument.
  • In command echo "The value of the $i-th argument is \"${!i}\"", \ serves as an escape character, and it preserves the literal value of the next character that follows. We use it because we want to print out the double quotation marks within a string enclosed in a pair of double quotation marks.
  • Generally, we use spaces to delimit command line arguments. However, we can still have arguments that has space in them. As this example shows, we simply put those arguments in quotation marks.

Instead of using variable indirect reference, we can also take advantage of the shift command without using the indirect reference to command line arguments. Below is the version of the clargs.sh using the shift command but without using the indirect reference.

#!/bin/bash

echo "The name of the script is $0"
echo "This command has $# command line arguments"
if [ $# -gt 0 ]; then
        echo "These command line arguments are $@"
        echo "Each of the arguments is"
        let i=1
        while (($#)); do
                echo "The value of the $i-th argument is \"${1}\""
                let i++
                shift
        done
fi

What does shift do? Perhaps, you should consult Bash’s online help,

help shift

Reading from Standard Input (and Files)

Bash provides a read command that allows us to read from the Standard Input or read from a file via redirection. First, we should consult Bash’s online help,

help read

To understand the manual, let us run a few examples. First, observe the following example we run in a bash terminal

$ read v
hello, world
$ echo $?
0
$ echo $v
hello,world
$ read -a arr
hello world
$ echo $?
0
$ for ((i=0;i<${#arr[@]};i++)); do echo ${arr[i]}; done
hello
world
$ read v
^D
$ echo $?
1
$

where ^D is key stroke CTRL-D. In the above, we enter a line, and the read command reads the line and assigns the value to variable v; next, we enter a line, and the read command reads the line and uses it to initialize array arr. CTRL-D (or ^D) means the end of “file” (or the input), and the exit code of read becomes 1, i.e., read fails to read.

Reading from Files via Pipe and Redirection

Understanding this, we write a script called printuser.sh that reads and prints out the usernames from /etc/passwd.

#!/bin/bash

IFS=':'
cat /etc/passwd | while read -a user
do
        echo ${user[0]}
done

We have a few explanations,

  • read uses the value of built-in variable IFS to split a line into fields to initialize an array. /etc/passwd contains one line for each user account, with seven fields delimited by colons (“:”), and the very first field is login name. We hence sets IFS to :.

  • read reads from the Standard Input. We use “pipe (|)” to redirect the Standard Output of the cat command to the Standard Input of the read command. We can achieve the same using the redirection operator <, i.e., we can revise the above script without using cat as follows, ```bash #!/bin/bash

IFS=’:’ while read -a user do echo ${user[0]} done < /etc/password


#### Reading From "Here Documents"

Sometimes we can read from so salled "Here Documents". A here document is a special code block that the shell redirects it to the Standard Input of the command like `read`. Below is an example, 

#!/bin/bash

IFS=’,’ while read -a student do echo “name=${student[0]} level=${student[1]}” done « END John Doe,Freshman,Male Jane Doe,Sophomore,Female Carla Doe,Senior,Female END echo “Read ${lines} lines”

We save this as `readheredoc.sh`, and run it,
```terminal
$ ./readheredoc.sh
name=John Doe level=Freshman
name=Jane Doe level=Sophomore
name=Brett Doe level=Junior
name=Carla Doe level=Senior
Read 4 lines
$

In the code the END ... END block is the here document that the shell redirects it as the Standard Input to read. Note that we can use any marker instead of END to mark the end of the here document, and we use << to inform the shell that we are redirecting a here document.