Bash logoBashv5.2INTERMEDIATE

Bash Scripting

Bash scripting reference - variables, quoting, parameter expansion, conditionals, loops, functions, arrays, and safe scripting patterns.

14 min read
bashshellscriptinglinuxclish

Sign in to mark items as known and track your progress.

Sign in

Getting Started

Shebang, making scripts executable, and running them.

Shebang & Running

Create, permit, and run a script.

bash
# Quick Reference
#!/usr/bin/env bash
echo "Hello, World!"

# make it runnable, then run
chmod +x script.sh
./script.sh
💡 #!/usr/bin/env bash finds bash on PATH - more portable than a hardcoded /bin/bash.
⚡ A script needs the executable bit (chmod +x) to run as ./script.sh.
📌 Run without +x via "bash script.sh" - the interpreter is explicit there.
🟢 # starts a comment to end of line; the shebang is only special on line 1.
basicsshebang

Variables & Quoting

Assigning values and controlling expansion with quotes.

Variables

Assignment, usage, and scope.

bash
# Quick Reference
name="Sam"          # NO spaces around =
echo "$name"        # use with $
echo "${name}"      # braces for clarity/adjacency
💡 No spaces around = in assignments: name="Sam" (name = "Sam" runs a command).
⚡ Use ${var} braces when the name touches other text: ${name}_backup.
📌 Declare function variables with local so they do not leak into the global scope.
🟢 export makes a variable visible to child processes; plain assignment stays in the shell.
variablesscope

Quoting

Single vs double quotes and why quoting matters.

bash
# Quick Reference
echo "$var"       # double: expands variables
echo '$var'       # single: literal, no expansion
echo "${arr[@]}"  # always quote expansions
💡 Double quotes expand $var and $(...); single quotes keep everything literal.
⚡ Always quote "$var" and "${arr[@]}" - unquoted values word-split and glob-expand.
📌 "$(cmd)" captures a command's output; prefer $(...) over legacy backticks.
🟢 Unquoted variables holding paths with spaces are the #1 source of shell bugs.
quotingexpansion

Parameters & Special Variables

Script arguments and built-in variables.

Positional & Special Params

Access arguments and shell state.

bash
# Quick Reference
$0   # script name
$1 $2 ...   # positional args
$@   # all args (each quoted)
$#   # arg count
$?   # last exit status
$$   # current PID
💡 Prefer "$@" (quoted) to iterate args - it preserves each argument's boundaries.
⚡ $? holds the previous command's exit code: 0 means success, non-zero means failure.
📌 shift drops $1 and renumbers the rest - handy for processing args one by one.
🟢 "$@" keeps arguments separate; "$*" joins them into a single word.
argumentsspecial-vars

Substitution & Arithmetic

Capturing output and doing math.

Command Substitution & Arithmetic

$(...) and $((...)).

bash
# Quick Reference
files="$(ls | wc -l)"     # capture command output
sum=$(( 3 + 4 ))          # arithmetic -> 7
(( count++ ))             # arithmetic evaluation
💡 $(( )) evaluates integer math; inside it, variables need no $ prefix.
⚡ Bash arithmetic is integer-only - use bc or awk for floating-point.
📌 (( expr )) returns success/failure by the result, so it chains with && and ||.
🟢 Prefer $(cmd) over backticks - it nests cleanly: $(dirname "$(pwd)").
arithmeticsubstitution

Conditionals

Tests, comparisons, and branching.

if & Test Expressions

if/elif/else with [[ ]].

bash
# Quick Reference
if [[ "$x" == "yes" ]]; then
    echo "ok"
elif [[ -z "$x" ]]; then
    echo "empty"
else
    echo "no"
fi
💡 Use [[ ]] (bash) over [ ] (POSIX test) - it handles empty vars, &&, and patterns safely.
⚡ == does glob-pattern matching in [[ ]]; =~ does regex matching.
📌 String tests: == != -z (empty) -n (non-empty). Numeric: -eq -ne -lt -gt -le -ge.
🟢 cmd && success || failure is a compact if/else, but beware if success itself can fail.
conditionalstest

File & String Tests

Common test operators.

bash
# Quick Reference
[[ -f file ]]   # regular file exists
[[ -d dir ]]    # directory exists
[[ -z "$s" ]]   # string is empty
[[ "$a" == "$b" ]]  # strings equal
💡 -f tests a regular file; -e tests any path; -d tests a directory.
⚡ Always quote the value in string tests ([[ -z "$s" ]]) to handle empties safely.
📌 Use -eq/-lt/-gt for numbers and ==/</> for strings - mixing them miscompares.
🟢 (( n > 5 )) reads more naturally than [[ "$n" -gt 5 ]] for pure number checks.
testsfiles

case Statements

