Bash Scripting, Part 1: Variables, Input, Arithmetic and If
In the last two labs you typed Linux commands one at a time. This week you start writing them down: a Bash script is a file of commands that Bash runs for you, top to bottom. You will create and run your first scripts, store values in variables, let the user supply data as input, calculate with whole numbers, and make decisions with if. Loops and functions come next week, in Part 2.
Every script on this page was saved in a folder called lab11 and really run on an Ubuntu 24.04 machine (Bash 5.2.21) by a normal user called student, on a machine named ubuntu-lab, with the default English locale (en_US.UTF-8). Every output shown is the real output of that run. When you repeat them, dates, times, and anything that counts the files on your system will differ; everything else should match.
Objectives
By the end of the section you should be able to:
- Explain what a Bash script is and how it runs as a process.
- Create a script, give it execute permission with
chmod 755, and run it with./script.shand withbash script.sh. - Explain the shebang line
#!/bin/bash, and why a script in the current folder needs./in front of its name. - Set and read variables, use single and double quotes, save a command's output with
$( ), and use the command line arguments$1,$2,$#,$@and the other special variables. - Pass a variable to another script with
export. - Take input with
read,read -pandread -s, read several words at once, and read data piped into the script (STDIN). - Do whole-number arithmetic with
let,expr,$(( ))and(( )), and find the length of a variable. - Make decisions with
if,else,elif, nestedif, thetestoperators,&&,||andcase, and read an exit status with$?.
Before you start: the lab folder
Open a terminal and make a folder for this lab:
$ mkdir ~/lab11
$ cd ~/lab11Create each script on this page in that folder with the Text Editor (or with nano name.sh in the terminal), using exactly the file name given above the listing. One data file is used by several examples. Save it as salesdata.txt:
stall1 apples 20 September 4
stall2 oranges 5 September 7
stall3 watermelons 12 September 10
stall4 peaches 7 September 15Part 1: What is a Bash script?
So what are they exactly?
Think of a script for a play or a film: it tells the actors what to say and do. A script for a computer tells the computer what to do. A Bash script is a plain text file which contains a series of commands, and Bash runs them one after the other.
The most important point to remember:
Anything you can run normally on the command line can be put into a script and it will do exactly the same thing. Similarly, anything you can put into a script can also be run normally on the command line and it will do exactly the same thing.
You do not need to change anything: type the commands as you would at the prompt. So if you know how to do something at the command line, you already know a fair bit of Bash scripting. It is a convention to give Bash scripts the extension .sh (for example myscript.sh), but Linux is an extensionless system, so a script does not need it in order to work.
How do they work? Programs and processes
A program is a set of instructions for the CPU, stored on the disk. When we "run a program", Linux copies those instructions into memory (RAM) and gives the copy some space for its variables: this running copy is a process. A process is a running instance of a program. Two terminals both running cp at the same moment means two cp processes; when they finish, the system destroys them.
Your terminal itself is a Bash process: it gives you the prompt. When you start a script, it does not run inside that process. It starts a new process and runs inside it. You will see why this matters when we export variables in Part 2.
Your first script
Save this as myscript.sh:
#!/bin/bash
# A sample Bash script
echo Hello World!Line by line:
#!/bin/bashis the shebang (explained below): it says which program runs this file.# A sample Bash scriptis a comment. Anything after#on a line is not executed; it is there for the human reader.echo Hello World!prints a message. You could type this command at the prompt and it would behave exactly the same.
Running it: the execute permission
Before we can run (or execute) a script, it must have the execute permission. For safety, this permission is not set on a new file. Forgetting it does no harm: you just get an error message.
Worked example 1: run the new script
Question. myscript.sh has just been saved. Predict what each of these commands prints: ./myscript.sh, ls -l myscript.sh, chmod 755 myscript.sh, ls -l myscript.sh, ./myscript.sh.
Solution.
$ ./myscript.sh
bash: ./myscript.sh: Permission denied
$ ls -l myscript.sh
-rw-r--r-- 1 student student 53 Sep 25 12:10 myscript.sh
$ chmod 755 myscript.sh
$ ls -l myscript.sh
-rwxr-xr-x 1 student student 53 Sep 25 12:10 myscript.sh
$ ./myscript.sh
Hello World!The first attempt is refused: the permissions -rw-r--r-- have no x. (On your machine a new file may show -rw-rw-r-- instead; there is still no x.) chmod 755 sets the permissions to rwxr-xr-x: the owner may read, write and execute, everyone else may read and execute. The shorthand 755 is often used for scripts because it lets you modify the script and lets everyone run it. After that the script runs and prints its message.
A second way to run a script is to start bash yourself and give it the file name as an argument. It works too, but it means extra typing every time:
$ bash myscript.sh
Hello World!Why the ./ in front?
When you type just a name, such as ls, Bash searches for it in a list of directories stored in the variable PATH, and runs the first match. It does not look in sub directories, and it does not look in the current directory:
$ myscript.sh
bash: myscript.sh: command not found
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/binThe directories are separated by :. Your own PATH may list more directories, but the current folder is not one of them. This is done for a few reasons:
- It lets you keep several versions of a program and choose which one runs by its place in the list.
- It is convenient: you can keep your own scripts in a folder that is in your
PATHand run them by name from anywhere. - It is safer: someone could create a harmful script called
lsin some folder; as long as that folder is not in yourPATH, typinglsnever runs it by accident.
To run a program that is not in your PATH, tell Bash where it is with a relative or an absolute path. The dot . means the current directory, so ./myscript.sh is a relative path. The absolute path works the same:
$ /home/student/lab11/myscript.sh
Hello World!The shebang (#!)
The characters #! at the start of the first line are called the shebang. They are followed by the path to the interpreter, the program that should run the rest of the file. For a Bash script that is /bin/bash; other kinds of scripts name their own interpreter.
Formatting matters here:
- The shebang must be on the very first line of the file (line 2 will not do, even if line 1 is blank).
- There must be no spaces before the
#or between the!and the path. - Use the absolute path of the interpreter, because the script may be run from any folder.
It is possible to leave the shebang out: if you run such a script from a Bash terminal, Bash assumes it is a Bash script. That only works when the person running it is in a Bash shell, which is not always the case, so always include the shebang.
Worked example 2: a broken shebang
Question. badbang.sh is executable, but one letter is missing from its interpreter path. Predict the result of ./badbang.sh and of bash badbang.sh.
#!/bin/bas
# the interpreter path is misspelled
echo Hello World!Solution.
$ ./badbang.sh
bash: ./badbang.sh: cannot execute: required file not found
$ bash badbang.sh
Hello World!./badbang.sh asks Linux to start the interpreter named in the shebang, /bin/bas, and that file does not exist, so the script cannot start. bash badbang.sh starts Bash directly: Bash is the interpreter now, and for Bash the first line is just a comment, so the script runs.
Comments
A comment is text that Bash ignores; it explains the code to whoever reads it later. A # starts a comment that runs to the end of the line. For a comment over several lines you can use : followed by text in single quotes: : is a command that does nothing, so the quoted text is ignored. The comment text must not contain a single quote of its own. You will see both kinds in number.sh in Part 5.
Formatting: spaces matter
In many places in Bash the presence or absence of a space is the difference between a line that works and one that does not. Bash was first built for users typing at a terminal and scripting was added later, around those decisions. The shebang was the first example; you will meet more in this lab. Get into the habit of noticing spaces when you read code.
Part 2: Variables
How variables work
A variable is a temporary store for a piece of information. There are two things we do with a variable:
- Set a value:
name=value. There is no$when setting, and no space on either side of=. - Read the value: put a
$before the name,$name, anywhere in a line.
Before Bash runs a line, it first looks for variable names in it and replaces each one with its value; then it runs the line. Variable names may be upper case, lower case or mixed, but Bash is case sensitive, so always write a name the same way. Give variables descriptive names.
Worked example 3: set and read a variable
Question. At the prompt we type these four commands. Predict each output: myvar = Hello, myvar=Hello, echo $myvar, echo $MYVAR.
Solution.
$ myvar = Hello
bash: myvar: command not found
$ myvar=Hello
$ echo $myvar
Hello
$ echo $MYVAR
With spaces around =, Bash reads myvar as a command with the arguments = and Hello, and there is no such command. Without spaces the value is stored and echo $myvar prints it. $MYVAR is a different variable that was never set, so echo prints an empty line.
Variables in a script
Save as simplevariables.sh:
#!/bin/bash
# A simple variable example
myvariable=Hello
anothervar=Linux
echo $myvariable $anothervar
echo
sampledir=/etc/apt
ls $sampledir- Lines 3 and 4 set the variables
myvariableandanothervar. - Line 5 prints both, to check that they were set.
- Line 6 runs
echowith no arguments: a good way to print a blank line and space the output out. - Line 7 sets a variable to the path of a directory, and line 8 runs
lswith the value ofsamplediras its argument.
$ ./simplevariables.sh
Hello Linux
apt.conf.d auth.conf.d keyrings preferences.d sources.list sources.list.d trusted.gpg.decho is used here only to show the variables were set; you do not need echo to use a variable. Variables also make scripts easier to manage: if a script refers to the same directory in several commands, set it once in a variable, and if the directory changes you update one line.
Quotes
So far each value was a single word. Bash uses spaces to separate items, so a value with a space needs quotes: they tell Bash the content is one item. You may use single quotes ' or double quotes ":
- Single quotes treat every character literally.
- Double quotes allow substitution: variables inside them are replaced by their values.
Worked example 4: single and double quotes
Question. Predict the output of each line:
$ myvar=Hello World
$ myvar='Hello World'
$ echo $myvar
$ newvar="More $myvar"
$ echo $newvar
$ newvar='More $myvar'
$ echo $newvarSolution.
$ myvar=Hello World
bash: World: command not found
$ myvar='Hello World'
$ echo $myvar
Hello World
$ newvar="More $myvar"
$ echo $newvar
More Hello World
$ newvar='More $myvar'
$ echo $newvar
More $myvarWithout quotes, Bash sees two items, and it tries to run World as a command. With quotes, the whole Hello World is stored. In double quotes $myvar is replaced by its value; in single quotes it stays as the text $myvar. Because commands behave the same at the prompt and in a script, the prompt is a quick place to experiment.
Command substitution
Command substitution saves the output of a command (what it would print on the screen) as the value of a variable. Put the command inside $( ):
$ myvar=$( ls /etc | wc -l )
$ echo There are $myvar entries in the directory /etc
There are 74 entries in the directory /etc
$ ls /etc/apt
apt.conf.d auth.conf.d keyrings preferences.d sources.list sources.list.d trusted.gpg.d
$ files=$( ls /etc/apt )
$ echo $files
apt.conf.d auth.conf.d keyrings preferences.d sources.list sources.list.d trusted.gpg.d74 is the number of entries in /etc on our lab machine; your system will print its own number. If the output goes over several lines, the variable keeps the line breaks (only the final one is removed). echo $files shows it on one line only because $files is not quoted: Bash splits the value into words and echo prints them with single spaces. echo "$files" prints them on 7 lines. When you use command substitution, echo the variable to check what it really holds.
Command line arguments
When you run ls -l /etc, the words -l and /etc are command line arguments to ls. A script receives arguments the same way: $1 holds the first argument, $2 the second, and so on. The system sets them for you; you only refer to them. Save as mycopy.sh:
#!/bin/bash
# A simple copy script
cp $1 $2
# Let's verify the copy worked
echo Details for $2
ls -lh $2- Line 3 copies the first argument (the source) to the second (the destination).
- Line 5 prints a message, and line 6 lists the new file with
-l(details) and-h(human readable size) to check the copy.
$ ./mycopy.sh salesdata.txt backup.txt
Details for backup.txt
-rw-r--r-- 1 student student 123 Sep 25 12:11 backup.txtOther special variables
The system sets a few more variables for every script:
| Variable | Holds |
|---|---|
$0 | the name of the Bash script |
$1 to $9 | the first 9 command line arguments |
$# | how many arguments were passed to the script |
$@ | all the arguments supplied to the script |
$? | the exit status of the most recently run process |
$$ | the process ID of the current script |
$USER | the user name of the user running the script |
$HOSTNAME | the host name of the machine the script is running on |
$SECONDS | the number of seconds since the script was started |
$RANDOM | a different random number each time it is referred to |
$LINENO | the current line number in the script |
The command env lists many other variables you can refer to.
Worked example 5: what did the script receive?
Question. Save args.sh and predict the output of ./args.sh red green blue, and then of ./args.sh with no arguments.
#!/bin/bash
# special variables set by the system
echo "Script name: $0"
echo "Number of arguments: $#"
echo "All arguments: $@"
echo "Second argument: $2"
echo "User: $USER, host: $HOSTNAME"
echo "This is line $LINENO"Solution.
$ ./args.sh red green blue
Script name: ./args.sh
Number of arguments: 3
All arguments: red green blue
Second argument: green
User: student, host: ubuntu-lab
This is line 8
$ ./args.sh
Script name: ./args.sh
Number of arguments: 0
All arguments:
Second argument:
User: student, host: ubuntu-lab
This is line 8$0 is the script name as it was typed, ./args.sh. With three arguments, $# is 3, $@ lists them all and $2 is green. With none, $# is 0 and $@ and $2 are empty. $LINENO is 8 because the echo is on line 8.
Exporting variables
Remember that a script runs in its own process. Variables belong to the process that created them; this is called scope. When one script runs another, the second script is a new process and does not see the first one's variables, unless the first one exports them.
Worked example 6: export and scope
Question. script1.sh sets two variables, exports only one of them, and runs script2.sh, which changes both. Predict the three lines printed by ./script1.sh.
#!/bin/bash
# demonstrate variable scope 1.
var1=blah
var2=foo
# Let's verify their current value
echo $0 :: var1 : $var1, var2 : $var2
export var1
./script2.sh
# Let's see what they are now
echo $0 :: var1 : $var1, var2 : $var2#!/bin/bash
# demonstrate variable scope 2
# Let's verify their current value
echo $0 :: var1 : $var1, var2 : $var2
# Let's change their values
var1=flop
var2=blehSolution.
$ ./script1.sh
./script1.sh :: var1 : blah, var2 : foo
./script2.sh :: var1 : blah, var2 :
./script1.sh :: var1 : blah, var2 : fooexport var1 tells Bash: whenever a new process is created, hand it a copy of var1. So script2.sh sees var1 (blah) but not var2 (empty). The copy is a separate variable in a separate process: when script2.sh changes it to flop, the original in script1.sh is still blah. Exporting works one way, from a process to the processes it starts.
Part 3: Input
Command line arguments are one way to give data to a script. Here are two more: asking the user while the script runs, and reading data piped into the script.
Ask the user with read
read takes a line of input from the user and saves it in a variable. Save as introduction.sh:
#!/bin/bash
# Ask the user for their city
echo Hello, which city are you from?
read varcity
echo It\'s nice to meet someone from $varcity- Line 3 prints a question.
- Line 4 runs
read, which waits for the user to type a line and press Enter, and saves it invarcity. - Line 5 prints a message to check that
readworked. The backslash inIt\'sescapes the quote, so it is printed as a plain'instead of starting a quoted string.
$ ./introduction.sh
Hello, which city are you from?
Mansoura
It's nice to meet someone from Mansoura(Mansoura is what was typed.)
read -p and read -s
Two useful options change how read behaves: -p shows a prompt, and -s makes the input silent (nothing is shown as you type). Together they are handy for a user name and a password. Save as login.sh:
#!/bin/bash
# Ask the user for login details
read -p 'Username: ' uservar
read -sp 'Password: ' passvar
echo
echo Thank you $uservar, we now have your login detailsThe prompts are in quotes so that they can end with a space; otherwise the typed text would start right after the colon.
$ ./login.sh
Username: student
Password:
Thank you student, we now have your login detailsThe password was typed but never appeared. Because -s also hides the Enter key, the cursor would stay on the Password: line, so line 5 runs echo with no arguments to move to a new line.
Several variables in one read
You can give read several variable names. It splits the line on spaces: the first word goes to the first variable, the second to the second, and so on. Save as cars.sh:
#!/bin/bash
# Demonstrate how read actually works
echo What cars do you like?
read car1 car2 car3
echo Your first car was: $car1
echo Your second car was: $car2
echo Your third car was: $car3Worked example 7: more words than variables
Question. The user types Jaguar Maserati Bentley in the first run, Jaguar Maserati Bentley Lotus in the second, and Jaguar Maserati in the third. Predict each output.
Solution.
$ ./cars.sh
What cars do you like?
Jaguar Maserati Bentley
Your first car was: Jaguar
Your second car was: Maserati
Your third car was: Bentley
$ ./cars.sh
What cars do you like?
Jaguar Maserati Bentley Lotus
Your first car was: Jaguar
Your second car was: Maserati
Your third car was: Bentley Lotus
$ ./cars.sh
What cars do you like?
Jaguar Maserati
Your first car was: Jaguar
Your second car was: Maserati
Your third car was:With exactly three words each variable gets one. If there are more words than variables, all the remaining words go into the last variable, car3. If there are fewer, the remaining variables are left empty.
Reading from STDIN
Linux lets you pipe simple commands together into a bigger solution, and a script can join a pipeline too. Every process has three special files: STDIN (input), STDOUT (output) and STDERR (errors), which Linux connects when you use a pipe or a redirection. Their shortcuts are /dev/stdin, /dev/stdout and /dev/stderr. To process data piped into a script, the script reads /dev/stdin like any normal file. Save as summary.sh:
#!/bin/bash
# A basic summary of my sales report
echo Here is a summary of the sales data:
echo ====================================
echo
cat /dev/stdin | cut -d' ' -f 2,3 | sort- Lines 3 to 5 print a title.
- Line 6 reads STDIN with
cat, keeps fields 2 and 3 withcut(the delimiter is a space) and sorts the result.
$ cat salesdata.txt
stall1 apples 20 September 4
stall2 oranges 5 September 7
stall3 watermelons 12 September 10
stall4 peaches 7 September 15
$ cat salesdata.txt | ./summary.sh
Here is a summary of the sales data:
====================================
apples 20
oranges 5
peaches 7
watermelons 12Redirecting the file into the script gives the same result:
$ ./summary.sh < salesdata.txt
Here is a summary of the sales data:
====================================
apples 20
oranges 5
peaches 7
watermelons 12Which method should I use?
| Method | Best when |
|---|---|
| Command line arguments | most of the time: easy for users, kept in the command history, and easy for other scripts (or a timed job) to call |
read while the script runs | the data should not stay in anyone's command history, such as login details |
| STDIN | the script only processes data, so it can sit in a pipeline |
You can combine them, for example a file name as an argument and, if it is missing, the data from STDIN (Part 5 shows how). Think about three things: ease of use, security of sensitive data, and robustness: make the script hard to use wrongly.
Part 4: Arithmetic
There are several ways to do arithmetic in Bash. We cover them all, but the recommended one is the last, double parentheses. The common operators are the same in every method:
| Operator | Operation |
|---|---|
+, -, *, / | addition, subtraction, multiplication, division |
var++ | increase the variable var by 1 |
var-- | decrease the variable var by 1 |
% | modulus: the remainder after division |
let
let is built into Bash. Its first part is usually the variable the result is saved into: let a=5+4. Save as let_example.sh:
#!/bin/bash
# Basic arithmetic using let
let a=5+4
echo $a
let "a = 5 + 4"
echo $a
let a++
echo $a
let "a = 4 * 5"
echo $a
let "a = $1 + 30"
echo $aWorked example 8: let
Question. Predict the output of ./let_example.sh 15.
Solution.
$ ./let_example.sh 15
9
9
10
20
45- Line 3 is the basic form: without quotes the expression must have no spaces.
- Line 5 uses quotes, which allow spaces for readability.
- Line 7,
a++, increasesaby 1:9becomes10. - Line 11 uses another variable:
$1is15, soa = 15 + 30 = 45.
expr
expr is similar to let, but instead of saving the result in a variable it prints it. You must put spaces between the items, and you must not use quotes. It is common to put expr inside command substitution to save its output. Save as expr_example.sh:
#!/bin/bash
# Basic arithmetic using expr
expr 5 + 4
expr "5 + 4"
expr 5+4
expr 5 \* $1
expr 11 % 2
a=$( expr 10 - 3 )
echo $aWorked example 9: expr
Question. Predict the output of ./expr_example.sh 12.
Solution.
$ ./expr_example.sh 12
9
5 + 4
5+4
60
1
7- Line 3 is the basic form and prints
9. - Line 4: with quotes the expression is not calculated, just printed.
- Line 5: without spaces the expression is not calculated either.
- Line 6:
*has a special meaning for Bash, so it is escaped with a backslash:5 * 12 = 60. - Line 7: modulus, the remainder of
11divided by2, is1. - Line 8 saves the output of
exprinawith command substitution.
What goes wrong without the backslash? Before expr runs, Bash replaces an unescaped * with the names of the files in the current folder, so expr receives file names instead of an operator:
$ expr 5 * 4
expr: syntax error: unexpected argument ‘args.sh’
$ expr 5 \* 4
20The error names the first file, in alphabetical order, in your folder. If you created the scripts of this page in order, that is args.sh.
Double parentheses
The recommended method uses double parentheses, like command substitution with one more pair: $(( expression )) returns the result. Save as expansion2.sh; the ; separates two commands on one line:
#!/bin/bash
# Basic arithmetic using double parentheses
a=$(( 4 + 5 )); echo $a
a=$((3+5)); echo $a
b=$(( a + 3 )); echo $b
b=$(( $a + 4 )); echo $b
(( b++ )); echo $b
(( b += 3 )); echo $b
a=$(( 4 * 5 )); echo $aWorked example 10: double parentheses
Question. Predict the seven numbers ./expansion2.sh prints.
Solution.
$ ./expansion2.sh
9
8
11
12
13
16
20- Line 3 is the basic form; you may space the expression out freely without quotes.
- Line 4 shows that it works the same with no spaces at all.
- Line 5 uses a variable without its
$, line 6 with it; both work:8 + 3 = 11, then8 + 4 = 12. - Line 7,
(( b++ )), increasesbby 1 to13. This form changes the variable directly, so no$is needed before the brackets. - Line 8,
(( b += 3 )), is short forb = b + 3:16. - Line 9 multiplies without escaping the
*:20.
Double parentheses are flexible in how you format them, which is part of why they are preferred; being built into Bash, they also run slightly faster. The result is always a whole number: any fraction is dropped. The usual order of operations applies, and brackets change it:
$ echo $(( 7 / 2 )) $(( 7 % 2 ))
3 1
$ echo $(( 2 * 3 + 4 )) $(( 2 * (3 + 4) ))
10 14Another example: arithmetic step by step
Question. Save arith.sh and predict its four lines.
#!/bin/bash
# Calculate the sum
result=$((50+25))
# Print summation value
echo "sum = $result"
# Calculate the division
result=$((50/25))
# Print division value
echo "division = $result"
# Assign a value to N
N=10
# Doing pre-decrement
((--N))
# Print the value of N
echo "Value after decrement = $N"
# Using shorthand operator
(( N += 10 ))
# Print the value of N
echo "Value after adding 10 = $N"Solution.
$ ./arith.sh
sum = 75
division = 2
Value after decrement = 9
Value after adding 10 = 1950 + 25 = 75 and 50 / 25 = 2. ((--N)) decreases N by 1 before it is used: 10 becomes 9. Then (( N += 10 )) gives 19.
Length of a variable
This is not really arithmetic, but it is useful. This form gives the number of characters in a variable's value:
${#variable}Save as length_example.sh:
#!/bin/bash
# Show the length of a variable.
a='Hello World'
echo ${#a}
b=4953
echo ${#b}$ ./length_example.sh
11
4Hello World has 11 characters (the space counts), and 4953 has 4.
Part 5: If statements
if statements let a script make decisions: run a piece of code only when a condition is true. Their syntax is very specific, so pay attention to every detail.
The basic if
if [ some test ]
then
commands
fiAnything between then and fi (if backwards) runs only if the test in the square brackets is true. Save as if_example.sh:
#!/bin/bash
# Basic if statement
if [ $1 -gt 100 ]
then
echo Hey that\'s a large number.
pwd
fi
date- Line 3: is the first command line argument greater than 100?
- Lines 5 and 6 run only if that test is true. There can be as many commands here as you like. The backslash lets
echoprint the'. - Line 7,
fi, ends theif. Line 8 is outside it, sodateruns whatever the result.
$ ./if_example.sh 15
Fri Sep 25 12:13:04 PM UTC 2026
$ ./if_example.sh 150
Hey that's a large number.
/home/student/lab11
Fri Sep 25 12:13:05 PM UTC 2026Always test a script with input that covers each possible case, here a small and a large number.
The test command
The square brackets [ ] are really the command test. So every operator of test can be used inside them. Because [ is a command, it needs a space after it like any other command:
$ [5 -lt 12]
bash: [5: command not foundThe most common operators (the full list is in man test):
| Operator | True when |
|---|---|
! EXPRESSION | the EXPRESSION is false |
-n STRING | the length of STRING is greater than zero |
-z STRING | the length of STRING is zero (it is empty) |
STRING1 = STRING2 | STRING1 is equal to STRING2 |
STRING1 != STRING2 | STRING1 is not equal to STRING2 |
INTEGER1 -eq INTEGER2 | INTEGER1 is numerically equal to INTEGER2 |
INTEGER1 -gt INTEGER2 | INTEGER1 is numerically greater than INTEGER2 |
INTEGER1 -lt INTEGER2 | INTEGER1 is numerically less than INTEGER2 |
INTEGER1 -ge INTEGER2 | INTEGER1 is numerically greater than or equal to INTEGER2 |
INTEGER1 -le INTEGER2 | INTEGER1 is numerically less than or equal to INTEGER2 |
-d FILE | FILE exists and is a directory |
-e FILE | FILE exists |
-r FILE | FILE exists and the read permission is granted |
-s FILE | FILE exists and its size is greater than zero (it is not empty) |
-w FILE | FILE exists and the write permission is granted |
-x FILE | FILE exists and the execute permission is granted |
Here FILE means a path, absolute or relative, to a file or a directory. Each of these examples is true (exit status 0, explained next), except the last two:
$ [ 5 -eq 5 ]
$ echo $?
0
$ [ 9 -gt 5 ]
$ echo $?
0
$ [ 3 -lt 8 ]
$ echo $?
0
$ [ 7 -ge 7 ]
$ echo $?
0
$ [ 4 -le 6 ]
$ echo $?
0
$ x=hello
$ [ -n "$x" ]
$ echo $?
0
$ [ -z "$x" ]
$ echo $?
1
$ [ -d /etc ]
$ echo $?
0
$ [ -x myscript.sh ]
$ echo $?
0
$ [ -w /etc/passwd ]
$ echo $?
1$x is not empty, so -z is false; and a normal user may not write to /etc/passwd, so -w is false.
test and the exit status $?
test prints nothing. It answers through its exit status, which the variable $? holds right after the command: 0 means true (success), 1 means false (failure). Because [ ] is just test, you can try a test at the prompt before you put it in a script.
Worked example 11: = versus -eq
Question. Predict each echo $?:
$ test 001 = 1
$ test 001 -eq 1
$ touch myfile
$ test -s myfile
$ ls /etc > myfile
$ test -s myfileSolution.
$ test 001 = 1
$ echo $?
1
$ test 001 -eq 1
$ echo $?
0
$ touch myfile
$ test -s myfile
$ echo $?
1
$ ls /etc > myfile
$ test -s myfile
$ echo $?
0= is a string comparison: 001 and 1 are not the same characters, so it is false (1). -eq is a numeric comparison: 001 is the number 1, so it is true (0). touch creates an empty myfile, so "size greater than zero" is false; after ls /etc is redirected into it, the file has content and the test is true.
If a numeric operator gets something that is not a whole number, test prints an error and returns 2:
$ test abc -eq 1
bash: test: abc: integer expression expected
$ echo $?
2Try any pair of values with every operator in the interactive widget: open the test command widget full screen.
Indenting
In the scripts above, the commands inside the if are indented. Bash does not require it, and the script runs exactly the same without it, but indenting makes the structure easy to see and simple mistakes harder to make. It matters more and more as scripts grow.
if, else
To run one set of commands when the test is true and another when it is false, add else:
if [ some test ]
then
commands
else
other commands
fiSave as var.sh. It compares a number typed by the user with the variable b:
#!/bin/bash
echo "Enter a number"
read a
b=100
if [[ $a -eq $b ]]
then
echo "Numbers are equal"
else
echo "Numbers are not equal"
fi$ ./var.sh
Enter a number
56
Numbers are not equal
$ ./var.sh
Enter a number
100
Numbers are equalThis script uses double square brackets [[ ]]. They take the same operators, and you will meet them in many scripts.
else also lets a script take its data from a file or from STDIN. Save as else.sh: if exactly one argument is given, nl numbers the lines of that file; otherwise it numbers what arrives on STDIN.
#!/bin/bash
# else example
if [ $# -eq 1 ]
then
nl $1
else
nl /dev/stdin
fi$ ./else.sh salesdata.txt
1 stall1 apples 20 September 4
2 stall2 oranges 5 September 7
3 stall3 watermelons 12 September 10
4 stall4 peaches 7 September 15if, elif, else
When there are several possible paths, test them one after another with elif (else if):
if [ some test ]
then
commands
elif [ some other test ]
then
different commands
else
other commands
fiBash checks the tests in order and runs only the first branch whose test is true. You can have as many elif branches as you like, and the final else is optional.
Worked example 12: the party
If you are 18 or over you may go to the party. If you are not, but you have a letter from your parents (yes), you may go but must be back before midnight. Otherwise you cannot go. Save as if_elif.sh:
#!/bin/bash
# elif statements
if [ $1 -ge 18 ]
then
echo You may go to the party.
elif [ $2 == 'yes' ]
then
echo You may go to the party but be back before midnight.
else
echo You may not go to the party.
fiQuestion. Predict ./if_elif.sh 20 no, ./if_elif.sh 16 yes and ./if_elif.sh 16 no.
Solution.
$ ./if_elif.sh 20 no
You may go to the party.
$ ./if_elif.sh 16 yes
You may go to the party but be back before midnight.
$ ./if_elif.sh 16 no
You may not go to the party.20 -ge 18 is true, so the second argument is never checked. 16 fails the first test; then yes == 'yes' is true in the second run and false in the third, which falls through to else.
Another example: less than, greater than or equal
number.sh uses both kinds of comment from Part 1 and an if, elif, else chain:
#!/bin/bash
#Take a number as input
echo "Enter a number"
read a
: '
Check the input number is
less than 10 or greater than 10 or equal to 10
'
if [[ $a -lt 10 ]]
then
echo "The number is less than 10"
elif [[ $a -gt 10 ]]
then
echo "The number is greater than 10"
else
echo "The number is equal to 10"
fiQuestion. Predict the output for the inputs 3, 10 and 90.
Solution.
$ ./number.sh
Enter a number
3
The number is less than 10
$ ./number.sh
Enter a number
10
The number is equal to 10
$ ./number.sh
Enter a number
90
The number is greater than 103 -lt 10 is true, so the first branch runs. 10 is neither less nor greater than 10, so both tests fail and else runs. 90 -gt 10 makes the elif branch run.
Nested if statements
An if can sit inside another if. Indenting shows clearly which fi closes which if. Save as nested_if.sh:
#!/bin/bash
# Nested if statements
if [ $1 -gt 100 ]
then
echo Hey that\'s a large number.
if (( $1 % 2 == 0 ))
then
echo And is also an even number.
fi
fi- Line 3: continue only if the first argument is greater than 100.
- Line 6 is a variation: to test an arithmetic expression, use double parentheses, just as for arithmetic.
- Line 8 runs only if both tests are true.
Worked example 13: nested tests
Question. Predict the output for 150, 151 and 50.
Solution.
$ ./nested_if.sh 150
Hey that's a large number.
And is also an even number.
$ ./nested_if.sh 151
Hey that's a large number.
$ ./nested_if.sh 50150 is large and even (150 % 2 is 0). 151 is large but odd. 50 fails the outer test, so nothing at all is printed. You can nest as deep as you need, but more than 3 levels is a sign that the logic should be reorganised.
Boolean operations: && and ||
Sometimes an action should happen only if several conditions are true, or if any one of them is:
- and:
&&is true when both tests are true. - or:
||is true when at least one test is true.
With single brackets, each test gets its own brackets. Save as and.sh: the file given as an argument is "useful" only if it is readable and not empty.
#!/bin/bash
# and example
if [ -r $1 ] && [ -s $1 ]
then
echo This file is useful.
fi$ touch empty.txt
$ ./and.sh empty.txt
$ ./and.sh salesdata.txt
This file is useful.With double brackets [[ ]], the && can go inside the brackets. cond.sh checks that the age is at least 18 and the code is 1100:
#!/bin/bash
echo "Enter your code"
read code
echo "Enter your age"
read age
if [[ $age -ge 18 && $code -eq '1100' ]]
then
echo "You are eligible to see the movie"
else
echo "You are not eligible to see the movie"
fiWorked example 14: && with read
Question. Predict the result for code 1100 with age 5, code 1100 with age 45, and code 1200 with age 45.
Solution.
$ ./cond.sh
Enter your code
1100
Enter your age
5
You are not eligible to see the movie
$ ./cond.sh
Enter your code
1100
Enter your age
45
You are eligible to see the movie
$ ./cond.sh
Enter your code
1200
Enter your age
45
You are not eligible to see the movieOnly the second run passes both tests. The first fails the age test and the third fails the code test; with &&, one false test is enough to reach else.
case statements
When a variable must be matched against a list of patterns, a chain of if and elif becomes hard to read. A case statement is cleaner. Save as case2.sh:
#!/bin/bash
# case example
case $1 in
start) echo starting ;;
stop) echo stopping ;;
restart) echo restarting ;;
*) echo don\'t know ;;
esac- Line 3 starts the
caseon the value of$1. - Each pattern ends with
), followed by its commands, and each branch ends with;;. - The pattern
*matches anything: a catch-all for when no other pattern matches. esac(casebackwards) ends the statement.
$ ./case2.sh start
starting
$ ./case2.sh stop
stopping
$ ./case2.sh restart
restarting
$ ./case2.sh pause
don't knowThe exit status of a script
Every command, and every script, finishes with an exit status, and $? holds it. A script sets its own with exit: exit 0 for success, exit 1 for failure. Another script can then run it and test $?. Save as first.sh:
#!/bin/bash
echo "Enter a numeric value"
read n
# Check the input value is less than or equal to 100 or not
if [[ $n -le 100 ]]
then
exit 0
else
exit 1
fiThen save as second.sh; it runs first.sh and tests its exit status:
#!/bin/bash
#Execute the file, first.sh
bash "first.sh"
#Check the return code is equal to 1 or not
if [ $? -eq 1 ]
then
echo "The input number is greater than 100"
else
echo "The input number is less than or equal to 100"
fi$ ./second.sh
Enter a numeric value
55
The input number is less than or equal to 100
$ ./second.sh
Enter a numeric value
110
The input number is greater than 100second.sh runs first.sh with bash, so first.sh does not even need execute permission. first.sh exits with 0 for 55 and 1 for 110, and second.sh reads that value from $? straight after.
See it run, line by line
The step-through widget runs three small scripts (a grade from a mark, a calculator on two arguments, and an account check with read -s). Step one line at a time and watch the variables, the input and the terminal change; every [ ] test is explained as it runs. Its outputs match real Bash exactly. Open the step-through widget full screen.
Practice
About 30 minutes. Solve Practice 1 and 2 on paper first and then check them in your terminal. Practice 3 and 4 are scripts to write, save, make executable and test. Practice 5 is for those who finish early. The answers are at the end of the page.
Practice 1: predict the output (about 5 minutes)
Write down the five lines ./practice1.sh prints:
#!/bin/bash
a=7
b=3
echo '$a + $b'
echo "$a + $b"
echo $(( a + b )) $(( a / b )) $(( a % b ))
expr $a \* $b
let "c = a * b - 1"
echo $c ${#c}Practice 2: true or false? (about 5 minutes)
nofile.txt does not exist. Predict the value of echo $? after each command:
test 5 -lt 12test abc = ABCtest 010 -eq 10test 010 = 10test -z ""test -e nofile.txt
Practice 3: the larger number (about 8 minutes)
Write larger.sh. It takes two numbers as command line arguments and prints The larger number is N. If the two numbers are equal it prints Both numbers are N. Test it with ./larger.sh 17 42, ./larger.sh 42 17 and ./larger.sh 9 9.
Practice 4: a shop bill (about 10 minutes)
Write bill.sh:
- Ask with
read -pfor the price of one item and for the quantity. - Compute the total with
$(( ))and printTotal: T. - If the total is 500 or more, the customer gets a 10% discount: print
Discount: D(the total divided by 10) andTo pay: P. - Otherwise print
No discount, to pay: T.
Test it with price 120 and quantity 3, then with price 200 and quantity 3.
Practice 5 (if you finish early): what is this path?
Write filecheck.sh. It takes one path as a command line argument and prints one line:
- with no argument:
Usage:followed by the script name andpath - if the path does not exist:
PATH does not exist - if it is a directory:
PATH is a directory - if it is an executable file:
PATH is an executable file - otherwise:
PATH is a file, not executable
Test it with no argument, with nofile.txt, /etc, bill.sh and salesdata.txt.
Answers
Answer 1
$ ./practice1.sh
$a + $b
7 + 3
10 2 1
21
20 2- In single quotes nothing is substituted, so the first line is the literal text.
- In double quotes the values are substituted:
7 + 3, but nothing is calculated. 7 + 3 = 10;7 / 3is2because the fraction is dropped; the remainder7 % 3is1.exprwith the escaped*prints21.letcomputesc = 7 * 3 - 1 = 20, and the value20has 2 characters.
Answer 2
$ test 5 -lt 12
$ echo $?
0
$ test abc = ABC
$ echo $?
1
$ test 010 -eq 10
$ echo $?
0
$ test 010 = 10
$ echo $?
1
$ test -z ""
$ echo $?
0
$ test -e nofile.txt
$ echo $?
15 is less than 12 (true). abc and ABC are different strings, because case matters (false). -eq reads 010 as the number 10 (true), but = compares the text 010 with 10 (false). An empty string makes -z true. The file does not exist, so -e is false.
Answer 3
#!/bin/bash
# print the larger of two numbers
if [ $1 -gt $2 ]
then
echo "The larger number is $1"
elif [ $1 -lt $2 ]
then
echo "The larger number is $2"
else
echo "Both numbers are $1"
fi$ ./larger.sh 17 42
The larger number is 42
$ ./larger.sh 42 17
The larger number is 42
$ ./larger.sh 9 9
Both numbers are 9Answer 4
#!/bin/bash
# a shop bill with a discount
read -p "Price of one item: " price
read -p "Quantity: " qty
total=$(( price * qty ))
echo "Total: $total"
if [ $total -ge 500 ]
then
discount=$(( total / 10 ))
echo "Discount: $discount"
echo "To pay: $(( total - discount ))"
else
echo "No discount, to pay: $total"
fi$ ./bill.sh
Price of one item: 120
Quantity: 3
Total: 360
No discount, to pay: 360
$ ./bill.sh
Price of one item: 200
Quantity: 3
Total: 600
Discount: 60
To pay: 540120 * 3 = 360 is below 500, so there is no discount. 200 * 3 = 600: the discount is 600 / 10 = 60 and the customer pays 600 - 60 = 540.
Answer 5
#!/bin/bash
# report on one path given on the command line
if [ $# -eq 0 ]
then
echo "Usage: $0 path"
elif [ ! -e $1 ]
then
echo "$1 does not exist"
elif [ -d $1 ]
then
echo "$1 is a directory"
elif [ -x $1 ]
then
echo "$1 is an executable file"
else
echo "$1 is a file, not executable"
fi$ ./filecheck.sh
Usage: ./filecheck.sh path
$ ./filecheck.sh nofile.txt
nofile.txt does not exist
$ ./filecheck.sh /etc
/etc is a directory
$ ./filecheck.sh bill.sh
bill.sh is an executable file
$ ./filecheck.sh salesdata.txt
salesdata.txt is a file, not executableThe order of the tests matters: the script first checks that an argument was given ($#), then that the path exists (the ! turns "exists" into "does not exist"), and only then asks what kind of path it is. bill.sh was made executable with chmod 755; salesdata.txt was not.