Linux
Intermediate 16 min read Linux

Bash Scripting for Automation

Stop typing the same ten commands forever — write them down once and let the machine do the work. Learn how to build reliable bash scripts with shebangs, variables and arguments, conditionals and loops, functions and exit codes, plus the production-grade patterns (set -euo pipefail) that keep automation from failing silently.

bashshell scriptingshebangvariablespositional parametersconditionalsloopsfunctionsexit codesset -euo pipefailautomationlinux
Introduction

What is this?

Bash scripting is writing sequences of shell commands into a file that the shell can execute on demand — from a two-line backup helper to a full deployment pipeline. A script is just a program: it takes arguments, reads variables, makes decisions with conditionals, repeats work with loops, groups logic into functions and reports success or failure through its exit code.

Motivation

Why do you need it?

  • Anything you type more than once deserves a script: backups, deploys, log rotation, environment setup and smoke tests.
  • Scripts are the glue between tools — bash sits at the heart of CI runners, cron jobs, Dockerfile RUN steps and Ansible shells.
  • A script encodes tribal knowledge: the exact sequence of commands that works is preserved, reviewable and repeatable.
  • Bash needs no runtime, no compiler and no dependencies beyond the shell — it runs on every Linux machine, container and cloud VM.
  • A small investment in disciplined patterns (set -euo pipefail, exit codes) turns fragile one-offs into reliable automation that fails loudly instead of silently.
Think about it

Mental model

Imagine

A checklist a pilot reads aloud on every flight

Think of

A bash script run by cron every night

The checklist is the same each time: verify, act, verify, log — and if any step fails, stop and escalate. That is exactly what a hardened bash script does with conditionals, exit codes and set -e: it never skips a step silently, and it tells you loudly when something is wrong.

Mechanics