Multi-way branching on patterns.

case

Match a value against patterns.

bash
# Quick Reference
case "$1" in
    start) echo "starting" ;;
    stop)  echo "stopping" ;;
    *)     echo "usage: start|stop" ;;
esac
💡 case matches glob patterns (not regex): * ? [..] and | for alternatives.
⚡ Each branch ends with ;; - forgetting it is a common syntax error.
📌 The *) branch is the catch-all default - put it last.
🟢 case is cleaner than a long if/elif chain when branching on one value.
casebranching

Loops

Iterating with for, while, and until.

for Loops

Iterate lists, ranges, and globs.

bash
# Quick Reference
for item in a b c; do echo "$item"; done
for i in {1..5}; do echo "$i"; done
for f in *.txt; do echo "$f"; done
💡 Loop over globs (for f in *.txt) directly - never parse ls output.
⚡ {1..5} generates a range; {0..10..2} adds a step. Ranges cannot use variables.
📌 Guard glob loops with [[ -e "$f" ]] || continue in case nothing matches.
🟢 Quote "$@" in for arg in "$@" so arguments with spaces stay intact.
loopsfor

while, until & select

Condition-driven loops and reading input.

bash
# Quick Reference
while [[ $n -lt 5 ]]; do (( n++ )); done
until [[ -f ready ]]; do sleep 1; done
while read -r line; do echo "$line"; done < file
💡 while IFS= read -r line; do ... done < file is THE safe way to read lines.
⚡ IFS= prevents trimming whitespace; -r stops backslash mangling in read.
📌 until is while's inverse - it loops until the condition succeeds.
🟢 break leaves the loop entirely; continue jumps to the next iteration.
loopswhileread

Functions

Reusable blocks with arguments and return values.

Functions

Define, pass arguments, and return results.

bash
# Quick Reference
greet() {
    local name="$1"
    echo "Hello, $name"
}
greet "Sam"
result="$(greet "Sam")"   # capture the echoed output
💡 Function args are $1, $2, ... just like script args - not named parameters.
⚡ return sets an exit status (0-255) for success/failure; echo + $(...) returns data.
📌 Declare internal variables local so functions do not clobber global state.
🟢 ${1:-default} gives a parameter a fallback when the caller omits it.
functions

Arrays

Indexed and associative arrays.

Indexed Arrays

Ordered, zero-based lists.

