Bash logoBashv5.2INTERMEDIATE

Bash Scripting

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

14 min read
bashshellscriptinglinuxclish
Loading your progress

Getting Started

Shebang, making scripts executable, and running them.

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.

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.

$(...) 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/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

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

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

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.

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

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

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.

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.

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.

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