Introduction to Bash Shell Scripting
A detailed overview to scripting the Bash Shell
Shell scripting is an powerful way to automate tasks that you regularly execute on your computer.
In this tutorial I give an extensive overview of shell scripting, and will be the base reference for more in-depth and advanced tutorials on creating practical shell scripts.
Check out my introduction to Bash post.
Bash gives you a set of commands that put together can be used to create little programs, that by convention we call scripts.
Note the difference. We don’t say Bash programming but Bash scripting, and we don’t call Bash scripts “Bash programs”. This is because you can generally reach a certain amount of complexity before feeling that your scripts gets out of hand.
But Bash scripts are great because you don’t need anything else than Bash to run them. No compiler, no interpreter, just your shell.
There are many things that you’ll miss from programming languages like Perl or JavaScript or Python.
Variables have no scope, they are all global, there is no standard library, you don’t have a module system, for instance. But the advantages are pretty great: you can very easily invoke any CLI tool just like you were in the shell, and the Unix approach of having many little utility commands really makes shell scripting shine. Perform network requests with wget
, process text using awk
, and more.
Shell scripting is one of the tools you’d better know, at least know how to read a program when you see it, and the goodies it can bring in your day to day work.
This tutorial guides you to the theory and concepts of Bash scripting. I will post more detailed tutorials on specific techniques or how to solve specific problems in the future.
Basics
Scripts are stored in files. You can give any name and extension to a shell script, it does not matter. The important thing is that it must start with a “shebang” on the first line:
#!/bin/bash
and it must be an executable file.
A file is set as executable using chmod
, an utility command.
You can use it like this:
chmod u+x myscript
to make the myscript
file executable for your user (I’m not going in the permissions rabbit hole here, but I will cover them soon).
Now you can execute the script if you are in the same folder by calling it ./myscript
, or using the full path to it.
While learning I encourage you - when possible - to use an online playground like this one, it makes things easier to test.
Comments
Comments are one of the most important things when writing programs. A line starting with the #
symbol is a comment (with the exception of the shebang line you saw above here).
#!/bin/bash
# this is a comment
A comment can start at the end of a line too:
#!/bin/bash
echo ok # this is a comment
Variables
You can set variables by using the =
operator:
name=value
Examples:
ROOM_NUMBER=237
name=Flavio
nickname="Flavio"
You can print a variable by using the echo
built-in command and prepending a $
to the var name:
echo $ROOM_NUMBER
echo $name
Operators
Bash implements some arithmetic operators commonly used across programming languages:
+
add-
subtract*
multiply/
divide%
modulo (remainder of the division)**
exponentiation
You can compare values using
<
<=
==
>=
>
You can also use these comparison operators:
-lt
lower than-gt
greater than-le
lower or equal than-ge
greater or equal than-eq
equal to-ne
not equal to
In this way:
#!/bin/bash
age=23
minimum=18
if test $age -lt $minimum
then
echo "Not old enough"
fi
Logical operators:
&&
logical AND||
logical OR
These shortcuts allow to perform an arithmetic operation and then an assignment:
+=
-=
*=
/=
%=
There are some more operators, but those are the most common ones.
Print to the screen
You can print anything to the screen using the echo
command:
#!/bin/bash
echo "test"
echo test
echo testing something
Logical conditions
AND: evaluates to 0
(zero) if both command
and anothercommand
execute and return 0
. Returning zero in shell commands means the command was successful. An error message is identified by a non-zero return value.
command && anothercommand
OR: evaluates to 0
(zero) if at least one of command
and anothercommand
execute and return 0
.
command || anothercommand
NOT: invert the logic return value of command
:
! command
Control structures
In you can use several control structures which you might be familiar with:
If/else statements
Simple if
:
if condition
then
command
fi
if
then else
:
if condition
then
command
else
anothercommand
fi
Nested if
- then - else
:
if condition
then
command
elif
anothercommand
else
yetanothercommand
fi
You can keep the else
on the same line by using a semicolon:
if condition ; then
command
fi
Example:
#!/bin/bash
DOGNAME=Roger
if [ "$DOGNAME" == "" ]; then
echo "Not a valid dog name!"
else
echo "Your dog name is $DOGNAME"
fi
Notice the brackets? We must add brackets when we have to do any kind of evaluation of a boolean expression.
Loops
While loop
While condition
resolves to a true value, run command
while condition
do
command
done
Until
Until condition
resolves to a true value, run command
until condition
do
command
done
For in
Iterate a list and execute a command for each item
for item in list
do
command
done
Break and continue
Inside the loops, you can use the break
and continue
statements to break the loop completely, or just skip the current iteration.
Case
A case control structure lets you pick different routes depending on a value.
case value in
a)
command
#...
;;
b)
command
#...
;;
esac
Like with fi
, esac
ends the case structure and as you can notice it’s case
spelled backwards.
We add a double semicolon after each case.
A *)
case would handle all the cases not explicitly expressed.
Using a |
symbol you can express multiple choices for a single case.
Example:
#!/bin/bash
read -p "How many shoes do you have?" value
case $value in
0|1)
echo "Not enough shoes! You can't walk"
;;
2)
echo "Awesome! Go walk!"
#...
;;
*)
echo "You got more shoes than you need"
#...
;;
esac
Select
A select
structure shows the user a menu of choices to choose:
select item in list
do
command
done
Example:
#!/bin/bash
select breed in husky setter "border collie" chiwawa STOP
do
if [ "$breed" == "" ]; then
echo Please choose one;
else
break
fi
done
echo "You chose $breed"
Testing conditions
I used the term condition
in the above control structures.
You can use the test
Bash built-in command to check for a condition and return a true (0
) or false value (not 0
).
Here is an example:
#!/bin/bash
if test "apples" == "apples"
then
echo Apples are apples
fi
if ! test "apples" == "oranges"
then
echo Apples are not oranges
fi
Reading input from the command line
You can make your scripts interactive by using the read
built-in command. This command reads a line from the standard input, and it can format input in a very flexible way.
The simplest usage is:
echo "Age:"
read age
this is going to print “Age:” and you can enter a number, press enter and that number will be assigned to the age
variable.
The -p
option of read
provides a built-in prompt, and puts the input in the age
variable:
read -p "Age: " age
The read
command has many more options, which you can inspect by typing help read
in Bash.
Adding options
Options are specified using a hyphen followed by a letter, like this:
ls -a
driveCar -b "Ford"
In your code you use the built-in command getopts
to parse the options and get the value.
If we accept options a
and b
, we feed getopts ab
to a while loop.
If option b
accepts a value, we append a colon :
after it, so we format our getopts
call like this: getopts ab: arg
Here’s a snippet:
while getopts xy: arg
do
case $arg in
x) echo "Option $arg enabled" ;;
y) echo "The value of $arg is $OPTARG" ;;
esac
done
In this script, $OPTARG
is automatically assigned the option letter. We tell getopts
which options we accept, and we handle each case separately.
Errors are handled automatically by getopts
. We can choose to handle error messages ourselves, by prepending a colon :
before the arguments definition: getopts :xy: arg
.
Then we also handle the \?
and :
cases. The first is called when we pass an invalid option, and the second is called when we miss an option:
while getopts :xy: arg
do
case $arg in
x) echo "Option $arg enabled" ;;
y) echo "The value of $arg is $OPTARG" ;;
:) echo "$0: Must supply an argument to -$OPTARG." >&2
exit 1 ;;
\?) echo "Invalid option -$OPTARG ignored." >&2 ;;
esac
done
Now if you miss to add an argument, the error messages are different.
Working with parameters
Inside a script you can access any parameter that was passed at invocation time. You access them using $0
, $1
, $2
and so on, depending on the position, where $0
is the name of the command and incrementing the number the position of the parameter is incremented. After position 9 you need curly braces: ${10}
, ${11}
…
For example running this startCar
script:
#!/bin/bash
echo $0
echo $1
as ./startCar fiesta
will print
./startCar
fiesta
The special $*
syntax prints all the parameters, and $#
the number of parameters passed:
#!/bin/bash
echo $# $*
./test.sh Milan Florence Rome
3 Milan Florence Rome
Separating options from parameters
If you have both options and parameters you need to separate them. You do this with a hyphen:
driveCar -m "Ford Fiesta" - Milan Florence Rome
Working with strings
Given a string:
dogname="Roger"
You can get its length using ${#dogname}
Always use quotes around strings, when working with them, to avoid Bash interpreting the special characters inside them.
You can compare 2 strings using the =
or ==
operator, it’s the same:
"$dogname" = "$anotherdogname"
"$dogname" == "$anotherdogname"
It’s not an assignment because of the spaces surrounding the =
.
You can also check for unequality:
"$dogname" != "$anotherdogname"
Arrays
An array is a list of items, declared inside parentheses:
breeds=(husky setter "border collie" chiwawa)
You can reference any item inside an array using square brackets:
breeds[0]
breeds[1]
and you can get the total number of items using this special syntax:
${#breeds[@]}
Built-in commands
So far you’ve seen a few built-in commands already, like these:
test
read
echo
Bash has a number of them. You can see them all by calling help
and then you can find the help for each command using for example help read
.
Functions
Just like in JavaScript or in any other programming language, you can create little reusable pieces of code, give them a name, and call them when you need.
Bash calls them functions.
You define a function with
function name {
}
Example:
function cleanFolder {
}
and you invoke it like this:
cleanFolder
You can pass parameter to folders, without the need of declaring them - you just reference them as $1
, $2
etc. Like you do for script parameters:
function cleanFolder {
echo "Clean folder $1"
}
cleanFolder "/Users/flavio/Desktop"
→ I wrote 17 books to help you become a better developer, download them all at $0 cost by joining my newsletter
→ JOIN MY CODING BOOTCAMP, an amazing cohort course that will be a huge step up in your coding career - covering React, Next.js - next edition February 2025