bash
# Quick Reference
arr=(a b c)
echo "${arr[0]}"      # a
echo "${arr[@]}"      # all elements
echo "${#arr[@]}"     # length -> 3
arr+=(d)              # append
💡 ${arr[@]} expands to all elements; ${#arr[@]} is the count.
⚡ Always quote "${arr[@]}" in loops so elements with spaces stay intact.
📌 Arrays are zero-indexed; ${arr[-1]} grabs the last element (bash 4.3+).
🟢 arr+=(item) appends; ${!arr[@]} lists the indices (useful for sparse arrays).
arraysindexed

Associative Arrays

Key-value maps (bash 4+).

bash
# Quick Reference
declare -A ages
ages[sam]=30
echo "${ages[sam]}"       # 30
echo "${!ages[@]}"        # keys
💡 Associative arrays require declare -A first - otherwise keys are treated as 0.
⚡ ${!map[@]} gives keys; ${map[@]} gives values; ${#map[@]} gives the count.
📌 Associative arrays need bash 4+ (macOS ships bash 3.2 - install a newer bash).
🟢 [[ -v map[key] ]] tests whether a key exists without inserting it.
arraysassociative

Parameter Expansion

Transform variables inline - no external commands.

Defaults, Length & Substrings

Fallbacks and slicing.

bash
# Quick Reference
${var:-default}   # use default if unset/empty
${var:=default}   # assign default if unset
${#var}           # length
${var:0:3}        # substring (offset, length)
💡 ${var:-def} substitutes a default without changing var; ${var:=def} also assigns it.
⚡ ${#var} is the length; ${var:offset:length} slices a substring.
📌 ${var:?msg} aborts the script with msg if var is unset - a cheap required-arg guard.
🟢 ${var^^} / ${var,,} upper/lowercase the whole string (bash 4+), no tr needed.
parameter-expansiondefaults

Pattern Removal & Replace

Trim prefixes/suffixes and substitute.

bash
# Quick Reference
${file#*/}     # remove shortest leading match
${file##*/}    # remove longest (-> basename)
${file%.*}     # remove shortest trailing (-> drop ext)
${var/old/new} # replace first match
💡 # / ## strip from the FRONT (shortest/longest); % / %% strip from the BACK.
⚡ ${f##*/} is a fast pure-bash basename; ${f%/*} is a pure-bash dirname.
📌 ${v/old/new} replaces the first match; ${v//old/new} replaces all (double slash).
🟢 These run in-process - far faster than calling sed/basename in a loop.
parameter-expansionstring

Input & Output

Reading input and writing/redirecting output.

read Input

Prompt for and parse user input.

bash
# Quick Reference
read -r -p "Name: " name
read -rsp "Password: " pass    # -s hides input
read -ra parts <<< "a b c"     # split into an array
💡 Always use read -r - without it, backslashes in input are silently eaten.
⚡ -p prints a prompt, -s hides typed input (passwords), -t sets a timeout.
📌 read -ra arr <<< "$str" splits a string into an array on whitespace.
🟢 read returns non-zero on EOF/timeout - chain with || for a fallback value.
inputread

Output & Redirection

echo, printf, and streams.

bash
# Quick Reference
echo "text"
printf "%s = %d\n" "x" 42
cmd > out.txt      # stdout to file (overwrite)
cmd >> out.txt     # append
cmd 2> err.txt     # stderr to file
💡 Prefer printf over echo for formatting and portability - echo flags vary across shells.
⚡ > truncates, >> appends; 2> redirects stderr; 2>&1 merges stderr into stdout.
📌 Order matters: cmd > f 2>&1 works; cmd 2>&1 > f does NOT merge as expected.
🟢 &>/dev/null silences a command completely (both stdout and stderr).
outputredirection

Here-docs & Process Substitution

Multi-line input and treating output as files.

Heredocs, Herestrings & Process Substitution

Feed blocks of text and command output.

bash
# Quick Reference
cat <<EOF
multi-line
text with $var expanded
EOF

grep foo <<< "$string"          # here-string
diff <(sort a) <(sort b)        # process substitution
💡 <<EOF expands $vars inside; <<'EOF' (quoted delimiter) keeps everything literal.
⚡ <<< "$str" (here-string) is a quick way to pipe one string into a command's stdin.
📌 <(cmd) turns command output into a temp file path - great for diff <(a) <(b).
🟢 <<-EOF strips leading tabs (only tabs, not spaces) so you can indent the block.
heredocprocess-substitution

Exit Codes & Error Handling

Fail fast and clean up reliably.

set -euo pipefail & trap

Strict mode and cleanup handlers.

bash
# Quick Reference
set -euo pipefail       # strict mode (put near the top)
trap 'echo "cleanup"' EXIT
trap 'echo "error on line $LINENO"' ERR
💡 Start scripts with set -euo pipefail so failures stop the script instead of cascading.
⚡ trap ... EXIT guarantees cleanup (temp files, locks) runs on every exit path.
📌 With -u, reference optional vars as ${VAR:-} to avoid an unbound-variable error.
🟢 Send error messages to stderr with >&2 so they are not captured by $(...).
error-handlingsettrap

Globbing & Brace Expansion

Filename patterns and generated sequences.

Globs & Braces

Match files and generate strings.

bash
# Quick Reference
*.txt        # any name ending .txt
file?.log    # single-char wildcard
{a,b,c}      # brace expansion -> a b c
{1..5}       # sequence -> 1 2 3 4 5
💡 Globs match existing files; brace expansion just generates text (files need not exist).
⚡ Enable ** recursive matching with shopt -s globstar.
📌 By default an unmatched glob stays literal - set shopt -s nullglob to get nothing instead.
🟢 mkdir -p base/{a,b,c} is a quick way to create several directories at once.
globbingbrace-expansion

Argument Parsing

Handle script options and flags.

getopts

Parse single-letter options.

bash
# Quick Reference
while getopts "vo:" opt; do
    case "$opt" in
        v) verbose=1 ;;
        o) out="$OPTARG" ;;
        *) echo "usage..."; exit 1 ;;
    esac
done
💡 In the optstring, a trailing colon (o:) means that option expects an argument in $OPTARG.
⚡ After the loop, shift $((OPTIND-1)) removes options so $@ holds the positional args.
📌 getopts handles only short options (-v); for --long flags, parse with a while/case loop.
🟢 A leading colon in the optstring (":vo:") enables silent error handling you control.
getoptsarguments

Debugging & Best Practices

Find bugs and write robust scripts.

Debugging & Safety

Tracing, linting, and guidelines.

bash
# Quick Reference
bash -x script.sh        # trace every command
set -x                   # trace on
set +x                   # trace off
shellcheck script.sh     # static analysis
💡 Run scripts through ShellCheck - it flags quoting, unset vars, and portability bugs.
⚡ bash -x (or set -x) traces each command with its expanded values - the fastest debugger.
📌 Quote every expansion, use [[ ]] over [ ], and $(...) over backticks by default.
🟢 : "${1:?usage...}" is a one-line guard that aborts when a required argument is missing.
debuggingbest-practices