Skip to content
BytePath v0.0.35

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.

Linux automationAvailable

Write small Bash scripts with safe quoting, reusable functions, and explicit error handling.

Difficulty
Beginner to Intermediate
Time
80 min
Practice
Write an input-validating report script
Assessment
Knowledge check

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.

#!/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.

By convention, status 0 means success and nonzero means failure. $? contains the last command’s status, so capture or test it immediately:

Terminal window
if grep -q 'ERROR' "$log_file"; then
printf 'The log contains an error marker.\n'
else
printf 'No matching marker was found.\n'
fi

Use 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.

Terminal window
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 1
done

local 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:

Terminal window
command >result.txt # replace stdout file
command >>result.txt # append stdout
command 2>errors.txt # replace stderr file
command >all.txt 2>&1 # send stdout and stderr to the same file
sort names.txt | uniq -c

Redirections 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.

Command substitution $(command) captures stdout and removes trailing newline characters:

Terminal window
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:

Terminal window
find ./input -type f -print0 |
while IFS= read -r -d '' file; do
printf 'Found: %s\n' "$file"
done

Bash read -d '' is Bash-specific. If portability to another shell matters, state that requirement and choose a POSIX-compatible approach.

#!/usr/bin/env bash
set -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 1
fi

set -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.

This script counts lines in supplied readable files and stops if one cannot be read:

#!/usr/bin/env bash
set -u
if (( $# == 0 )); then
printf 'Usage: %s FILE...\n' "$0" >&2
exit 2
fi
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"
done

Save 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.

  • 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 -e catches every error.
  • Hiding stderr or overwriting reports unintentionally.
  • Running a script with sudo without 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.