Skip to content

Bash Script Basics

The "13. Disk, Filesystem, LVM, and Storage" module made one important observation in Disk-Full Troubleshooting: the df -hdf -idulsof sequence was a precise, ordered check that had to be repeated by hand every time. Any task — a check, a backup, a cleanup — that gets repeated by hand more than a few times is a natural candidate for automation. Bash scripting is exactly that automation tool. This article opens the "14. Bash Scripting" module, starting with what a script is, its structure, and how it's run; later articles add variables, conditionals, loops, functions, and error handling, and the module ends with three complete, working production scripts.

What You Will Learn

By the end of this article you will be able to:

  • explain what a script is and why it's worth writing one;
  • explain the shebang line's purpose and how to choose it;
  • grant a script execute permission and run it in two different ways;
  • follow commenting conventions;
  • start checking a script with shellcheck.

What a Script Is and Why It's Needed

A script is a set of commands, written into a text file, executed in sequence. Any command you'd type one at a time into a terminal can be placed inside a script; the difference is that a script is written once and then rerun any number of times, in exactly the same order, without mistakes. A script is executed by an interpreter — a program that reads the commands one by one and hands them to the system. For Bash scripts, that interpreter is the bash program itself.

A script earns its keep in situations like these:

  • the same sequence of commands needs to run regularly (daily, weekly);
  • a command line is long enough that retyping it by hand invites mistakes;
  • a task needs to run unattended, through cron or a systemd timer;
  • the same action needs to be applied to multiple servers.

A script isn't a programming language — it's a shell that organizes existing commands. For heavy computation or processing large amounts of data, a full programming language like Python is the usual choice; but a large share of a system administrator's day-to-day work — managing files, processes, and commands — is solved at exactly the level of a Bash script.

Your First Script

Using a plain text editor (nano, vim), create a file named hello.sh:

#!/bin/bash

# This script prints a simple greeting
echo "Hello, Linux!"

Making the file executable and running it:

chmod +x hello.sh
./hello.sh
Hello, Linux!

chmod +x adds the execute permission to the file — without it, the system won't allow the file to run as a script, even if the text itself is written correctly. The full logic of permissions was covered in File Permissions; what matters for a script is that the owner (or the relevant group/others category) needs the x bit.

Shebang: Choosing the Interpreter

A script's first line — #!/bin/bash — is called the shebang (# — "sharp," ! — "bang," shortened together to "shebang"). This line tells the system which program should execute the commands in the file. When a script is run as ./hello.sh, the system actually runs the program named in the shebang and passes the script's path to it as an argument — meaning the command actually executed looks like /bin/bash ./hello.sh. This can be checked in the process table:

sleep 100 &
ps -ef | grep sleep

If a shebang script is run instead of sleep, ps -ef output shows a line in exactly the form /bin/bash /path/script.sh — proving what the shebang does.

Two forms exist:

#!/bin/bash
#!/usr/bin/env bash

The first points to bash's exact path and works correctly on most Linux distributions. The second finds bash through PATH via the env program; it's used when more portability is needed (for example, when bash might be installed at a different path on different systems). In this course, and on production Linux servers, bash is almost always located at /bin/bash, so the #!/bin/bash form is used.

A shebang isn't specific to bash — it can point to any interpreter:

#!/bin/sh
#!/usr/bin/python3
#!/usr/bin/perl

If no shebang is written at all, the script runs through the current interactive shell — this can sometimes seem to work, but it's unreliable: different users can have different default shells (bash, zsh, dash), and their syntax isn't identical. This is why writing an explicit shebang is always good practice.

Note

On some distributions (Debian/Ubuntu, for example), /bin/sh actually points to dash, not bashdash is a POSIX shell that doesn't support many of Bash's convenient extensions (arrays, [[ ]], local, and others). This is why a script using Bash-specific syntax must have exactly #!/bin/bash as its shebang, not #!/bin/sh.

Two Ways to Run a Script

1. With a relative or absolute path:

./hello.sh
/home/<username>/scripts/hello.sh

This works even if the script's directory isn't in the PATH variable, since the path is given explicitly. The ./ marker means the current directory — it can't be dropped, or the shell searches for the script in PATH and usually can't find it.

2. By name alone (if the script is in PATH):

hello.sh

