Bash scripting is where Linux command-line knowledge becomes automation. A Bash script can validate environment state, deploy an application, rotate logs, back up files, or glue together tools that do one job each.
Bash is not the best language for every task, but it is excellent for lightweight automation that lives close to the operating system.
A script usually begins with a shebang line that tells the system which interpreter to use.
#!/usr/bin/env bashSave the script, make it executable, and run it:
chmod +x deploy.sh
./deploy.shVariables store values.
name="devops"
env="production"
echo "$name"Bash also provides positional parameters for command-line arguments.
echo "$1"
echo "$2"
echo "$#"$1 is the first argument$2 is the second argument$# is the number of arguments$@ represents all argumentsExample:
./backup.sh /var/log nginxInside the script, $1 might be /var/log and $2 might be nginx.
if, elif, and elseUse conditionals to make decisions.
if [ -f /etc/nginx/nginx.conf ]; then
echo "Nginx config exists"
elif [ -d /etc/nginx ]; then
echo "Nginx directory exists"
else
echo "Nginx not installed"
fiCommon test operators:
-f file exists-d directory exists-z string is empty-n string is not emptyfor and whileA for loop repeats over a list.
for host in app1 app2 app3; do
echo "Checking $host"
doneA while loop repeats while a condition is true.
count=1
while [ "$count" -le 3 ]; do
echo "Attempt $count"
count=$((count + 1))
doneLoops are common in operations scripts that process files, services, or hosts.
Functions group logic into reusable units.
log_info() {
echo "[INFO] $1"
}
check_file() {
if [ -f "$1" ]; then
echo "Found: $1"
else
echo "Missing: $1"
fi
}Then call them:
log_info "Starting check"
check_file /etc/hostsFunctions improve readability and reduce duplication.
Bash supports arrays.
services=(nginx sshd docker)
echo "${services[0]}"
echo "${services[@]}"Basic string operations are also useful.
filename="app.log"
echo "${filename%.log}"
echo "${filename/app/service}"These features help with small automation tasks without calling external tools for everything.
Every command returns an exit code. 0 usually means success. Non-zero means failure.
mkdir testdir
echo $?In scripts, you often check exit codes directly or stop early on failure.
cp config.yaml config.yaml.bak || exit 1You can also end your own script with a chosen exit code:
exit 0
exit 1Bash scripts interact with three standard streams:
Example:
echo "normal message"
echo "error message" >&2Redirection lets you control where output goes.
./script.sh > output.log 2> error.log
./script.sh < input.txtThis is important when integrating scripts into CI pipelines or scheduled jobs.
traptrap lets you run cleanup code when a script exits or receives a signal.
cleanup() {
echo "Cleaning up"
}
trap cleanup EXITThis is useful for removing lock files, stopping background jobs, or printing final status messages.
Tip:
trapis a simple way to make scripts safer and more professional.
#!/usr/bin/env bash
if [ "$#" -lt 1 ]; then
echo "Usage: $0 <directory>" >&2
exit 1
fi
target_dir="$1"
if [ ! -d "$target_dir" ]; then
echo "Directory not found: $target_dir" >&2
exit 2
fi
echo "Listing files in $target_dir"
ls -lah "$target_dir"This script demonstrates positional parameters, conditionals, stderr, and exit codes.
#!/usr/bin/env bash
services=(nginx sshd docker)
for service in "${services[@]}"; do
if systemctl is-active --quiet "$service"; then
echo "$service is running"
else
echo "$service is not running" >&2
fi
doneThis type of script is realistic for server health reporting.
"$1" to avoid word-splitting bugsBash becomes much easier when you treat it as structured automation, not just a long list of commands.
Question: In the tutorial, what does $# represent inside a Bash script?
<MCQExercise exercise={{
id: 'linux-bash-scripting-q1',
title: 'Exercise 1: Counting script arguments',
description: 'Identify what the special parameter $# contains in Bash.',
options: [
{ id: 'a', text: 'The exit code of the previous command' },
{ id: 'b', text: 'The path to the current script only' },
{ id: 'c', text: 'The number of command-line arguments passed to the script' },
{ id: 'd', text: 'All arguments joined into a single environment variable named #' },
],
correctId: 'c',
explanation: 'The lesson introduces $# as the argument count alongside $1, $2, and $@. That makes it useful for validating whether the caller provided enough inputs before the script continues.',
wrongExplanations: {
a: 'The previous command exit code is stored in $?, not $#.',
b: 'The script path is available through other means such as $0. $# is specifically the count of positional arguments.',
d: 'All arguments are represented by $@, not by $#.',
},
}} />
Scenario: Your script wants to print a usage message when no directory argument is provided.
Question: Why does the example use >&2 for that message?
<MCQExercise exercise={{
id: 'linux-bash-scripting-q2',
title: 'Exercise 2: Writing to stderr',
description: 'Choose why a Bash script redirects an error message to file descriptor 2.',
options: [
{ id: 'a', text: 'Because file descriptor 2 is stderr, which keeps error output separate from normal output.' },
{ id: 'b', text: 'Because file descriptor 2 always writes into /etc/hosts.' },
{ id: 'c', text: 'Because >&2 converts the script into an executable file.' },
{ id: 'd', text: 'Because stderr is the same stream as stdin, so input and errors stay together.' },
],
correctId: 'a',
explanation: 'The tutorial explains the three standard streams and uses >&2 to send error text to stderr. Keeping errors separate from normal output is important when scripts are piped, logged, or integrated into CI jobs.',
wrongExplanations: {
b: 'File descriptor 2 refers to the standard error stream, not a specific file on disk.',
c: 'Executability is controlled by the shebang and permissions such as chmod +x, not by stderr redirection.',
d: 'stdin is file descriptor 0, not 2. stderr is a separate output stream.',
},
}} />
Question: What does trap cleanup EXIT do in the lesson's example?
<MCQExercise exercise={{
id: 'linux-bash-scripting-q3',
title: 'Exercise 3: Registering exit cleanup',
description: 'Identify what the trap statement accomplishes when the script exits.',
options: [
{ id: 'a', text: 'It prevents the script from ever exiting.' },
{ id: 'b', text: 'It renames the cleanup function to EXIT.' },
{ id: 'c', text: 'It sends cleanup output to stdin.' },
{ id: 'd', text: 'It tells Bash to run the cleanup function when the script exits.' },
],
correctId: 'd',
explanation: 'The tutorial presents trap as a safety tool for running cleanup logic on exit. That pattern is useful for removing lock files, stopping background jobs, or printing final status even when the script ends unexpectedly.',
wrongExplanations: {
a: 'trap does not block exiting. It lets you attach logic that runs when exiting happens.',
b: 'The function name does not change. EXIT is the event that triggers the existing function.',
c: 'trap is about signal and exit handling, not about redirecting output streams.',
},
}} />
set -euo pipefail. Without it, a failed command is ignored and the script carries on."$file" handles spaces and empty values; bare $file breaks on both.[[ ]] over [ ] in bash. Safer with empty variables and it supports pattern matching.$? is the exit code of the last command, and 0 means success. Check it or let set -e do it.trap 'cleanup' EXIT runs your cleanup whether the script succeeds, fails or is interrupted."$@" preserves arguments with spaces; $* and bare $@ do not.shellcheck. It catches most real bugs before you do.[ is a command whose arguments happen to look like syntax, so an unquoted empty variable turns [ $x = "y" ] into [ = "y" ] and it fails with a syntax error. [[ ]] is bash syntax rather than a command: it handles empty variables safely, supports && and || inside, and adds pattern matching ([[ $f == *.txt ]]) and regex (=~). Use [[ ]] in anything with a bash shebang; [ is only needed for strict POSIX sh scripts.
Because unquoted variables are word-split on whitespace and glob-expanded. rm $file with file="my document.txt" tries to delete two files, neither of which exists; rm "$file" deletes the right one. An empty unquoted variable disappears entirely, which can turn a command into something quite different from what you intended. The rule is simple enough to apply without thinking: always "$var", and always "$@" for arguments. shellcheck flags every violation.
Every command sets $? to its exit code: 0 for success, non-zero for failure. Test it directly with if command; then, which reads better than comparing $?, or capture it immediately with rc=$? if you need the value, since the next command overwrites it. With set -e the script exits automatically on failure, and the idiom for "this one is allowed to fail" is command || true. In your own scripts, exit 1 on error so callers and CI can detect it.
trap runs a command when the shell receives a signal or exits. The standard use is cleanup: trap 'rm -rf "$tmpdir"' EXIT deletes a temporary directory whether the script finishes normally, fails under set -e, or is interrupted with Ctrl-C. Without it, an early exit leaves temporary files, lock files and half-finished state behind. Trapping EXIT covers all those cases at once, which is why it is preferable to trapping individual signals.
Bash is excellent at running other programs and piping between them, if the script is mostly command invocations and a little control flow, it is the right tool. Move to Python once you need data structures, JSON or arithmetic beyond counting, error handling more nuanced than exit codes, or once the script passes a few hundred lines. The honest signal is associative arrays and nested quoting: by the time you are fighting bash's syntax rather than using it, the rewrite has already paid for itself.
Install software on any distro: apt versus apt-get, remove versus purge, fixing dpkg lock errors, dnf and rpm on Red Hat systems, and when snap is worth it.
Learn Linux environment variables: shell versus exported variables, .bashrc versus .bash_profile, extending PATH safely and why sudo drops your environment.
Schedule jobs with crontab: the five time fields, common examples, why a script that works manually fails in cron, capturing output, and systemd timers.