askesis

Bash scripting: 50 cards on quoting, tests, loops and error handling

Bash scripting flashcards: shebangs, quoting, exit codes, tests, loops, functions, arrays, parameter expansion, redirection, set -euo pipefail and traps.

IT & DevOpsBeginner50 cardsFree

Practice now

Study without an account

0 of 50 seen0 graduated
What does #!/usr/bin/env bash on the first line do?

Scheduled with the default Askesis timings. Progress is saved in this browser.

All 50 cards

  1. 1
    What does #!/usr/bin/env bash on the first line do?
    This shebang tells the kernel which interpreter runs the script; env finds bash on the PATH, which is more portable than a fixed #!/bin/bash
    It only applies when you run the file directly (./script.sh), which also needs execute permission.
  2. 2
    #!/bin/sh vs #!/usr/bin/env bash
    sh is a POSIX shell (dash on Debian and Ubuntu) without Bash features such as [[ ]], arrays and ${var//a/b}; use a Bash shebang whenever a script relies on them
  3. 3
    Running ./script.sh vs source script.sh
    Running starts a child process, so its variables and cd vanish when it ends; source (or .) runs the file in the current shell, so its changes persist
    Source files that set environment variables or define functions for your current shell.
  4. 4
    What’s wrong with name = "Ada"?
    The spaces make Bash run a command called name with the arguments = and Ada. Assignments take no spaces: name="Ada"
  5. 5
    Single vs double quotes
    Single quotes keep everything literal; double quotes still expand $var, $(command) and $((arithmetic)) but prevent word splitting and globbing
    bash
    name="Ada"
    echo 'Hi $name'   # Hi $name
    echo "Hi $name"   # Hi Ada
  6. 6
    Why quote variables, as in rm "$file"?
    Unquoted expansions are split on whitespace and glob-expanded: with file="my report.txt", rm $file tries to delete my and report.txt
    Quote every expansion unless you specifically want splitting. ShellCheck flags missing quotes.
  7. 7
    Store a command’s output in a variable
    bash
    today=$(date +%F)
    lines=$(wc -l < access.log)
    Prefer $(...) to backticks: it nests and reads more clearly. Trailing newlines are stripped.
  8. 8
    Integer arithmetic in Bash
    bash
    total=$(( price * qty ))
    (( count++ ))
    echo $(( 7 / 2 ))   # 3
    Bash arithmetic is integer-only; use bc or awk for decimals.
  9. 9
    $0, $1 and $#
    The script’s name, its first argument, and the number of arguments
    From the tenth argument on, use braces: ${10}.
  10. 10
    "$@" vs "$*"
    "$@" expands to each argument as a separate word, keeping spaces inside arguments intact; "$*" joins them all into one string
    To pass arguments on to another command, you almost always want "$@".
  11. 11
    What does shift do?
    Drops $1, moves the remaining arguments down by one ($2 becomes $1) and decreases $#
    shift 2 drops two. It is common in argument-parsing loops.
  12. 12
    Parse short options such as -v and -f file
    bash
    while getopts "vf:" opt; do
      case $opt in
        v) verbose=1 ;;
        f) file="$OPTARG" ;;
        *) usage; exit 2 ;;
      esac
    done
    shift $((OPTIND - 1))
    A colon after a letter means the option takes a value. getopts only handles short options.
  13. 13
    Exit with a usage message when the first argument is missing
    bash
    env="${1:?usage: deploy.sh <env>}"
    ${var:?message} prints the message to stderr and exits with a non-zero status if the variable is unset or empty.
  14. 14
    Exit status conventions
    0 means success and 1–255 failure; 2 often signals misuse, 126 a command that isn’t executable, 127 command not found, and 128 + N death by signal N (130 after Ctrl+C)
    A script exits with exit N, or else with the status of its last command.
  15. 15
    Why save $? right away?
    Every command, even echo or [, overwrites it, so store it immediately: cmd; status=$?
    Often you don’t need $? at all: test the command directly with if.
  16. 16
    Run a block only if grep finds a match
    bash
    if grep -q "ERROR" app.log; then
      echo "errors found"
    fi
    if tests a command’s exit status, so there is no need for [ $? -eq 0 ].
  17. 17
    Fail early when a required tool is missing
    bash
    command -v jq >/dev/null || { echo "jq is required" >&2; exit 1; }
    command -v is built in and portable, unlike which.
  18. 18
    [ ] vs [[ ]]
    [ is an ordinary command (POSIX test), so unquoted variables inside it are split and globbed. [[ ]] is Bash syntax: no splitting, plus &&, ||, glob patterns and regex matching with =~
  19. 19
    Why is [[ 10 < 9 ]] true?
    Inside [[ ]], < and > compare strings alphabetically, and “10” sorts before “9”. For numbers use -lt, -gt and friends, or (( 10 < 9 ))
    The numeric operators are -eq, -ne, -lt, -le, -gt and -ge.
  20. 20
    Common file tests
    -e exists, -f regular file, -d directory, -s non-empty file, and -r, -w, -x readable, writable, executable
    bash
    [[ -f .env ]] && source .env
  21. 21
    -z vs -n
    [[ -z $var ]] is true when the string is empty; [[ -n $var ]] is true when it is not
  22. 22
    Match a regex and extract its groups
    bash
    re='^([0-9]+)\.([0-9]+)$'
    if [[ $version =~ $re ]]; then
      major=${BASH_REMATCH[1]} minor=${BASH_REMATCH[2]}
    fi
    Keep the regex in a variable and don’t quote it after =~: a quoted pattern is matched as a literal string.
  23. 23
    Branch on a value with case
    bash
    case "$1" in
      start)      start_app ;;
      stop)       stop_app ;;
      -h|--help)  usage ;;
      *)          echo "unknown command: $1" >&2; exit 2 ;;
    esac
    Patterns are globs, | separates alternatives and *) is the default branch.
  24. 24
    Loop over all .log files in a directory
    bash
    for f in *.log; do
      gzip "$f"
    done
    If nothing matches, the loop runs once with the literal string *.log; shopt -s nullglob makes it run zero times instead.
  25. 25
    Count from 1 to n when n is a variable
    bash
    for (( i = 1; i <= n; i++ )); do
      echo "$i"
    done
    Brace expansion like {1..5} happens before variables are expanded, so {1..$n} doesn’t work.
  26. 26
    Read a file line by line
    bash
    while IFS= read -r line; do
      printf '%s\n' "$line"
    done < input.txt
    IFS= keeps leading and trailing whitespace and -r keeps backslashes. Don’t loop over $(cat file).
  27. 27
    A counter set inside cmd | while read … is 0 afterwards. Why?
    Each part of a pipeline runs in a subshell, so the loop’s variables disappear with it. Feed the loop with process substitution instead: done < <(cmd)
    Alternatively, shopt -s lastpipe runs the last part of a pipeline in the current shell (in scripts, where job control is off).
  28. 28
    Wait until a service is healthy
    bash
    until curl -fsS http://localhost:8080/health >/dev/null; do
      sleep 2
    done
    until loops as long as the command fails. Add a retry limit in real scripts.
  29. 29
    Run two jobs in parallel and wait for both
    bash
    build_frontend &
    build_backend &
    wait
    Plain wait returns 0 even if a job failed. Save each PID from $! and run wait "$pid" to get that job’s exit status.
  30. 30
    Define a function with a local variable
    bash
    greet() {
      local name="$1"
      echo "Hello, $name"
    }
    
    greet "Ada"
    Without local, variables set in a function are global. Inside a function, $1, $2 and $@ are the function’s own arguments.
  31. 31
    How does a Bash function return data?
    It prints the data and the caller captures it: result=$(get_version). return only sets an exit status from 0 to 255
    Larger values wrap around: return 300 gives a status of 44.
  32. 32
    Indexed arrays
    bash
    files=(a.txt "b c.txt")
    files+=(d.txt)
    echo "${files[0]}"    # a.txt
    echo "${#files[@]}"   # 3
    for f in "${files[@]}"; do echo "$f"; done
    Quote "${files[@]}" so elements containing spaces stay whole.
  33. 33
    Associative arrays
    bash
    declare -A port=([web]=80 [db]=5432)
    echo "${port[db]}"   # 5432
    for name in "${!port[@]}"; do echo "$name"; done
    They need Bash 4 or newer; macOS still ships Bash 3.2 as /bin/bash.
  34. 34
    ${var:-default} vs ${var:=default}
    Both give default when var is unset or empty; := also assigns it to var
    Without the colon, as in ${var-default}, only an unset variable triggers the default, not an empty one.
  35. 35
    Get the file name, directory and extension from a path
    bash
    path=/var/log/app/error.log
    echo "${path##*/}"   # error.log
    echo "${path%/*}"    # /var/log/app
    echo "${path%.*}"    # /var/log/app/error
    echo "${path##*.}"   # log
    # removes the shortest matching prefix and ## the longest; % and %% do the same for suffixes.
  36. 36
    Replace text inside a variable
    ${var/old/new} replaces the first match; ${var//old/new} replaces all of them
    bash
    f="annual report.txt"
    echo "${f// /_}"   # annual_report.txt
  37. 37
    String length and substrings
    ${#var} is the length; ${var:offset:length} extracts a substring, as in ${commit:0:7}
  38. 38
    Print an error message to stderr
    bash
    echo "config file not found" >&2
    Keep stdout for real output, so script | other-tool isn’t polluted by messages.
  39. 39
    cmd > out.log 2>&1 vs cmd 2>&1 > out.log
    Redirections apply left to right. The first sends both streams to the file; the second points stderr at the terminal (where stdout was at that moment) and only then moves stdout to the file
    To pipe errors along with output: cmd 2>&1 | grep -i error.
  40. 40
    What does &> do?
    Redirects both stdout and stderr, so cmd &> /dev/null silences a command completely
    It is Bash shorthand for > file 2>&1; &>> appends.
  41. 41
    Write a multi-line file with a here-document
    bash
    cat > app.conf <<EOF
    host=$HOST
    port=8080
    EOF
    Variables expand inside it. Quote the delimiter (<<'EOF') to keep the text literal, or use <<-EOF to strip leading tabs.
  42. 42
    Compare the sorted contents of two files without temp files
    bash
    diff <(sort a.txt) <(sort b.txt)
    Process substitution, <(cmd), exposes a command’s output as a file path.
  43. 43
    Send everything a script prints to a log file and the screen
    bash
    exec > >(tee -a deploy.log) 2>&1
    Placed near the top, it applies to all later output of the script.
  44. 44
    What does set -euo pipefail do?
    -e exits on the first failing command, -u makes expanding an unset variable an error, and pipefail makes a pipeline fail if any command in it fails
    A common first line after the shebang in scripts that should stop on errors.
  45. 45
    When doesn’t set -e stop a script on a failing command?
    When the command is an if, while or until condition, part of an && or || list (except the last), negated with !, or masked by local, as in local out=$(cmd)
    Declare and assign separately so failures count: local out; out=$(cmd).
  46. 46
    Without pipefail, what is the status of false | true?
    0, because a pipeline returns the status of its last command. With set -o pipefail it returns the status of the rightmost command that failed
    ${PIPESTATUS[@]} lists the status of every command in the last pipeline.
  47. 47
    Always delete a temp file when the script exits
    bash
    tmp=$(mktemp)
    trap 'rm -f "$tmp"' EXIT
    The EXIT trap runs on a normal end, an exit, a set -e failure or Ctrl+C. Nothing can trap kill -9 (SIGKILL).
  48. 48
    Report the line number where a script failed
    bash
    trap 'echo "Error on line $LINENO" >&2' ERR
    Single quotes delay expansion until the trap runs. Add set -E so functions inherit the trap.
  49. 49
    Trace a script while it runs
    bash
    bash -x deploy.sh   # trace the whole script
    set -x              # trace from here on
    set +x              # stop tracing
    Each command is printed after expansion, prefixed with +. ShellCheck catches many quoting and logic bugs before you run anything.
  50. 50
    Get the directory the script lives in
    bash
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    Use it to load files that sit next to the script, wherever it is run from.

Frequently asked questions

How is this different from the Linux and Git & terminal decks?

Those cover commands such as find, grep, pipes and permissions. This deck is about the Bash language itself: variables, quoting, control flow, functions and error handling in scripts.

Will these scripts work in sh or zsh?

Not always. Features such as [[ ]], arrays, pattern substitution in variables and process substitution are Bash-specific, so use a Bash shebang or stick to POSIX features when a script must run under sh.

Does it work on macOS?

Mostly. macOS ships the old Bash 3.2 and uses zsh as its default shell; associative arrays and a few other features need Bash 4 or newer, which you can install with Homebrew.

Intermediate

Linux command line

Linux commands and files for sysadmins and LPIC-1 candidates: find and grep, permissions and special bits, fstab, systemd targets, package managers, users, cron and more.

50 cards
Beginner

Linux filesystem basics

Learn the Linux directory tree: what /etc, /var, /usr, /proc and /dev are for, plus passwd, shadow, fstab and hosts. Free flashcards, no signup.

50 cards
Intermediate

Intermediate Linux filesystem

Linux paths admins use daily: /usr/local, /var/lib, /run, /sys, /etc/skel, sudoers, Bash startup files, SSH keys, resolv.conf and /proc. Free flashcards.

50 cards
Advanced

Advanced Linux filesystem

Deeper Linux files for LPIC-1: profile.d, sudoers.d, systemd units, GRUB config, nsswitch.conf, ld.so.conf, distro logs and /lib/modules. Free flashcards.

50 cards
Beginner

Vim

Learn Vim from the keyboard up: modes, motions, operators and text objects, search and :s substitutions, registers, macros, visual mode and windows.

50 cards
Beginner

Networking

Computer networking fundamentals: OSI and TCP/IP layers, TCP vs UDP, common port numbers, subnetting and CIDR, DNS, DHCP and ARP, and IPv6 basics.

50 cards
Beginner

Cyber security

Cyber security flashcards: the CIA triad, phishing, XSS, CSRF, SQL injection, MITM, hashing vs encryption, TLS, MFA, least privilege, OWASP and zero trust.

50 cards
Beginner

Docker

Learn Docker from images and layers to multi-stage builds, volumes, networks and Compose, with the commands and best practices you use daily.

50 cards
Intermediate

Kubernetes

Kubernetes fundamentals for developers and CKA or CKAD candidates: Pods, Deployments, Services, storage, probes, resources and scheduling, plus the kubectl commands you use daily.

50 cards
Beginner

AWS

AWS flashcards: regions and AZs, EC2, S3 storage classes, IAM users, roles and policies, VPC networking, RDS, Lambda, CloudWatch and Route 53.

50 cards
Beginner

Terraform

Terraform flashcards: HCL, providers, resources, variables and outputs, state and remote backends, plan and apply, modules, count, for_each, drift and import.

50 cards