Bash Scripting Fundamentals
Before you start
Who is this for?
For Linux learners who know basic shell commands and want to automate repeatable tasks safely.
Prerequisites
- Linux Essentials
- Basic terminal use
What you'll be able to do
- Quote values safely
- Use parameters, conditions, loops, and functions
- Handle statuses and streams
- Write a script with validation and error handling
Practical outcome
Produce a reusable report script that preserves filenames with spaces and reports failures.
Write small Bash scripts with safe quoting, reusable functions, and explicit error handling.
Bash reads a script, expands variables and patterns, runs commands, and makes their exit statuses available. A script is a program: quote data, validate assumptions, and test it with harmless inputs before using it on real files or services. The examples follow the GNU Bash Reference Manual.
1. Start with arguments and variables
Section titled “1. Start with arguments and variables”#!/usr/bin/env bash
name=${1:?Usage: $0 NAME}output_dir=${2:-./reports}printf 'Hello, %s\n' "$name"printf 'Output directory: %s\n' "$output_dir"$0 is the script name, $1 and $2 are positional parameters, and ${1:?message} stops with a useful error if the first argument is missing. ${2:-value} uses a default when the second argument is unset or empty. Assignments have no spaces around =. Use "$variable" to pass one value as one argument and avoid word splitting or wildcard expansion.
Single quotes preserve every enclosed character literally; double quotes still allow parameter expansion such as "$name". Prefer printf over echo when exact formatting matters.
2. Exit statuses and conditionals
Section titled “2. Exit statuses and conditionals”By convention, status 0 means success and nonzero means failure. $? contains the last command’s status, so capture or test it immediately:
if grep -q 'ERROR' "$log_file"; then printf 'The log contains an error marker.\n'else printf 'No matching marker was found.\n'fiUse if command; then ... when a nonzero status is an expected branch. [ ... ] and [[ ... ]] test strings or files; arithmetic conditions use (( ... )) in Bash. Remember that grep returns status 1 when it finds no match, which is not the same as a command execution failure for every use case.
3. Functions, loops, and parameters
Section titled “3. Functions, loops, and parameters”report_file() { local file=$1 [[ -r "$file" ]] || { printf 'Cannot read: %s\n' "$file" >&2; return 1; } printf '%s: ' "$file" wc -l < "$file"}
for file in "$@"; do report_file "$file" || exit 1donelocal limits a variable to a function. return gives the caller a status, not arbitrary text; print data to stdout or return it through a file or variable deliberately. for iterates over each positional argument. while is useful for reading input one line at a time.
4. Standard streams, pipes, and redirection
Section titled “4. Standard streams, pipes, and redirection”A process normally has stdin (0), stdout (1), and stderr (2). A pipe sends one command’s stdout to another command’s stdin. Redirection sends a stream to a file or another descriptor:
command >result.txt # replace stdout filecommand >>result.txt # append stdoutcommand 2>errors.txt # replace stderr filecommand >all.txt 2>&1 # send stdout and stderr to the same filesort names.txt | uniq -cRedirections are processed from left to right, so their order matters. 2>&1 points stderr at the current stdout destination. Put 2>&1 after > when both should go to that file. Avoid redirecting diagnostic errors away while you are still troubleshooting.
5. Command substitution and input
Section titled “5. Command substitution and input”Command substitution $(command) captures stdout and removes trailing newline characters:
current_branch=$(git branch --show-current)printf 'Branch: %s\n' "$current_branch"Do not use for item in $(find ...) to process filenames: whitespace and wildcard characters break the list. Use NUL-delimited paths instead:
find ./input -type f -print0 | while IFS= read -r -d '' file; do printf 'Found: %s\n' "$file" doneBash read -d '' is Bash-specific. If portability to another shell matters, state that requirement and choose a POSIX-compatible approach.
6. Error handling and cleanup
Section titled “6. Error handling and cleanup”#!/usr/bin/env bashset -u -o pipefail
work_dir=$(mktemp -d) || { printf 'Could not create temporary directory\n' >&2; exit 1; }cleanup() { rm -rf -- "$work_dir"; }trap cleanup EXIT
if ! cp -- "$1" "$work_dir/input"; then printf 'Copy failed; source was not changed.\n' >&2 exit 1fiset -u catches many unset-variable mistakes; pipefail makes a pipeline report failure when a command before the last one fails. set -e can be surprising in conditionals, functions, and pipelines, so it is not a substitute for explicit checks. A cleanup trap must point only at a directory the script created and owns. Validate required arguments before using them in paths or destructive commands.
7. Practical script: summarize text files
Section titled “7. Practical script: summarize text files”This script counts lines in supplied readable files and stops if one cannot be read:
#!/usr/bin/env bashset -u
if (( $# == 0 )); then printf 'Usage: %s FILE...\n' "$0" >&2 exit 2fi
for file in "$@"; do if [[ ! -f "$file" || ! -r "$file" ]]; then printf 'Not a readable regular file: %s\n' "$file" >&2 exit 1 fi lines=$(wc -l < "$file") || exit 1 printf '%s\t%s\n' "$lines" "$file"doneSave it as line-count.sh, inspect it, then run bash -n line-count.sh to check syntax. Test with a temporary file whose name contains spaces and a nonexistent path. A successful syntax check does not prove that every runtime path behaves correctly.
8. Common mistakes and review checklist
Section titled “8. Common mistakes and review checklist”- Leaving data variables unquoted, which splits names containing spaces.
- Treating every nonzero status as the same kind of failure.
- Reading filenames with command substitution or whitespace splitting.
- Assuming
set -ecatches every error. - Hiding stderr or overwriting reports unintentionally.
- Running a script with
sudowithout a clear need.
Before running a script, identify what files it reads and writes, whether it changes services or permissions, and how to restore the starting state. For service troubleshooting, continue with Linux Services and systemd Logs.