How it works

  • The shebang (#!/usr/bin/env bash) tells the kernel which interpreter to run the file with, so ./script.sh works without typing bash explicitly.
  • Bash executes the file line by line in a child shell, keeping variables and working directory changes isolated from your interactive session.
  • Positional parameters ($1, $2, ...) map to the arguments you pass on the command line, and $? always holds the exit status of the last command.
  • Conditionals (if/test) and loops (for/while) are syntax that controls the order and repetition of command execution.
  • An exit code of 0 reports success; any non-zero value reports failure. set -euo pipefail makes the script terminate automatically when a command or pipeline fails.

How a bash script executes from invocation to exit status.

You invoke
Shebang + shell
Variables & arguments
Conditionals & loops
Exit status
Reference

Key commands

bash -n deploy.sh

Syntax-check a script without executing it. No output means the syntax is valid.

(no output)
echo "$HOME"

Print the value of a variable. Quoting prevents word-splitting and globbing.

/home/dev
[ -f /etc/passwd ] && echo "passwd exists"

The test builtin checks a condition; && runs the echo only if the test succeeds.

passwd exists
for i in {1..3}; do echo "item $i"; done

A for loop over a brace-expanded range, printing each value.

item 1
item 2
item 3
set -u; echo "$UNDEFINED"

With -u (nounset) set, referencing an unset variable aborts the shell.

set -o pipefail; false | echo "after pipe"

Without pipefail the pipeline succeeds; with it, the pipeline inherits the failure of false.

Hands-on

Examples

A deployment script that fails loudly instead of silently

A deployment script that fails loudly instead of silently
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="${1:-/opt/myapp}"
BACKUP_DIR="${2:-/var/backups}"
NOW="$(date +%Y%m%d-%H%M%S)"
backup() {
mkdir -p "$BACKUP_DIR"
cp -r "$APP_DIR" "$BACKUP_DIR/$APP_DIR-$NOW"
echo "backup created at $BACKUP_DIR/$APP_DIR-$NOW"
}
deploy() {
mkdir -p "$APP_DIR"
echo "release $(date +%H%M)" > "$APP_DIR/release.txt"
echo "deployed release $(date +%H%M) to $APP_DIR"
}
for service in web worker; do
systemctl is-active --quiet "$service" || {
echo "$service is not running - aborting" >&2
exit 1
}
done
backup
deploy
echo "deploy script finished"

set -euo pipefail turns silent failures into hard stops: an undefined variable aborts (u), a failed command aborts (e) and a failed command inside a pipeline aborts (pipefail). Arguments fall back to defaults with ${1:-...}, the for loop gates the deploy on both services being active, and the explicit exit 1 hands a clear signal to cron or CI.

Real world

In production

  • Deploy and release scripts: build, copy artifacts, run migrations, restart services — executed by cron or CI runners.
  • Backup and rotation jobs that tar a directory, upload it and clean up old copies, always logging and exiting non-zero on failure.
  • Health-check and self-healing wrappers: restart a service if a probe fails, capped with retry limits.
  • Environment bootstrap scripts (provisioning) that install packages, create users and apply config on fresh VMs.
  • Database maintenance and ETL glue — dump, transform with awk/sed, and load — glued together where heavy tools are overkill.
Guidelines

Best practices

  • Start every script with #!/usr/bin/env bash and set -euo pipefail.
  • Quote every variable expansion: "$VAR" — unquoted expansions silently split and glob.
  • Use ${VAR:-default} for optional inputs and validate required arguments at the top of the script.
  • Prefer clear names and functions for logic that is used more than once; keep each function doing one job.
  • Always exit non-zero on failure and write a meaningful message to stderr with >&2.
  • Run shellcheck on every script in CI — it catches quoting, POSIX and syntax bugs a human misses.
Watch out

Common mistakes

  • Spaces around = in assignments: name = "alice" runs the command name, not an assignment.
  • Forgetting to make the script executable, then failing with ./script.sh: Permission denied.
  • Using $@ unquoted, which splits arguments; use "$@" to preserve each argument as one word.
  • Writing [ "$x" = "y" ] with && inside [ ], which is invalid — use [ "$x" = "y" ] && ... outside.
  • Relying on set -e alone while a failing command is hidden inside a pipeline — add set -o pipefail.
  • Not redirecting cron script output, so errors vanish into cron mail and nobody reads them.
Root cause analysis

Common issues

Error
./deploy.sh: Permission denied
Root cause

The file is not executable, or the filesystem is mounted noexec.

Fix

chmod +x deploy.sh, or run bash deploy.sh. For noexec mounts, run from a normal directory.

Error
deploy.sh: line 3: $APP_DIR: unbound variable
Root cause

set -u is active and APP_DIR was never assigned before use.

Fix

Assign defaults with "${1:-/opt/myapp}" or set APP_DIR before it is referenced.

Error
The script continues even though a command failed inside a pipeline
Root cause

set -e does not catch failures of commands that are not the last in a pipeline.

Fix

Add set -o pipefail so the pipeline fails if any stage fails.

When things break

Debugging tips

  • bash -n script.sh checks syntax only; bash -x script.sh traces every command and its expansion.
  • Add set -x temporarily inside the script to see values as they are evaluated, then remove it.
  • echo "DEBUG: VAR=$VAR" before and after the suspect block to see where the value changes.
  • Run shellcheck and fix every warning before debugging behavior — most bash bugs are lint-visible.
  • Reduce: isolate the failing line in a tiny test script, then grow it back one line at a time.
Interviews

Interview checkpoint

Practice

Hands-on lab

Write, harden and run your first automation script
15 min · intermediate
In progress
Objectives
  • Create a bash script with a shebang, variables and positional arguments
  • Use a conditional and a loop inside the script
  • Make the script executable and run it both ways
  • Add set -euo pipefail and confirm the script stops on failure

Write, harden and run your first automation script

15 min · intermediate

0/6 steps
0%
sandbox · Linux sandbox
$
Try these commands
Test yourself

Quick quiz

0/6
1

What is the purpose of the shebang line #!/usr/bin/env bash?

2

Which test checks whether a file exists in a bash conditional?

3

With set -e active, a script exits immediately the moment any command returns a non-zero status.

4

In echo "${NAME:-world}", what is printed when the variable NAME is unset?

5

You run ./deploy.sh prod api. Inside the script, what does $1 hold?

6

Your script runs curl | jq and the jq step fails, but set -e never triggers and the script keeps going. Which single line fixes this?

Quick reference

Cheat sheet

Script structure
#!/usr/bin/env bashShebang — run this file with bash
bash script.shRun a script explicitly
bash -n script.shSyntax check without running
bash -x script.shTrace every executed line
chmod +x script.shMake a script executable
Variables & arguments
name="alice"Assign (no spaces around =)
echo "$name"Use a variable (quote it!)
$1 $2 $3Positional arguments
$@ / $#All arguments / count of arguments
$?Exit status of the last command
${var:-default}Fallback value if empty
Conditionals & loops
if [ -f "$f" ]; then ...Test a file exists
[ "$x" = "y" ]String equality test
[ "$n" -gt 5 ]Numeric greater-than test
for i in {1..10}; do ...; doneIterate a range
while read -r line; do ...; done < fileRead a file line by line
Production hardening
set -euo pipefailExit on error, unset var, pipe failure
set -xDebug trace output
exit 1Return a non-zero status
local x=1Scoped variable inside a function
trap "cleanup" EXITRun cleanup on exit
Go deeper

Official documentation