Freebie

Script structure

Script structure

A bash script is a plain text file with execute permissions. The first line, called the shebang, tells the operating system which interpreter to run:

#!/bin/bash

An alternative form locates the bash executable via PATH, which improves portability across systems where bash lives in a non-standard location:

#!/usr/bin/env bash

Store scripts that you want available system-wide in /usr/local/bin. Store personal scripts in ~/bin and add that directory to your PATH.

Running a script

Before you can run a script, set the execute permission with chmod:

chmod +x my_script.sh
./my_script.sh

The ./ prefix is required because the current directory is not in PATH by default. This is a deliberate security decision: it prevents a malicious file named ls or cp from running in place of the real command.

To run a script in a specific shell without setting execute permissions, pass it as an argument:

bash my_script.sh

Execution flags

Pass flags between bash and the script name to change how the shell runs the script. You can combine flags into a single argument:

bash -xv script.sh      # trace + verbose
bash -xvn script.sh     # trace + verbose + syntax check only

The following table lists the most useful flags:

FlagNameEffect
-nNo executeParse the script and check syntax without running any commands
-xXtracePrint each command after expansion, prefixed with +, before executing it
-vVerbosePrint each line as bash reads it, before expansion
-eExit on errorExit immediately when any command returns a non-zero status
-uUnset as errorTreat references to unset variables as errors and exit
-o pipefailPipeline failureReturn the exit status of the first failed command in a pipeline, not the last
-iInteractiveRun the shell in interactive mode, loading ~/.bashrc and enabling job control
-rRestrictedRun in restricted mode. See Restricted shell
-sStdinRead commands from stdin; any remaining arguments become positional parameters

-n: check syntax without running

Use -n to catch syntax errors before you run a script in production. Bash parses the entire file and reports errors but executes nothing:

bash -n script.sh
script.sh: line 12: syntax error near unexpected token `fi'

-n does not catch runtime errors such as a missing file or an undefined variable. It only validates that the script parses correctly.

-x: trace execution

-x prints each command after the shell expands it, prefixed with +. This shows you exactly what bash runs, including the values of any variables:

bash -x deploy.sh
+ DEST=/var/www/html
+ rsync -av ./dist/ /var/www/html
+ systemctl reload nginx

Use -x when a script produces unexpected results and you need to see the expanded commands rather than the source lines.

-v: verbose output

-v prints each line as bash reads it, before variable expansion or substitution. This is useful for tracing logic errors in scripts that use complex expansions:

bash -v script.sh
for file in "$@"; do
+ for file in report.txt notes.txt

-e and -u: fail fast

-e exits the script as soon as any command fails. -u exits when the script references a variable that has not been set. Together they catch two of the most common silent failures in shell scripts:

bash -eu deploy.sh

You can also set these inside the script itself with set, which is the more common pattern for production scripts:

#!/bin/bash
set -eu

-o pipefail: catch pipeline errors

By default, a pipeline returns the exit status of its last command, even if an earlier command failed. -o pipefail changes this so the pipeline fails if any command in it fails:

bash -o pipefail script.sh

Combine it with -e and -u for the strictest error handling:

bash -euo pipefail script.sh

Or set it at the top of the script:

#!/bin/bash
set -euo pipefail

Debugging

Add the -x flag to the shebang to print each command as the shell executes it. The shell prefixes each command with +:

#!/bin/bash -x

For example, running a script with debugging enabled produces output like this:

+ DIR=.
+ find . -type f
+ read file
+ [[ ./config.sh = *[[:space:]]* ]]

You can also enable debugging for a specific section of a script by wrapping it:

set -x          # enable debugging
mv "$old" "$new"
set +x          # disable debugging

Two other useful options to add to scripts are set -e, which exits immediately on any error, and set -u, which treats unset variables as errors. Together, they catch many common mistakes:

#!/bin/bash
set -eu