Bash Scripting for Beginners
The moment you type the same three commands twice, you're ready for scripting. A bash script is nothing more mysterious than commands in a file — everything you already know from the terminal works unchanged. This guide covers the seven concepts that turn command-line knowledge into automation.
Your first script
#!/bin/bash
# backup.sh — my first script
echo "Starting backup..."
tar -czf "backup-$(date +%F).tar.gz" ~/documents
echo "Done." Three things to notice. The first line, #!/bin/bash (the shebang), tells the system which interpreter runs the file — it must be line one, exactly. Lines starting with # are comments. And the commands are literally what you'd type interactively.
Make it runnable and run it:
chmod +x backup.sh
./backup.sh The ./ matters: the current directory isn't on the search path, so you point at the script explicitly. (Why chmod +x? See the permissions guide.)
Variables
name="production-db"
backup_dir="/var/backups"
echo "Backing up $name to $backup_dir" Two rules that bite every beginner: no spaces around = (name = "x" is an error — bash thinks name is a command), and quote your variables when you use them: "$name" survives spaces in the value; bare $name splits on them.
Command substitution: $( )
today=$(date +%F)
file_count=$(ls | wc -l)
echo "It's $today and there are $file_count files here" $(command) runs the command and substitutes its output — the bridge between "commands that print things" and "variables that hold things". You already used it in the first script to timestamp the backup name.
Arguments: $1, $2, $@
#!/bin/bash
# greet.sh
echo "Hello, $1!"
echo "All arguments: $@"
echo "Number of arguments: $#" Running ./greet.sh world extra prints "Hello, world!". $1 through $9 are positional arguments, $@ is all of them, $# counts them. This is what turns a script into a reusable tool instead of a hardcoded one.
Conditions: if
#!/bin/bash
if [ -z "$1" ]; then
echo "Usage: $0 filename" >&2
exit 1
fi
if [ -f "$1" ]; then
echo "$1 exists, $(wc -l < "$1") lines"
else
echo "$1 not found" >&2
exit 1
fi The brackets are a command (test) — which is why the spaces inside them are mandatory. The tests you'll use constantly: -f file exists and is a file, -d dir is a directory, -z str string is empty, -n str string is non-empty, and comparisons =, != for strings and -eq, -lt, -gt for numbers.
Loops: for
# Over files
for f in *.log; do
gzip "$f"
done
# Over a range
for i in {1..5}; do
echo "attempt $i"
done
# Over command output
for host in $(cat servers.txt); do
ssh "$host" uptime
done The file loop is the workhorse: anything you can do to one file, a three-line for loop does to a thousand. Combine with the mv or cp patterns and batch renaming stops needing special tools.
Exit codes: how scripts talk to each other
Every command finishes with a status: 0 means success, anything else means failure. $? holds the last one. This is the mechanism behind && and ||:
make build && echo "built OK" || echo "build FAILED"
grep -q "ERROR" app.log && mail -s "errors!" you@example.com < app.log Your own scripts should exit 1 on failure (as the examples above do) so that they compose with && too.
The safety header
Start every non-trivial script with:
#!/bin/bash
set -euo pipefail -e aborts on the first failing command instead of blundering on; -u makes using an unset variable an error (catches typos like $bakup_dir); pipefail makes a pipeline fail if any stage fails, not just the last. These three flags convert silent disasters into loud, early errors — the difference between amateur and production scripts.
A complete real script
#!/bin/bash
set -euo pipefail
# rotate-backups.sh DIR KEEP — keep the newest KEEP backups in DIR
dir="${1:?usage: rotate-backups.sh DIR KEEP}"
keep="${2:-5}"
cd "$dir"
count=$(ls -1 backup-*.tar.gz 2>/dev/null | wc -l)
if [ "$count" -le "$keep" ]; then
echo "Only $count backups, nothing to delete."
exit 0
fi
ls -1t backup-*.tar.gz | tail -n +$((keep + 1)) | xargs -r rm -v
echo "Kept $keep newest backups." Everything from this guide in twelve lines: safety flags, argument handling with defaults, command substitution, a condition, a pipeline (tail + xargs), and clean exit codes. Adapt it — rotation scripts like this one run on every server in existence.
Keep going
Practice the building blocks in the interactive course, keep the cheat sheet handy, and study grep, sed and awk — the three commands that appear in almost every serious script.