Writing Bash Shell Scripts
Table of Content
- First Script, Shebang, Exit Status, and Comment
- Getting Help
- Testing Script Code Snippet
- Data Types and Variables
- Flow Control
- Input and Output
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.
-
Open a terminal window, use a text editor, such as,
nano
orvi
, 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
-
Next, we make the script executable. Before we do, let’s first examine the file mode of the
helloworld.sh
using thels
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 thex
mode. -
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.
- Let’s write a Python script that prints out a “Hello, World!” message. On the terminal window, use a text editor, such as,
nano
orvi
to create ahelloworldpy
file whose content is as follows,#!/usr/bin/python3 import sys print('Hello, World!') sys.exit(0)
- Make the script executable, e.g.,
$ chmod +x helloworldpy
- Run the script, e.g.,
$ ./helloworldpy Hello, World!
- Now use a text edit and replace
/usr/bin/python3
by/usr/bin/python4
in thehellowordpy
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.
- 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 thehelloworld.sh
as a “Bourne-Again shell script” or a bash script, and thehellowordpy
as a “Python script”. This is because thefile
utility determines the type of scripts using the “shebang” line. - 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 thefile
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 thesubscript
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.
command
. The exit status code of the command is the value of the conditional. Following UNIX convention, value0
means success (true), non-zero failure (false).(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.[ condition ]
. The shell invokestest
to evaluate the condition, andtest
exits with the value of the condition as the exit status code. The exit status code is the value of the conditional.[[ condition ]]
. Similar to[ condition ]
, The shell invokestest
to evaluate the condition, andtest
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.((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 ofn
-
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 variablei
, i.e.,${i}
.{$i}
gives us the value of the variablei
, “directly” since there is only one variable reference to variablei
. When we use${!i}
, there are two variables references. First, we get the value of variablei
, that is one reference to variablei
. We use the value as a variable name, and then retrieve the value of this variable (whose name is the value ofi
), 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 variableIFS
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 setsIFS
to:
. -
read
reads from the Standard Input. We use “pipe (|
)” to redirect the Standard Output of thecat
command to the Standard Input of theread
command. We can achieve the same using the redirection operator<
, i.e., we can revise the above script without usingcat
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.