This only works if the script's directory has been added to the $PATH variable. The PATH mechanism covered in Aliases and Environment Variables works exactly the same way here — the shell searches for the command name in the directories listed in PATH.

A script can also be run by calling the interpreter directly, in which case execute permission (the x bit) isn't required, since bash reads the file itself:

bash hello.sh

This is convenient for a quick check, but for a script used regularly, chmod +x and keeping it as a standalone executable file is recommended.

Comments

Everything after a # character (except the shebang, since it's interpreted as a special first line) is ignored by the interpreter — this is a comment:

#!/bin/bash

# This whole line is a comment
echo "Hello"  # The right-hand side of this line is also a comment

Comments are for the person reading the code, not the code itself. A good script's comment explains why something is written a particular way (not what it does — the command itself already shows that) — for example, the reason for a nonstandard workaround or a non-obvious constraint.

Checking a Script: shellcheck

As a script grows longer, small mistakes (a misspelled variable name, a forgotten quote) can go unnoticed. shellcheck is a static analysis tool for Bash scripts that catches common mistakes at the writing stage:

sudo apt install shellcheck
shellcheck hello.sh
In hello.sh line 4:
echo "Hello, Linux!"
^-- SC2148 (error): Tips depend on target shell and yours is unknown. Add a shebang.

(This example deliberately removes the shebang to show shellcheck warning about it.) Running every script written throughout this module through shellcheck is recommended — it's especially useful once variables and conditionals show up in later articles.

Debugging a Script: bash -x and set -x

shellcheck catches mistakes before a script runs; -x (xtrace) helps when a script runs but does the wrong thing and you can't see why. It prints each command after all variable and command substitution, right before it executes — so you see the actual values Bash used, not what you assumed.

Run the whole script in trace mode without editing it:

bash -x hello.sh
+ echo 'Hello, Linux!'
Hello, Linux!

Each traced line is prefixed with +. To trace only a suspect section rather than the whole script, wrap it with set -x/set +x:

#!/bin/bash
name="Linux"
set -x            # tracing on
greeting="Hello, ${name}!"
set +x            # tracing off
echo "$greeting"
+ greeting='Hello, Linux!'
+ set +x
Hello, Linux!

This is the fastest way to answer the classic interview question "a script isn't behaving as expected — how do you debug it?": shellcheck for static mistakes, then bash -x (or a scoped set -x) to watch the real, expanded commands as they run.

Tip

Combine it with the strict flags from Exit Codes and Error Handling: running a script as bash -xeuo pipefail script.sh traces every command and stops at the first error and the first unset variable — the single most useful one-line debugging invocation.

Common Mistakes

Execute Permission Not Granted

bash: ./hello.sh: Permission denied

Cause — the file lacks the x bit. Fix: chmod +x hello.sh.

Forgetting ./

bash: hello.sh: command not found

The current directory usually isn't in PATH, so the shell can't find the script. Fix: write ./hello.sh, or give the absolute path.

A Space or Wrong Character in the Shebang Line

# !/bin/bash

Leaving a space between # and ! turns the line into an ordinary comment, and the shebang stops working — the script runs through the current shell instead, which can lead to unexpected behavior.

Line-Ending Characters (CRLF) from a File Edited on Windows

If a script is edited on Windows and then copied to a Linux server, the following error can show up:

bash: ./hello.sh: /bin/bash^M: bad interpreter: No such file or directory

The cause — Windows marks line endings with \r\n (CRLF), while Linux uses only \n; the extra \r character gets appended to the shebang line and breaks the path. Checking and fixing it:

file hello.sh
sed -i 's/\r$//' hello.sh

Exercises

  1. Write a script named whoami.sh that prints the output of whoami, hostname, and date in sequence. Make it executable and run it both ways (./whoami.sh and bash whoami.sh).
  2. Deliberately write the shebang line as # !/bin/bash (with a space in the middle) and observe what changes.
  3. Check the script from exercise 1 with shellcheck and analyze the output.

Summary

A Bash script combines a sequence of commands into a single executable file. The shebang line determines which interpreter is used, and chmod +x grants the file execute permission — these two are the foundation of any script. The next article, Variables and Arguments, covers how to store data inside a script and pass it in from outside (via the command line) — turning a script from a static list of commands into a flexible tool.

References