How To Create A Bash Script, With Example Code

With bash scripts, you can run sequences of commands. They prevent you from repeating yourself and allow you to store long and tedious to type commands for later usage. In this article, you’ll learn how to create a Bash script. You’ll also learn how to read arguments, use variables, and create for-loops to repeat actions.

Before you start creating scripts, make sure you are familiar with the basic Linux commands.

Create a Bash script

A command-line script starts with the shebang — a sequence of the two characters #!. After these two characters, you can name any interpreter and a bunch of arguments, like this:

#!interpreter [optional arguments]

A shell script is usually based on the most popular shell called Bash, so this would simply become:

#!/bin/bash

Sometimes you’ll see this instead:

#!/usr/bin/env bash

When using /usr/bin/env, the provided command will be searched for in the current $PATH, instead of hardcoding it to the /bin/bash location. Bash will almost always be found at /bin/bash, so the first method is fine for us.

However, you may want to use this trick to create an executable Python script. Python can be installed anywhere, depending on the way it is installed. I’ve seen it in /usr/bin/, /bin, and /usr/local/bin, just to name a few. If you’re running a virtual environment, it can be anywhere. So searching the $PATH is a good option, since that will find the Python version from the virtual environment.

We’ll get to creating an actual shell script in a moment. But first, we’ll take a look at variables and for-loops to juice things up a little.

Bash variables

Just like other languages, Bash has variables. One peculiar thing about variables in Bash is that you access them with the dollar sign, but you set them without it. To clarify, here’s an example:

myvar="Hello world"
echo $myvar

Bash has a number of reserved variables, that are automatically set for you. The following table is a non-exhaustive list of some of the more useful ones:

VariableFunction
$0Name of the current script.
$1 … $9First 9 arguments to the script.
$#Number of arguments passed to the script.
$@All the arguments supplied to the script.
$USERThe username of the user running the script.
$HOSTNAMEThe hostname of the machine the script is running on.
$SECONDSThe number of seconds since the script was started.
$RANDOMReturns a different random number each time is it referred to.
$LINENOReturns the current line number in the Bash script.
Bash reserved variables

Bash Script arguments

We’re now able to create a shell script, but this script will do the same thing each time you run it. We can make it infinitely more interesting by using arguments.

Here’s an example, call it arguments.sh:

#!/bin/bash
echo "Hello $1, from $0"

If we run this with one argument, the result is as follows:

$ ./arguments.sh Weirdo
Hello Weirdo, from ./arguments.sh

What Bash does, is chop up your entire command and assign it to the not very imaginative variables $0, $1, $2, et cetera. As you can see, the first part of our command is the name of the script itself. This can come in handy, as can be seen in the next section.

Create a safety net

Before moving on to the serious stuff, I want to teach you about an important safety net.

Once you start creating scripts, you will inevitably make mistakes. We all do. One very common mistake in scripts can be especially disastrous. It’s the unset variable. If you don’t know what I mean, just keep reading as it will become clear in a moment.

We can prevent using unset variables by starting our scripts with the following command:

set -u

That’s all! From here on, your script is treating unset variables as an error when you are substituting.

For example, this script will fail:

#!/bin/bash
set -u
rm -rf /$my_directory

The output:

$ ./test.sh
./test.sh: line 3: my_directory: unbound variable

That’s because I never set $my_directory. Without set -u, it won’t fail but instead just ignore the empty variable, leaving us with the command rm -rf /. We all know what that does, right? If executed by a privileged user (like root), this mistake would have wiped my entire filesystem instead of the directory I intended to delete.

Make a habit of always starting your scripts with set -u.

While you’re at it, add two more safety options. Many experienced script writers start every Bash script with this line:

set -euo pipefail

Here’s what the three options do:

  • -e stops the script as soon as a command fails, instead of happily continuing with the next command.
  • -u treats unset variables as an error, as you just saw.
  • -o pipefail makes a pipeline like command1 | command2 fail when any command in it fails. Without it, only the exit code of the last command counts.

For example, with set -euo pipefail at the top, this script stops at the failing ls. You’ll see the error from ls, and the echo never runs:

