Bash Scripting, Part 2: Loops and Functions
Last week you wrote your first Bash scripts: variables, input with read and $1, arithmetic with double brackets, and decisions with if. Every one of those scripts ran from top to bottom once. This week adds the two tools that make scripts really useful. Loops repeat a block of commands, once per number, per word, per file or per line. Functions give a block of commands a name, so you write it once and call it as often as you like. We finish with scripts that call other scripts and read each other's exit codes. The last half hour is yours: six problems, solved on paper first and then run in your own Ubuntu machine.
Every output on this page was produced by really running the script with bash 5.2 on Ubuntu 24.04, as a normal user called student, inside a folder called lab12. When a script reads from the keyboard, the value typed is shown on its own line, exactly as the terminal showed it.
Objectives
By the end of the section you should be able to:
- Repeat commands with a
whileloop and anuntilloop, and find an off by one error. - Walk through a list with
for: typed words, a variable, a range of numbers with a step, the C stylefor ((...)), a set of files, the command line arguments and an array. - Leave a loop early with
break, skip one pass withcontinue, read a file line by line withwhile read, and build a menu withselect. - Define and call a function, pass it arguments, and send back a return status with
returnand a value with command substitution$( ). - Explain variable scope: what
localchanges, and what a function can still change outside itself. - Run one script from another, read its exit code with
$?, and tellsource,bash,evalandexecapart.
Before you start: the lab folder
Open a terminal and make one folder for the whole lab:
mkdir ~/lab12
cd ~/lab12To try a script, save the listing in that folder with nano name.sh (or the Text Editor), then run it with bash name.sh. If you prefer ./name.sh, run chmod +x name.sh once first, as in Part 1.
Two small text files are used by the examples. Create them in the same folder:
hardware.txt
Monitor
Keyboard
Mouse
Scanner
Printermyfile.txt
Tomato
Lettuce
CapsicumPart 1: while and until
What is a loop?
A loop takes a series of commands and keeps re-running them until a particular situation is reached. Loops automate repetitive tasks: the commands are written once, and the loop runs them as many times as needed. Bash has three basic loop structures, and every loop body sits between the keywords do and done.
| Loop | Runs its body |
|---|---|
while | while a test is true |
until | until a test becomes true (so, while it is false) |
for | once for each item in a list |
while
A while loop says: while an expression is true, keep executing these lines of code. The test goes between square brackets, exactly like the test of an if statement:
while [ some test ]
do
commands
doneThis script prints the numbers 1 to 10:
while_loop.sh
#!/bin/bash
# Basic while loop
counter=1
while [ $counter -le 10 ]
do
echo $counter
((counter++))
done
echo All done- Line 3 gives the variable
counterits starting value. - Line 4: while the test is true (
counteris less than or equal to 10), run the commands betweendoanddone. - Line 6 prints the counter; any commands can go here.
- Line 7 uses double brackets to add 1 to
counter. - Line 8 is the bottom of the loop, so Bash goes back to line 4 and performs the test again. If the test is true it runs the body again; if it is false it continues with the commands after
done.
$ bash while_loop.sh
1
2
3
4
5
6
7
8
9
10
All doneWorked example 1: the off by one error
Change only the test of while_loop.sh from -le (less than or equal) to -lt (less than). Predict the output before reading on.
while [ $counter -lt 10 ]$ bash while_lt.sh
1
2
3
4
5
6
7
8
9
All doneThe loop now stops at 9. When counter is 10 the test 10 -lt 10 is false, so the body never runs for 10. This mistake is called an off by one error: easy to make, and easy to fix once you have seen it. Check the first and the last pass of every loop you write.
until
The until loop is written almost exactly like while (just replace the word). The difference is that it executes the commands until the test becomes true:
until [ some test ]
do
commands
doneuntil_loop.sh
#!/bin/bash
# Basic until loop
counter=1
until [ $counter -gt 10 ]
do
echo $counter
((counter++))
done
echo All done$ bash until_loop.sh
1
2
3
4
5
6
7
8
9
10
All doneThe output is identical to while_loop.sh: the test was simply turned around, from "while counter is at most 10" to "until counter is greater than 10". A while loop could handle every case on its own; until exists because some conditions read better that way. "Leave the towel on the line until it is dry" is clearer than "leave the towel on the line while it is not dry". Pick the loop that makes your code the easiest to read.
Worked example 2: even numbers divisible by 5
This script from the examples document prints the numbers from 1 to 20 that are even and divisible by 5. Predict its output.
while.sh
#!/bin/bash
# Print the message
echo "Print the numbers which are even and divisible by 5"
# Initialize the counter
counter=1
# Iterate the loop while the $counter value is less than or equal to 20
while [ $counter -le 20 ]
do
# Check the $counter is divisible by 2 and 5
if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
then
echo "$counter"
fi
# Increment $counter by 1
((counter++))
done
echo "Done"$ bash while.sh
Print the numbers which are even and divisible by 5
10
20
Done- The loop runs 20 times, for
counter= 1 to 20. - Inside
[[ ]], the sides of-eqare calculated before they are compared, so$counter%2is the remainder after dividing by 2. A remainder of 0 means "divisible". &&joins the two tests: both must be true. Only 10 and 20 pass both.
Worked example 3: counting down with until
The next script from the examples document starts at 20 and subtracts 1 on every pass. Its comment says it prints odd numbers. Predict the first and the last number printed.
until.sh
#!/bin/bash
# Initialize the variable, n
n=20
# Repeat the loop until the value of $n is less than 0
until [ $n -lt 0 ]
do
# Check the value of n is odd
if [[ $n%2 -gt 0 ]]
then
echo $n
fi
# Decrement the value of n by 1
((n=$n-1))
done$ bash until.sh
19
17
15
13
11
9
7
5
3
1ngoes 20, 19, 18, ..., 1, 0. After the pass withn= 0,nbecomes -1, the test-1 -lt 0is true, anduntilstops.$n%2 -gt 0is true only when the remainder is 1, so only odd numbers are printed: 19 first (20 is even), 1 last (0 is even).((n=$n-1))is another way of writing the decrement;((n--))does the same.
Interactive: the loop visualizer
Open the loop visualizer full screen. Choose while, until, a list, a brace range or the C style for, set the start, the test, the limit and the step, then press Step or Play: the current line is highlighted, the flow chart shows whether Bash is testing, running the body or updating the counter, and the output appears line by line. Try a step that moves away from the limit to see an endless loop. Every script the widget can build was run with real Bash, and all 965 outputs match.
Part 2: for loops
for over a list
The for loop is a little different from the other two. It says: for each of the items in a given list, perform the given set of commands.
for var in list
do
commands
doneIt takes each item of the list in order, assigns it to the variable var, runs the commands between do and done, then goes back to the top, takes the next item and repeats. The list is a series of strings separated by spaces. The loop ends when the list is used up.
Worked example 4: a week of plans
The examples document walks through the days of the week and prints a plan for each. Predict the lines for Tuesday and Sunday.
for.sh
#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
# Check the weekday name is Monday or Thursday
if [[ $day == 'Monday' || $day == 'Thursday' ]]
then
echo "Meeting on $day at 9:30 am"
# Check the weekday name is Tuesday or Wednesday or Friday
elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
then
echo "Training on $day at 11:00 am"
else
# Print 'Holiday' for other days
echo "$day is Holiday"
fi
done$ bash for.sh
Meeting on Monday at 9:30 am
Training on Tuesday at 11:00 am
Training on Wednesday at 11:00 am
Meeting on Thursday at 9:30 am
Training on Friday at 11:00 am
Saturday is Holiday
Sunday is HolidayThe same idea reads a sentence word by word: typed after in, the sentence is split at its spaces and every word becomes one pass.
readstr.sh
#!/bin/bash
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
echo $value
done$ bash readstr.sh
Bash
Programming
for
the
BeginnersWorked example 5: a list stored in a variable
The list can come from a variable. Predict the output of both scripts; the only difference is the pair of double quotes on line 4.
for_loop.sh
#!/bin/bash
# Basic for loop
planets='Mercury Venus Earth'
for planet in $planets
do
echo $planet
done
echo All donefor_quoted.sh
#!/bin/bash
# The same list, but the variable is quoted
planets='Mercury Venus Earth'
for planet in "$planets"
do
echo $planet
done
echo All done$ bash for_loop.sh
Mercury
Venus
Earth
All done
$ bash for_quoted.sh
Mercury Venus Earth
All doneWithout quotes, the value of $planets is split at its spaces into three items, so the body runs three times. With quotes, the whole string is one item, so the body runs once and prints the whole string.
Ranges of numbers
A for loop can also process a series of numbers. Two dots between two numbers inside curly brackets make a range:
for_loop_series.sh
#!/bin/bash
# Basic range in for loop
for value in {1..5}
do
echo $value
done
echo All done$ bash for_loop_series.sh
1
2
3
4
5
All doneThere must be no spaces inside the curly brackets. With spaces, the range is not seen as a range but as a list of three items:
for value in { 1..5 }$ bash for_loop_spaces.sh
{
1..5
}
All doneThe start and the end can be any numbers. If the first is larger than the second, the range counts down. Two more dots and a number set the step, the value to increase or decrease by each time:
for_loop_stepping.sh
#!/bin/bash
# Basic range with steps for loop
for value in {10..0..2}
do
echo $value
done
echo All done$ bash for_loop_stepping.sh
10
8
6
4
2
0
All doneWorked example 6: predict the range
Without running anything, write down what each loop prints (one number per line), then check with the loop visualizer, which uses the same ranges.
| Range | Printed |
|---|---|
1..10..3 inside curly brackets | 1, 4, 7, 10 |
10..0..3 inside curly brackets | 10, 7, 4, 1 |
0..10..2 inside curly brackets | 0, 2, 4, 6, 8, 10 |
The end value is printed only when a step lands on it exactly: 10 minus three steps of 3 gives 1, and the next step, -2, is past 0.
The C style for loop
A second syntax writes the start, the test and the change in one line, between double brackets and separated by semicolons:
for_loop_alternate_syntax.sh
#!/bin/bash
# Basic range with alternate syntax
for ((num = 1; num <= 5; num++))
do
echo $num
done
echo All done$ bash for_loop_alternate_syntax.sh
1
2
3
4
5
All donenum = 1runs once, before the loop starts.- The middle part, "num less than or equal to 5", is the test, checked before every pass, like the test of a
whileloop. num++runs at the end of every pass.
Processing a set of files
One of the more useful applications of for loops is processing a set of files, using a wildcard. The following script makes a .php copy of every .html file in the folder given as the first argument:
copy_html.sh
#!/bin/bash
# Make a php copy of any html files
for value in $1/*.html
do
cp $value $1/$( basename -s .html $value ).php
done$1/*.htmlexpands to the list of.htmlfiles in that folder, so each pass gets one file name.basename -s .html site/about.htmlprintsabout: the name without the folder and without the suffix.$( )puts that output into the command line, so the copy is calledsite/about.php.
$ ls site
about.html index.html
$ bash copy_html.sh site
$ ls site
about.html about.php index.html index.phpLooping over the command line arguments
"$@" is the list of all the arguments given to the script, one item per argument. The examples document uses it to read arguments written as name=value pairs:
cl2.sh
#!/bin/bash
# Print the argument variables
echo "Argument values are: $1 $2"
# Read each argument separately using for loop
for arg in "$@"
do
# Separate argument name and value
key=$(echo $arg | cut -f1 -d=)
value=$(echo $arg | cut -f2 -d=)
# Print message based on the argument's name
case $key in
id) echo "Student ID = $value";;
mark) echo "Obtained mark = $value";;
*)
esac
done$ bash cl2.sh id=2026101 mark=90
Argument values are: id=2026101 mark=90
Student ID = 2026101
Obtained mark = 90- Each pass takes one argument, for example
id=2026101. cut -f1 -d=keeps the part before the=(the name),cut -f2 -d=the part after it (the value).casecompares the name with each pattern and runs the matching line;*)matches anything else and does nothing here.
Splitting a string into an array
The examples document splits a sentence typed by the user into the elements of an array, then loops over the array:
split.sh
#!/bin/bash
# Input a string value
echo "Enter a string value"
read text
# Set the delimiter
IFS=' '
# Split the value of $text into an array based on space delimiter
read -a arr <<< "$text"
# Print each value of the array
for value in "${arr[@]}"
do
printf "$value\n"
done$ bash split.sh
Enter a string value
Learn Bash programming
Learn
Bash
programmingIFS(the internal field separator) sets the delimiter, here a space.read -a arrreads the words of the text into the arrayarr; the three less-than signs feed the value of$texttoreadas its input.- The for loop list written with
arr[@]means "every element of the array", one element per pass.
Part 3: Controlling loops
Most of the time a loop runs through in a smooth and orderly manner. Sometimes we need to intervene, and two statements do that: break and continue.
break
break tells Bash to leave the loop straight away. There may be a normal situation that ends the loop and an exceptional one that should end it early. This script searches the range 22 to 30 and stops at the first number divisible by 7:
break_demo.sh
#!/bin/bash
# Stop at the first number divisible by 7
for value in {22..30}
do
if (( value % 7 == 0 ))
then
echo "found $value"
break
fi
echo "checked $value"
done
echo All done$ bash break_demo.sh
checked 22
checked 23
checked 24
checked 25
checked 26
checked 27
found 28
All doneThe numbers 29 and 30 are never checked: after break, Bash continues with the first command after done.
continue
continue tells Bash to stop running this pass of the loop and begin the next one. This script skips the multiples of 3:
continue_demo.sh
#!/bin/bash
# Skip the multiples of 3
for n in {1..10}
do
if (( n % 3 == 0 ))
then
continue
fi
echo -n "$n "
done
echo
echo All done$ bash continue_demo.sh
1 2 4 5 7 8 10
All doneecho -n prints without a newline, so the numbers stay on one line; the lone echo after the loop ends that line.
A real use: back up the files of a folder, but skip any file we are not allowed to read instead of failing on it. First make a folder notes with three files and an empty folder backup, then remove all the permissions of notes/secret.txt with chmod 000:
mkdir notes backup
echo "first note" > notes/a.txt
echo "second note" > notes/b.txt
echo "my password" > notes/secret.txt
chmod 000 notes/secret.txtcopy_check.sh
#!/bin/bash
# Back up every file of the folder $1, skipping unreadable ones
for value in $1/*
do
if [ ! -r $value ]
then
echo $value not readable 1>&2
continue
fi
cp $value backup/
echo "backed up $value"
done$ ls notes
a.txt b.txt secret.txt
$ bash copy_check.sh notes
backed up notes/a.txt
backed up notes/b.txt
notes/secret.txt not readable
$ ls backup
a.txt b.txt[ ! -r $value ]is true when the file is not readable.1>&2sends the message to the error output instead of the normal output; on the terminal both appear.
Reading a file line by line
cat reads a whole file at once. A while loop with read reads it one line per pass: the file is redirected into the loop after done, and read line takes the next line on every pass. When there are no lines left, read fails and the loop ends.
readfile.sh
#!/bin/bash
echo "Reading file using cat command"
# Read the content of the file using `cat` command
content=`cat hardware.txt`
echo $content
echo "Reading file line by line using loop"
# Assign the filename
filename='hardware.txt'
# Each line of the file will be read by each iteration of the loop
while read line
do
# print the line
echo $line
done < $filename$ cat hardware.txt
Monitor
Keyboard
Mouse
Scanner
Printer
$ bash readfile.sh
Reading file using cat command
Monitor Keyboard Mouse Scanner Printer
Reading file line by line using loop
Monitor
Keyboard
Mouse
Scanner
PrinterThe first method prints the five words on one line: $content holds the whole file, and echo $content without quotes splits it into words and joins them with single spaces. The loop keeps every line separate.
Menus with select
select builds a simple menu. It shows every item of the list with a number, then prints a prompt. When the user types a number, the matching item goes into the variable and the commands between do and done run; then the prompt appears again.
select_example.sh
#!/bin/bash
# A simple menu system
planets='Mars Venus Earth Quit'
PS3='Select planet: '
select planet in $planets
do
if [ "$planet" == 'Quit' ]
then
break
fi
echo Hello $planet
done
echo Bye$ bash select_example.sh
1) Mars
2) Venus
3) Earth
4) Quit
Select planet: 2
Hello Venus
Select planet: 1
Hello Mars
Select planet: 5
Hello
Select planet: 4
ByePS3is the prompt thatselectprints. Without it, the prompt is#?.selectdoes no error checking:5matches no item, soplanetis empty and the script printsHellowith nothing after it.- Pressing Enter without a number shows the list again. The loop ends on
breakor when the input ends. "$planet"is quoted on purpose. Without the quotes, an emptyplanetleaves the test with nothing on its left side, and the same input5produces this error beforeHello:
select_unquoted.sh: line 7: [: ==: unary operator expectedThe examples document builds a language menu the same way, without PS3, and uses exit 0 to end the whole script:
menu.sh
#!/bin/bash
# Print message for the user
echo "Select your favorite language"
# Define the list of a menu item
select language in C# Java PHP Python Bash Exit
do
# Print the selected value
if [[ $language == "Exit" ]]
then
exit 0
else
echo "Selected language is $language"
fi
done$ bash menu.sh
Select your favorite language
1) C#
2) Java
3) PHP
4) Python
5) Bash
6) Exit
#? 3
Selected language is PHP
#? 6Part 4: Functions
What is a function?
A function is a small script within a script: a chunk of code with a name, which you may call as many times as you like. Functions are useful when a task has to be performed several times. Instead of writing the same code over and over, you write it once in a function and call the function every time.
A function can be written in two formats. Both work the same way; the choice is personal preference.
function_name () {
commands
}function function_name {
commands
}- In other languages the arguments are listed inside the round brackets. In Bash the brackets are there only for decoration: never put anything inside them.
- The definition of the function must appear in the script before any call to it.
- Pick descriptive names, so it is obvious what task each function serves.
function_example.sh
#!/bin/bash
# Basic function
print_something () {
echo Hello I am a function
}
print_something
print_something$ bash function_example.sh
Hello I am a function
Hello I am a functionLines 3 to 5 only define the function; nothing is printed while Bash reads them. Each of the last two lines calls it by writing its name, like a command.
Worked example 7: calling before defining
This script calls print_something once before the definition and once after it. Predict both lines of output.
call_before.sh
#!/bin/bash
# Calling a function before it is defined
print_something
print_something () {
echo Hello I am a function
}
print_something$ bash call_before.sh
call_before.sh: line 3: print_something: command not found
Hello I am a functionOn line 3 Bash has not yet read the definition, so print_something is just an unknown command. The script does not stop: it reads the definition and the second call works.
Passing arguments
Data is sent to a function the same way command line arguments are sent to a script: written directly after the function name. Inside the function they are $1, $2, and so on.
arguments_example.sh
#!/bin/bash
# Passing arguments to a function
print_something () {
echo Hello $1
}
print_something Mars
print_something Jupiter$ bash arguments_example.sh
Hello Mars
Hello JupiterEach call gets its own $1: Mars in the first call and Jupiter in the second.
Return status
In most languages a function can send a value back to the place that called it. Bash functions cannot. They can set a return status instead, just as a program or a command exits with an exit status that says whether it succeeded. The keyword is return, and the variable $? holds the status of the command or function that ran last.
return_status_example.sh
#!/bin/bash
# Setting a return status for a function
print_something () {
echo Hello $1
return 5
}
print_something Mars
print_something Jupiter
echo The previous function has a return value of $?$ bash return_status_example.sh
Hello Mars
Hello Jupiter
The previous function has a return value of 5- A status of 0 means everything went successfully; a non-zero status means an error occurred.
- The status does not have to be a fixed number; it can be a variable.
$?changes after every command. Read it immediately after the call, or save it in a variable.
Returning a value with command substitution
To get a real value back, have the function print the result, and only the result, and catch the printed text with command substitution $( ):
return_hack.sh
#!/bin/bash
# Setting a return value to a function
lines_in_file () {
cat $1 | wc -l
}
num_lines=$( lines_in_file $1 )
echo The file $1 has $num_lines lines in it.$ cat myfile.txt
Tomato
Lettuce
Capsicum
$ bash return_hack.sh myfile.txt
The file myfile.txt has 3 lines in it.- Line 4 prints the number of lines in the file whose name is in
$1. - Line 6 runs the function inside
$( ), so what it prints is not shown on the screen: it becomes the value ofnum_lines. - Inside the function,
$1is the function's argument. On line 6,$1is the script's argument,myfile.txt, which is passed on to the function.
Be careful: if you call the function without $( ), it prints its result on the screen and num_lines stays empty. The call stack widget below shows both versions; the second one really prints 3 and then The file myfile.txt has lines in it.
Variable scope: local and global
Scope decides which parts of a script can see which variables. By default a variable is global: it is visible everywhere in the script, including inside functions. The keyword local, written in front of a variable the first time it is set inside a function, makes it local: visible only inside that function.
local var_name=var_valueWorked example 8: local versus global
Predict the three lines this script prints.
local_variables.sh
#!/bin/bash
# Experimenting with variable scope
var_change () {
local var1='local 1'
echo Inside function: var1 is $var1 : var2 is $var2
var1='changed again'
var2='2 changed again'
}
var1='global 1'
var2='global 2'
echo Before function call: var1 is $var1 : var2 is $var2
var_change
echo After function call: var1 is $var1 : var2 is $var2$ bash local_variables.sh
Before function call: var1 is global 1 : var2 is global 2
Inside function: var1 is local 1 : var2 is global 2
After function call: var1 is global 1 : var2 is 2 changed again| Line | var1 | var2 |
|---|---|---|
| Before the call | the global, global 1 | the global, global 2 |
| Inside the function | the local var1, local 1 | still the global, global 2 |
| After the call | the global, never touched: global 1 | the global, changed inside: 2 changed again |
local var1creates a second, localvar1. While the function runs, it hides the global one;var1='changed again'changes the local copy.var2has no local copy, sovar2='2 changed again'inside the function changes the globalvar2.- When the function ends, the local
var1disappears and the global one is visible again, unchanged.
Remove the word local and the last line becomes After function call: var1 is changed again : var2 is 2 changed again: now the function overwrites both globals. This is why functions should use local variables, so they cannot accidentally change a variable of the same name elsewhere in the script. Use global variables only as a last resort.
Interactive: the call stack
Open the function call stack full screen. It runs four scripts from this part one line at a time: local_variables.sh (switch local on and off), return_status_example.sh (choose the value after return), return_hack.sh (with $( ) or as a plain call) and the ls wrapper below (with and without command). Watch a frame being pushed on the call stack for every call, with its own $1 and local variables, the global variables it hides or changes, the value of $?, and where the printed text goes. All nine versions were run with real Bash and the widget shows exactly what they printed.
Three functions from the examples document
func.sh declares three functions: one prints a message, one stores a string in a global variable for the caller to read, and one reads its argument into a local variable and calculates the area of a circle with bc, the calculator that handles decimals.
func.sh
#!/bin/bash
# Declare a simple function
function print_message()
{
echo "Bash programming for beginner"
}
# Declare a function to return a string value
function ret_strdata()
{
# Initialize the variable with string value
return_str="Learn bash programming with functions"
}
# Declare a function to read argument value
function calculate_area()
{
# Read the passed argument value
local radius=$1
area=$(echo $radius*$radius*3.14 | bc)
# Print the area value
echo "Area of the circle is $area"
}
# Call the function to print a simple message
print_message
# Call the function that will assign a string value in a variable
ret_strdata
# Print the value of the variable
echo $return_str
# Read the radius value
echo "Enter the radius value"
read rad
# Call the function with radius value
calculate_area $rad$ bash func.sh
Bash programming for beginner
Learn bash programming with functions
Enter the radius value
4
Area of the circle is 50.24- The definitions use the keyword
functionand round brackets: Bash accepts that too. ret_strdata"returns" a string the global way: it setsreturn_str, and the caller reads it after the call.- The area is
4*4*3.14 = 50.24. Double brackets work with whole numbers only, which is whybcdoes this calculation. Ifbcis missing on your machine, install it withsudo apt install bc.
Overriding a command
A function may have the same name as a command. That makes a wrapper: for example, every ls in the script should really run ls -lh.
override.sh
#!/bin/bash
# Create a wrapper around the command ls
ls () {
command ls -lh
}
ls$ bash override.sh
total 4.0K
-rwxr-xr-x 1 student student 85 Sep 25 09:00 override.shA function normally takes precedence over a command with the same name. Inside the function, the keyword command says "run the command ls, not the function". Without it, ls -lh inside the function would call the function ls again, which would call it again, and so on. Every call adds a frame to the call stack and none of them ever returns, so the stack fills up. Run that way, the script printed nothing for a few seconds and then crashed with Segmentation fault (core dumped) and exit code 139.
Designing functions
- A task that must be performed several times is a good candidate for a function.
- Ancillary tasks, such as checking that a file exists and is readable, can go in their own functions, logically separate from the main part of the script.
- A function is most reusable when it performs one task only. Find the balance: functions that do too much lose the benefit, and too many tiny functions make the code grow without reason.
Part 5: Scripts working together
Exit codes between scripts
A script ends with an exit code, just as a function returns a status. exit 0 means success and any other number an error. A second script can run the first and read its code with $?.
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
fisecond.sh
#!/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$ bash second.sh
Enter a numeric value
55
The input number is less than or equal to 100
$ bash second.sh
Enter a numeric value
110
The input number is greater than 100first.sh prints no result at all: its only answer is the exit code, 0 for 55 and 1 for 110. second.sh tests $? straight after the bash "first.sh" line, before any other command can replace it.
Calling other scripts: source, bash, eval, exec
The examples document calls four small scripts in four different ways. Save them next to callpro.sh:
add.sh
#!/bin/bash
a=60
b=40
((result=$a+$b))
echo "The addition of $a+$b=$result"subtract.sh
#!/bin/bash
a=$1
b=$2
((result=$a-$b))
echo "The subtraction of $a-$b=$result"multiply.sh
#!/bin/bash
((result=$1*$2))
echo "The multiplication of $1 and $2 is $result"divide.sh
#!/bin/bash
a=$1
b=2
((result=$a/$b))
echo "The division of $a by $b is $result"exec starts divide.sh directly as a program, not through bash, so the file must be executable. Run this once, or the last call fails with Permission denied:
chmod +x divide.shcallpro.sh (two echo lines added to show what each call leaves behind, and a last line that never runs):
#!/bin/bash
script1="add.sh"
script2="subtract.sh"
script3="multiply.sh"
script4="/home/student/lab12/divide.sh"
source "$script1"
echo "After source, result is $result"
bash $script2 50 20
echo "After bash, result is still $result"
echo "Enter the value of a"
read a
echo "Enter the value of b"
read b
eval bash $script3 $a $b
exec $script4 30
echo "This line never runs"$ bash callpro.sh
The addition of 60+40=100
After source, result is 100
The subtraction of 50-20=30
After bash, result is still 100
Enter the value of a
6
Enter the value of b
7
The multiplication of 6 and 7 is 42
The division of 30 by 2 is 15| Call | What happens |
|---|---|
source add.sh | runs add.sh inside the current script, so its variable result (100) is still there afterwards |
bash subtract.sh 50 20 | starts a new Bash for the script; its result (30) disappears with it, so ours is still 100 |
eval bash multiply.sh $a $b | builds the command line from its arguments (here with 6 and 7) and runs it |
exec divide.sh 30 | replaces the current script with divide.sh; nothing after exec runs |
Practice
About 30 minutes. For each problem, write your answer on paper first, then type the script in ~/lab12 and run it to check yourself. Use only what this lab and Part 1 covered.
Practice 1: predict two loops
Write down exactly what this script prints (about 4 minutes).
p1.sh
#!/bin/bash
counter=10
until [ $counter -lt 4 ]
do
echo -n "$counter "
((counter-=3))
done
echo
for value in {15..3..4}
do
echo -n "$value "
done
echoPractice 2: even or odd
Write even_odd.sh, a script that prints the numbers 1 to 10, each on its own line, followed by is even or is odd (about 5 minutes). For example, the first two lines are 1 is odd and 2 is even.
Practice 3: a grade function
Write grade.sh (about 6 minutes):
- A function
gradethat reads its argument into a local variablemarkand printsAwhen the mark is at least 85,Bwhen it is at least 65, andCotherwise. - A for loop over the marks
92 70 40 85that catches each letter with$( )in a variablegand prints a line such asmark 92 gets A.
Practice 4: predict the scope
Write down the one line this script prints (about 4 minutes).
scope.sh
#!/bin/bash
count=0
add () {
local step=$1
count=$(( count + step ))
step=100
}
step=7
add 5
add 3
echo "count=$count step=$step"Practice 5: a validity check with return
Write valid.sh (about 6 minutes). A function valid returns status 0 when its argument is a mark from 0 to 100 and status 1 otherwise. A for loop calls it for 45 120 100 -3, tests $? after each call, and prints 45 ok or 120 invalid, and so on.
Practice 6: number the lines of a file
Write number_lines.sh, which reads hardware.txt line by line with while read, prints each line after its number (1: Monitor), and finishes with the total, Total: 5 lines (about 5 minutes).
Answers
Practice 1
$ bash p1.sh
10 7 4
15 11 7 3 until:counteris 10, 7, 4, then 1. The test1 -lt 4is true, so the loop stops before printing 1.- The range starts at 15 and counts down by 4, because 15 is larger than 3: 15, 11, 7, 3. The end value 3 is reached exactly, so it is printed.
- Each
echo -nleaves a space after the number and no newline; the loneechoends the line.
Practice 2
#!/bin/bash
# Print 1 to 10 and whether each number is even or odd
for n in {1..10}
do
if [[ $n%2 -eq 0 ]]
then
echo "$n is even"
else
echo "$n is odd"
fi
done$ bash even_odd.sh
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 is even
7 is odd
8 is even
9 is odd
10 is evenA while loop with a counter from 1 to 10, or a C style for loop from 1 to 10, gives the same output.
Practice 3
#!/bin/bash
# Print the grade letter of a mark
grade () {
local mark=$1
if [ $mark -ge 85 ]
then
echo A
elif [ $mark -ge 65 ]
then
echo B
else
echo C
fi
}
for m in 92 70 40 85
do
g=$( grade $m )
echo "mark $m gets $g"
done$ bash grade.sh
mark 92 gets A
mark 70 gets B
mark 40 gets C
mark 85 gets A85 gets A because the test is -ge (at least). With -gt it would get B: another off by one.
Practice 4
$ bash scope.sh
count=8 step=7counthas no local copy, so both calls change the global: 0 + 5 = 5, then 5 + 3 = 8.stepis local insideadd, sostep=100changes only the local copy. The globalstepis still 7.
Practice 5
#!/bin/bash
# Return status 0 for a mark from 0 to 100, 1 otherwise
valid () {
if [[ $1 -ge 0 && $1 -le 100 ]]
then
return 0
else
return 1
fi
}
for m in 45 120 100 -3
do
valid $m
if [ $? -eq 0 ]
then
echo "$m ok"
else
echo "$m invalid"
fi
done$ bash valid.sh
45 ok
120 invalid
100 ok
-3 invalid$? is tested on the line right after valid $m, so it still holds the function's status.
Practice 6
#!/bin/bash
# Number the lines of a file
n=0
while read line
do
((n++))
echo "$n: $line"
done < hardware.txt
echo "Total: $n lines"$ bash number_lines.sh
1: Monitor
2: Keyboard
3: Mouse
4: Scanner
5: Printer
Total: 5 linesn counts the passes of the loop, one per line, so after done it holds the number of lines.