Bash Scripting, Part 2
By the end of this lesson you will be able to write Bash scripts that repeat work with loops, organize logic into functions, call one script from another, build interactive menus, manipulate strings, and read or write files through redirection and pipes.
Objectives
- Repeat work with
forover a word list,whilewith a counter, anduntil; update counters with(( )). - Write functions in both declaration forms, pass arguments, keep variables
local, and hand a result back through a global variable. - Run one script from another with
source,bash,evalandexec, and know how each differs. - Build a menu with
select, process command-line arguments withcase, and handle strings withIFS,read -a,${var^^}andtr. - Read a file line by line, write with
>and>>, and chainsort,grepandwcwith pipes.
Prerequisites
- The previous lesson on Bash scripting: the shebang line, making a script executable, variables,
read,$1/$2,$?,exit,$(( ))arithmetic,bc, andif/elif/elsewith[[ ]]. - A Linux terminal (a local install, a virtual machine, or WSL).
- The
bccalculator, needed for one function example. If it is missing, install it with your package manager. - A single working directory for all the scripts in this lesson, since one task's script calls another task's helper scripts by name.
"Create the script" below means: paste the listing into a text editor, save it with a .sh extension, then run it with bash name.sh (or make it executable once and run ./name.sh afterwards). Two short steps in this lesson deliberately repeat scripts from the previous lesson: they are review, placed where this material builds on them.
Concept 1: Loops
Definition. A loop repeats a block of commands. Bash has three loop keywords, for, while and until, and every loop body sits between do and done.
Purpose. Any work that must happen once per item (each day of the week, each word of a sentence, each number from 1 to 20) is written once inside the body and the loop runs it as many times as needed.
How it works. for takes the next word from a list on every pass and stops when the list is used up. while and until test a condition before every pass:
flowchart TD
A[Start] --> B{Condition}
B -- "while: true / until: false" --> C[Run the body between do and done]
C --> D["Update the counter, for example counter++"]
D --> B
B -- "while: false / until: true" --> E[Continue after done]The only difference between while and until: while runs as long as the test succeeds, until as long as it fails.
for over a word list
The most common form walks through a list of words, one per iteration:
#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
if [[ $day == 'Monday' || $day == 'Thursday' ]]
then
echo "Meeting on $day at 9:30 am"
elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
then
echo "Training on $day at 11:00 am"
else
echo "$day is Holiday"
fi
donefor day in ...:dayis the loop variable. On each pass it takes the next word from the list afterin.do/done: the body. Everything between them runs once per word.- The
if/elif/elseinside the body compares the current$daywith string values, so each day gets its own message.
for over the words of a sentence
The list after in does not have to be a set of separate names. A sentence typed there is split on its spaces, and each word becomes one iteration, which is how a for loop reads string data word by word:
#!/bin/bash
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
echo $value
donefor value in Bash Programming for the Beginners: the five words form the list;valuereceives them one at a time.echo $value: prints the current word on its own line, so the output isBash,Programming,for,the,Beginners, one per line.
while with a counter
#!/bin/bash
echo "Print the numbers which are even and divisible by 5"
counter=1
while [ $counter -le 20 ]
do
if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
then
echo "$counter"
fi
((counter++))
done
echo "Done"counter=1: the loop variable must be set before the loop, or the test fails immediately.while [ $counter -le 20 ]: the body runs whilecounteris less than or equal to 20.[[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]:-eqinside[[ ]]evaluates both sides as arithmetic, so$counter%2becomes, for example,10%2, which is 0.%is the remainder operator.((counter++)): adds 1 tocounter. Forgetting this line gives an infinite loop; pressCtrl+Cif that happens.
until
#!/bin/bash
n=20
until [ $n -lt 0 ]
do
if [[ $n%2 -gt 0 ]]
then
echo $n
fi
((n=$n-1))
doneuntil [ $n -lt 0 ]: the body runs untilnbecomes less than 0, in other words whilenis 0 or more.[[ $n%2 -gt 0 ]]: a remainder greater than 0 meansnis odd, so the script prints the odd numbers counting down from 19 to 1.((n=$n-1)): the counter goes down instead of up. This is the same step as the pre-decrement((--N))seen earlier.
Concept 2: Functions
Definition. A function is a named block of commands that you define once and call as many times as you need. Calling it is the same as typing its name as a command.
Purpose. When the same block is needed in several places of a script, a function holds it once, and each place calls the name instead of repeating the lines.
How it works. The definition gives the name, empty parentheses and the body in braces. A call is the bare name, optionally followed by values; inside the function those values are $1, $2, exactly as command-line arguments are for a script. A result comes back either as text the function prints or as a variable the function sets and the caller reads afterwards:
flowchart LR
A[Main script] -- "call: calculate_area 5, the 5 becomes $1" --> B[Function body]
B -- "sets a global variable, for example return_str" --> A
B -- "prints a line with echo" --> T[Terminal]Declaration forms, arguments and local
#!/bin/bash
function print_message()
{
echo "Bash programming for beginner"
}
function ret_strdata()
{
return_str="Learn bash programming step by step"
}
function calculate_area()
{
local radius=$1
area=$(echo $radius*$radius*3.14 | bc)
echo "Area of the circle is $area"
}
print_message
ret_strdata
echo $return_str
echo "Enter the radius value"
read rad
calculate_area $radfunction print_message() { ... }: the first declaration form. The keywordfunctionis optional, soprint_message() { ... }is the second form and means exactly the same thing. Both need the empty parentheses and the braces.ret_strdatasetsreturn_strwith nolocal. Variables are global by default, so the main script can print it after the call: the "return through a global variable" style, which is how a function hands text back in Bash.calculate_areareceives its argument the way a script does: the first value after the function name becomes$1.local radius=$1copies it into a variable that exists only inside the function; withoutlocal, a function that sets a name used by the main script overwrites it.$(echo $radius*$radius*3.14 | bc):(( ))handles integers only, so the decimal multiplication is piped tobc, and$( )captures whatbcprints. A bare*can be expanded into file names by the shell; here the pattern5*5*3.14matches no file, so it reachesbcunchanged, but quoting the expression as"$radius * $radius * 3.14"is the safe habit.print_message:ret_strdata,calculate_area $rad: the calls. Definitions must come before the calls; Bash reads top to bottom and does not know a function until it has passed its definition.
Scope: global by default, local on request
The local keyword from the listing above deserves its own demonstration, because the difference is invisible until a function and the main script use the same name:
#!/bin/bash
counter=100
show_local()
{
local counter=5
echo "Inside function counter = $counter"
}
show_local
echo "Outside function counter = $counter"counter=100: a global variable set by the main script.local counter=5: inside the function, a second, separatecounterthat exists only until the function returns.show_localthenecho "Outside ...": the function prints5; the main script still prints100, because the global one was never touched. Remove the wordlocaland both lines print5.
Between whole scripts, a result travels as an exit code instead: a script ends with exit 0 or exit 1, and the script that ran it checks $?.
Concept 3: Calling one script from another
Definition. A script can start another script the way you start it from the prompt. Four commands do this, and they differ in which shell runs the second script and what happens to the first one.
Purpose. Splitting work into several files keeps each one short, and the caller decides whether the second file's variables should survive, whether arguments are passed, and whether the caller continues afterwards.
How it works. A child shell is a second bash process started by the first one; it gets its own copy of the variables, and everything it sets disappears when it exits. Only source avoids the child shell:
flowchart TD
S[callpro.sh in shell A] --> S1["source add.sh: same shell, variables stay"]
S --> S2["bash subtract.sh 50 20: child shell B, returns to A"]
S --> S3["eval bash multiply.sh $a $b: build the command text, then run it"]
S --> S4["exec full/path/divide.sh 30: shell A replaced, nothing after runs"]source file: reads the file's commands into the current shell; any variable the file sets is still there afterwards.sourcereceives the name as a file argument, so a bareadd.shin the current directory works.bash file args: starts a child shell. Arguments become$1,$2inside it; its variables vanish when it finishes. Likesource,bashtakes the name as a file argument, so a baresubtract.shworks.eval bash file $a $b:evalfirst joins its arguments into one line of text, then runs that text as a command. It matters when the command text is only built at run time, for example when the whole command lives in a variable. With plain variables, as in the examples here, the result is identical to typingbash multiply.sh 6 7; the listing showsevalonly for its syntax.exec file args: replaces the running script with the target program, so the lines afterexecnever execute.execlooks its target up inPATHexactly like a typed command name, so a baredivide.shfails withexec: divide.sh: not found; a full path or a./divide.shform both work.$PWDholds the same text thepwdcommand prints: the full path of the current directory.
Concept 4: Menus with select and arguments with case
Definition. select name in list prints the list as a numbered menu, shows the prompt #?, reads a number, and stores the matching word in name. case compares one value against several patterns and runs the branch of the first match.
Purpose. select gives a script an interactive menu in one line. case replaces a long if/elif chain when one value must be matched against many fixed words.
How it works. select is a loop: it repeats until the script exits, so the body tests for an "Exit" choice and calls exit 0:
flowchart TD
A[Print the numbered list] --> B["Show the #? prompt"]
B --> C[Read a number]
C --> D[Store the matching word in language]
D --> E{Is it Exit?}
E -- yes --> F[exit 0]
E -- no --> G[echo the selected word]
G --> BCombined with cut, case handles key=value arguments:
for arg in "$@"
do
key=$(echo $arg | cut -f1 -d=)
value=$(echo $arg | cut -f2 -d=)
case $key in
name) echo "Student's name = $value";;
mark) echo "Obtained mark = $value";;
*)
esac
done"$@": all command-line arguments, each kept as one word even if it contains spaces (soname="Ahmed Ali"arrives intact).cut -f1 -d=: cut the text at=(-d=sets the delimiter) and keep field 1;-f2keeps field 2.case $key in ... esac: each pattern ends with), each branch ends with;;, and*)is the default that matches anything. Here the default branch is empty, so an unknown key is silently ignored.
Concept 5: Strings
Definition. A string is a piece of text held in a variable. Bash has no separate string functions; the common operations are done with quoting, expansions and small commands.
Purpose. Scripts join text, compare it, split it into words and change its case constantly: building a message, checking an answer, reading a line of input word by word.
How it works.
- Concatenation: place the variables next to each other,
echo "$string1$string2". - Comparison:
if [ "$text" == "Python" ]. Quote the variable so an empty input does not break the test. - Splitting and case change are the two new operations, each shown by a listing below.
Splitting a string into an array
An array is one variable that holds several values, numbered from 0: arr[0] is the first element, "${arr[@]}" lists every element as separate words. read -a arr stores each word of its input as the next element of arr.
#!/bin/bash
echo "Enter a string value"
read text
IFS=' '
read -a arr <<< "$text"
for value in "${arr[@]}";
do
printf "$value\n"
doneread text: the whole typed line lands intext.IFS=' ': sets the Internal Field Separator, the characterreadsplits on, to a space.read -a arr <<< "$text":<<<feeds the string toreadas if it were typed, and-astores each word as the next element ofarr.for value in "${arr[@]}";: the loop walks the elements, one per pass. The trailing;before the line break is optional and harmless.printf "$value\n":printfprints exactly its format string, so\nmust be written to get a newline;echoadds one on its own. TypingLearn Bash programmingprints the three words on three lines.
The sentence loop in Concept 1 split words in the same way, but its words were fixed in the script; this listing splits text typed at run time.
Changing case
#!/bin/bash
text1='[email protected]'
echo "${text1^^}"
text2='Bash Programming Basics'
echo $text2 | tr '[:upper:]' '[:lower:]'${text1^^}: Bash's own uppercase expansion; it prints[email protected].echo $text2 | tr '[:upper:]' '[:lower:]':trturns every uppercase character coming through the pipe into lowercase, givingbash programming basics. Quote the bracket expressions so the shell does not treat them as filename patterns.
Concept 6: Files and pipes
Definition. Standard output is what a command prints to the terminal; standard input is what it reads from the keyboard. Both can be redirected: > and | take the output somewhere else, read and | feed the input from somewhere else.
Purpose. Redirection lets a script keep results in a file and read them back later; a pipe lets several commands act as one without a temporary file.
How it works. Reading a file:
#!/bin/bash
echo "Reading file using cat command"
content=`cat hardware.txt`
echo $content
echo "Reading file line by line using loop"
filename='hardware.txt'
while read line;
do
echo $line
done<$filenamecontent=`cat hardware.txt`: backticks are the older spelling of$( ); the whole file lands in one variable.echo $contentwithout quotes prints it as one line, because the newlines become spaces.while read line;:read linesucceeds as long as a line is left, so thewhilestops at the end of the file. The trailing;before the line break is optional, exactly as in theforloop of Concept 5.done<$filename: redirects the file into the whole loop, so everyreadtakes the next line from the file instead of the keyboard, and each line prints separately.
Writing: echo $str1 > test.txt creates or overwrites the file; echo $str2 >> test.txt appends.
A pipe | sends the standard output of one command into the standard input of the next:
flowchart LR
A[marks.txt] --> B[sort] -- sorted lines --> C["grep 'Keya'"] -- matching lines --> D[wc -l] --> E[2]Task 1: Four loops in one script
- Create
loops.shwith the four loops of Concept 1 assembled into one file:
#!/bin/bash
# Read a weekday name in each iteration of the loop
for day in Monday Tuesday Wednesday Thursday Friday Saturday Sunday
do
if [[ $day == 'Monday' || $day == 'Thursday' ]]
then
echo "Meeting on $day at 9:30 am"
elif [[ $day == 'Tuesday' || $day == 'Wednesday' || $day == 'Friday' ]]
then
echo "Training on $day at 11:00 am"
else
echo "$day is Holiday"
fi
done
# Read each word of a text by using for loop
for value in Bash Programming for the Beginners
do
echo $value
done
echo "Print the numbers which are even and divisible by 5"
counter=1
while [ $counter -le 20 ]
do
if [[ $counter%2 -eq 0 && $counter%5 -eq 0 ]]
then
echo "$counter"
fi
((counter++))
done
echo "Done"
n=20
until [ $n -lt 0 ]
do
if [[ $n%2 -gt 0 ]]
then
echo $n
fi
((n=$n-1))
done- Run
bash loops.sh. Expected output:
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 Holiday
Bash
Programming
for
the
Beginners
Print the numbers which are even and divisible by 5
10
20
Done
19
17
15
13
11
9
7
5
3
1- Change
until [ $n -lt 0 ]tountil [ $n -lt 10 ]and run again. Expected: the last block now stops at11, because the loop ends as soon asndrops below 10. - Remove the
((counter++))line from thewhileblock, run the script, and pressCtrl+Cafter a second. Expected: "Done" never prints, becausecounterstays at 1 forever. Put the line back.
Task 2: Functions, local variables and exit codes
- Create
func.shfrom the three-function listing in Concept 2 and runbash func.sh, typing5at the prompt. Expected output:
Bash programming for beginner
Learn bash programming step by step
Enter the radius value
5
Area of the circle is 78.50- Create
scope.shfrom theshow_locallisting in Concept 2 and runbash scope.sh. Expected output:
Inside function counter = 5
Outside function counter = 100- Delete the word
localinshow_localand run again. Expected: both lines now show5, because the function overwrote the script's global variable. - Review from the previous lesson: a script hands a result to its caller through an exit code. Create the two files below, then run
bash second.shtwice, typing55and then110.
first.sh:
#!/bin/bash
echo "Enter a numeric value"
read n
if [[ $n -le 100 ]]
then
exit 0
else
exit 1
fisecond.sh:
#!/bin/bash
bash "first.sh"
if [ $? -eq 1 ]
then
echo "The input number is greater than 100"
else
echo "The input number is less than or equal to 100"
fiExpected for the two runs:
Enter a numeric value
55
The input number is less than or equal to 100
Enter a numeric value
110
The input number is greater than 100second.sh never sees the number, only the exit code of first.sh.
Task 3: Calling scripts with source, bash, eval and exec
- Create the four helper scripts. Each listing is a complete file.
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"- Make the four scripts executable, since
execneeds its target to be executable. - Create
callpro.sh.execlooks up its target like a typed command, so it needs the full path ofdivide.sh;$PWDsupplies the path thatpwdprints, so the script works no matter which directory it is created in:
#!/bin/bash
script1="add.sh"
script2="subtract.sh"
script3="multiply.sh"
script4="$PWD/divide.sh"
source "$script1"
echo "After source, result is still visible here: $result"
bash $script2 50 20
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 because exec replaced the shell"- Run
bash callpro.shand enter6then7. Expected output:
The addition of 60+40=100
After source, result is still visible here: 100
The subtraction of 50-20=30
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 15The second line shows 100 because source ran add.sh in the same shell. The eval line prints the same as bash multiply.sh 6 7 would, as Concept 3 explains. The final echo never appears because exec replaced that shell with divide.sh.
- Change
source "$script1"tobash "$script1"and run again. Expected: the second line ends right after the colon, becauseadd.shran in a child shell (a secondbashprocess) whoseresultwas discarded when it exited. - Change
script4="$PWD/divide.sh"toscript4="divide.sh"and run again. Expected last line:callpro.sh: line 18: exec: divide.sh: not found, becauseexecsearchedPATHand the current directory is not in it. Restore the$PWDform.
Task 4: A menu and a command-line calculator
- Create
menu.sh:
#!/bin/bash
echo "Select your favorite language"
select language in C# Java PHP Python Bash Exit
do
if [[ $language == "Exit" ]]
then
exit 0
else
echo "Selected language is $language"
fi
doneselect language in C# Java PHP Python Bash Exit: the six words are the menu; the chosen word lands inlanguage.if [[ $language == "Exit" ]]thenexit 0: the only way out of the loop.echo "Selected language is $language": any other choice is echoed and the#?prompt returns.
- Run
bash menu.sh, type3, then6. Expected:
Select your favorite language
1) C#
2) Java
3) PHP
4) Python
5) Bash
6) Exit
#? 3
Selected language is PHP
#? 6The menu prints once, the #? prompt returns after every choice, and 6 ends the script through exit 0.
- Review: create
cl1.sh, which reads three arguments: a number, an operator and a number.xstands for multiplication because a bare*on the command line would be expanded into file names.
#!/bin/bash
echo "Argument values are: $1 $2 $3"
operand1=$1
operand2=$3
operator=$2
if [[ $operator == '+' ]]
then
((result=$operand1+$operand2))
elif [[ $operator == '-' ]]
then
((result=$operand1-$operand2))
elif [[ $operator == 'x' ]]
then
((result=$operand1*$operand2))
elif [[ $operator == '/' ]]
then
((result=$operand1/$operand2))
fi
echo -e "Result is = $result"echo -e enables backslash escapes such as \n inside the text; here the text has none, so it makes no visible difference and plain echo would print the same line.
- Run it four times:
bash cl1.sh 6 + 3,bash cl1.sh 6 - 3,bash cl1.sh 6 x 3,bash cl1.sh 6 / 3. Expected output:
Argument values are: 6 + 3
Result is = 9
Argument values are: 6 - 3
Result is = 3
Argument values are: 6 x 3
Result is = 18
Argument values are: 6 / 3
Result is = 2- Create
cl2.shfrom thecaselisting in Concept 4, with#!/bin/bashandecho "Argument values are: $1 $2"as its first two lines. Runbash cl2.sh name="Ahmed Ali" mark=90. Expected:
Argument values are: name=Ahmed Ali mark=90
Student's name = Ahmed Ali
Obtained mark = 90- Modify the empty default branch: replace the line
*)with*) echo "Unknown argument: $key";;. Runbash cl2.sh name="Ahmed Ali" mark=90 age=20. Expected:
Argument values are: name=Ahmed Ali mark=90
Student's name = Ahmed Ali
Obtained mark = 90
Unknown argument: ageThe first line still shows only two arguments because it prints $1 $2; the loop over "$@" sees all three, and age reaches the *) branch.
Task 5: Strings and files
- Create
split.shfrom the splitting listing in Concept 5. Run it and typeLearn Bash programming. Expected:
Enter a string value
Learn Bash programming
Learn
Bash
programmingThe input has three words, so the array has three elements and the loop prints three lines.
- Create
case.shfrom the case-change listing in Concept 5 and runbash case.sh. Expected:
[email protected]
bash programming basics- Create
hardware.txtwith one word per line:Monitor,Keyboard,Mouse,Scanner,Printer. Createreadfile.shfrom the Concept 6 listing and runbash readfile.sh. Expected:
Reading file using cat command
Monitor Keyboard Mouse Scanner Printer
Reading file line by line using loop
Monitor
Keyboard
Mouse
Scanner
Printer- Create
writefile.sh, run it, and typeHello BashthenSecond line:
#!/bin/bash
echo "Enter some text"
read str1
echo $str1 > test.txt
echo "Enter some other text"
read str2
echo $str2 >> test.txt
echo `cat test.txt`read str1thenecho $str1 > test.txt: the first text creates (or overwrites) the file.read str2thenecho $str2 >> test.txt: the second text is appended as a new line.echo `cat test.txt`: prints the file's content; the unquoted backticks turn the two lines into one, as in Concept 6.
Expected session:
Enter some text
Hello Bash
Enter some other text
Second line
Hello Bash Second lineThen run cat test.txt and expect the two lines on separate rows: > created the file and >> appended to it. Run the script again with different words: the file holds only the new pair, because > overwrote it.
Task 6: Pipes
- Create
marks.txtwith exactly these six lines (columns separated by spaces or tabs):
Asraf CSE-409 79
Kabir CSE-304 95
Keya CSE-101 67
Asraf CSE-304 88
Keya CSE-409 90
Asraf CSE-101 92- Do the job the long way, three commands and a temporary file:
sort marks.txt
grep 'Keya' marks.txt > temp.txt
wc -l temp.txtExpected output of the three commands:
Asraf CSE-101 92
Asraf CSE-304 88
Asraf CSE-409 79
Kabir CSE-304 95
Keya CSE-101 67
Keya CSE-409 90
2 temp.txtsort orders the lines alphabetically (the three Asraf lines by course code), grep writes the two Keya lines into temp.txt, and wc -l counts them.
- Do it the short way:
sort marks.txt | grep 'Keya' | wc -l. Expected output:2. Notemp.txtis needed; each output flows straight into the next command. - Change the pipeline to count
Asrafinstead. Expected:3.
Summary
for word in listwalks a list, whether the list is a set of names or the words of a sentence;whileruns while a test succeeds;untilwhile it fails. Every body sits indo ... done.- Update counters with
((counter++))or((n=$n-1)); a loop without an update never ends. - Functions are declared as
name() { }with or without thefunctionkeyword, must be defined before they are called, and receive arguments as$1,$2. Variables are global by default;localkeeps one inside the function. A function hands text back by setting a global variable; whole scripts hand a result back withexit N, read through$?. sourcekeeps the called script's variables;bashruns it in a child shell;evalbuilds the command text first and behaves likebashwhen the text is plain;execreplaces the current shell for good and needs a full path such as$PWD/divide.sh.selectbuilds a numbered menu with the#?prompt and loops untilexit;case ... esacmatches patterns,*)being the default;cut -f1 -d=splitskey=value.- Strings: concatenate by adjacency, compare with
[ "$a" == "$b" ], split into an array withIFSplusread -aand walk it with"${arr[@]}", change case with${var^^}ortr.printfneeds an explicit\n. - Read files with
while read line; do ...; done<file, write with>(overwrite) and>>(append), and chain commands with|, which links one command's standard output to the next command's standard input.