#!/bin/bash
set -euo pipefail
ls /does-not-exist
echo "This line never runs"

Bash for loop

The usefulness of the command-line, and especially scripts, increases exponentially once you master Bash for-loops. And even though they might look scary, they are really not that hard.

Syntax of a Bash for-loop

The basic syntax of a loop is:

for VARIABLE in A LIST
do
  command1
  command2
  commandN
done

Example Bash script

The A LIST part can be anything: file names, numbers, or strings. It will be a list of file names in most scripts, though, since that’s what we’re often working with in Bash. Now that we know how to create a loop, let’s look at our first script:

#!/bin/bash

echo "You can list numbers and text like this:"

for n in 1 2 3 four
do
  echo "Number $n"
done

echo "Or specify a range of numbers:"

for n in {1..5}
do
  echo "Number $n"
done

echo "Or loop over the files in the current directory:"
for f in *
do
  echo "$f"
doneCode language: Shell Session (shell)

In the last for-loop, I used *. Bash replaces it with the names of all files in the current directory, and the for-loop gets them one by one. Note the quotes around "$f". They make sure a file name with spaces in it is treated as one name.

You’ll often see for f in $(ls) in older scripts. The $(...) part runs a command and substitutes its output, which is useful in general. For file names it’s a bad idea, though: Bash splits the output of ls on spaces, so a file called my notes.txt turns into two loop iterations, my and notes.txt. Use * instead.

Starting a Bash script

To start this script, we can do two things. First, we can run it with:

$ bash loop.sh

The second way, that I recommend, is making the file executable. The OS will know how to execute our file because of our shebang line at the top! Making the file executable is done by setting the file’s “execute flag” like this:

$ chmod +x loop.sh

You can now run the script with:

$ ./loop.sh

The output in my case is:

$ ./loop.sh
You can list numbers and text like this:
Number 1
Number 2
Number 3
Number four
Or specify a range of numbers:
Number 1
Number 2
Number 3
Number 4
Number 5
Or loop over the files in the current directory:
loop.sh
my notes.txt
notes.txt

It might differ for you, depending on which files are in the directory that you’re running the script in.

Bash conditional programming

Sometimes you only want to run a command if a certain condition is true. For this, we have the if… then… else…fi construct in bash.

Check arguments in bash script

We can use conditional programming to improve our previous example, arguments.sh, since it contained a little problem. It expects a name in $1 without checking if it actually gets one. Let’s fix this:

#!/bin/bash
if test -z "$1"
then
  echo "Usage: $0 <Your name>"
else
  echo "Hello $1, from $0"
fiCode language: Shell Session (shell)

The test command in bash

With test -z we can check if a variable’s length is zero. If that is the case, we print some friendly usage instructions:

$ ./arguments.sh
Usage: ./arguments.sh <Your name>Code language: Shell Session (shell)

If you followed my advice and put set -u at the top, this script stops with $1: unbound variable when you run it without an argument. Write "${1:-}" instead of "$1". It means: use $1, or an empty string if it isn’t set.

The test command can test for many things so it really aids in conditional programming. The full list can be seen when you enter man test on the command line. That’s right, you don’t need Google for everything! Using man-pages is part of being a command-line ninja! You’ll find that there’s a man page for pretty much everything you can do in your terminal. After all, in the old days, we didn’t have the Internet to google everything…

Here is one more example in which we compare two values to see if they are the same:

#!/bin/bash
for i in {1..10}
do
  if test $i -eq 3 
  then
    echo "I found the 3!"
  else
    echo "Not looking for the $i"
  fi
doneCode language: Shell Session (shell)

This loop runs for 10 iterations and checks if $i is equal to 3 each time. Can you predict the output?

Run it to check your answer. Here’s what it prints:

Not looking for the 1
Not looking for the 2
I found the 3!
Not looking for the 4
Not looking for the 5
Not looking for the 6
Not looking for the 7
Not looking for the 8
Not looking for the 9
Not looking for the 10

Although the else-part is optional, you always need to end with a fi.

Related articles

Leave a Comment