Skip to content

Introduction to Bash Shell Scripting

New Course Coming Soon:

Get Really Good at Git

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:

You can compare values using

You can also use these comparison operators:

In this way:

#!/bin/bash
age=23
minimum=18
if test $age -lt $minimum
then
  echo "Not old enough"
fi

Logical operators:

These shortcuts allow to perform an arithmetic operation and then an assignment:

There are some more operators, but those are the most common ones.

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:

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"
Are you intimidated by Git? Can’t figure out merge vs rebase? Are you afraid of screwing up something any time you have to do something in Git? Do you rely on ChatGPT or random people’s answer on StackOverflow to fix your problems? Your coworkers are tired of explaining Git to you all the time? Git is something we all need to use, but few of us really master it. I created this course to improve your Git (and GitHub) knowledge at a radical level. A course that helps you feel less frustrated with Git. Launching Summer 2024. Join the waiting list!

Here is how can I help you: