From terminal commands to reliable automation
#!/bin/bash and set -euo pipefailPortable glue for DevOps automation
entrypoint.sh)build.sh, deploy.sh)Most common bugs come from expansion and quoting.
Shebang + strict mode + execution
#!/bin/bash
set -euo pipefail # fail fast: command errors, undefined vars, pipeline failures
echo "Hello, DevOps World!" # write a line to stdout
#!/bin/bash: choose Bash interpreter explicitly-e: stop on command failure-u: fail on undefined variables-o pipefail: fail if any pipeline command failschmod +x hello.sh # add execute bit so file can run as a program
./hello.sh # run script from current directory
chmod: change file permissions+x: add execute permission./: run file from current directoryhello.sh: target file receiving execute bitPermission denied#!/bin/bash
set -euo pipefail
log_file="/tmp/my-script.log" # store logs in /tmp for safe testing
log_info() {
echo "[INFO] $*" | tee -a "$log_file" # print and append to log file
}
if [ "$#" -lt 1 ]; then # $# is number of script arguments
echo "Usage: $0 <name>" # $0 is script name
exit 1 # non-zero exit status means failure
fi
name="$1" # first positional argument
log_info "Hello, $name"
Structure: defaults → functions → validation → main logic
#!/bin/bash
set -euo pipefail
name="Alice" # regular shell variable
export ROLE="student" # exported var visible to child processes
echo "Hi $name ($ROLE)" # expand variables inside double quotes
env | sort # list environment variables alphabetically
export passes variable to child processesenv | sort lists env variables alphabeticallyread -r -p "Enter your name: " name # -r keeps backslashes literal, -p prints prompt
src="$1" # first CLI argument
dst="$2" # second CLI argument
if [ -z "$src" ] || [ -z "$dst" ]; then # -z checks empty string, || is logical OR
echo "Usage: $0 <source> <destination>"
exit 1
fi
cp -r -- "$src" "$dst" # -r recursive copy, -- ends option parsing
-r with read: keep backslashes literal-p: show prompt$1 and $2: first and second positional arguments[ -z "$src" ]: true when string is empty||: logical OR between test conditionscp -r: recursive copy for directories--: end of options (safe path handling)"$@"files=("$@") # save all args as array, preserving spaces
for f in "${files[@]}"; do # iterate safely over array items
echo "Processing: $f"
done
"$@": each argument preserved separately$*: merges all args into one string$HOME becomes its value$(pwd) becomes command output*.log match filenamesname="Ali Veli"
echo "Hello $name" # double quotes: expand vars, keep one argument
echo '$HOME' # single quotes: literal text, no expansion
file="My Notes.txt"
rm "$file" # quoted path stays one argument
rm $file # unquoted path may split on spaces (unsafe)
"$var", "$@".
"..." allows $var and $(cmd) expansion'...' is literal text, no expansionMost bugs happen in the final two steps when values are unquoted.
#!/bin/bash
set -euo pipefail
read -r -p "Enter a directory: " dir
if [ -d "$dir" ]; then # -d: true if path exists and is a directory
echo "Directory exists."
elif [ -e "$dir" ]; then # -e: true if path exists (any type)
echo "Path exists, but is not a directory."
else
echo "Path not found."
fi
-d directory exists, -e path exists
-f file exists, -r readable, -w writable.
servers=(web01 web02 db01) # Bash array
for s in "${servers[@]}"; do # "${arr[@]}" keeps each element separate
echo "Processing $s"
done
count=0
while [ "$count" -lt 3 ]; do # -lt means numeric "less than"
date # print current date/time
count=$((count + 1)) # arithmetic expansion
sleep 1 # pause 1 second
done
"${servers[@]}" preserves each item safely$((...)) does integer arithmetic-lt means "less than" in numeric comparisonssleep 1 waits for 1 second between iterations#!/bin/bash
set -euo pipefail
backup_path() {
local src="$1" # local variable only inside function
local dst="$2"
tar -czf "$dst" -- "$src" # -c create, -z gzip, -f archive name
}
backup_path "$HOME/Documents" "/tmp/docs.tgz"
local keeps variables inside the functiontar -czf: create gzip archive filemkdir /root/test # likely fails without privileges
echo $? # exit code of previous command (0 success, non-zero fail)
0 means successcommand > out.log # redirect stdout, overwrite file
command >> out.log # redirect stdout, append file
command 2> err.log # redirect stderr (fd 2) only
command > all.log 2>&1 # send stderr to same target as stdout
command1 | command2 # pipe stdout of command1 into stdin of command2
stdout = normal output, stderr = errors2>&1 sends stderr to the same place as stdout> overwrites file, >> appends to file2> redirects only error stream| pipes stdout of left command to stdin of right commandpipefail# without pipefail:
false | true # left command fails, right succeeds
echo $? # returns 0 from last command (misleading)
# with set -o pipefail:
set -o pipefail # pipeline status becomes first failing command
false | true
echo $? # now returns non-zero
pipefail, failed commands can be hidden in pipelines.
bash -n script.sh # parse only, do not execute commands
bash -x script.sh # execute with command trace
set -x # enable tracing inside script
# ... commands ...
set +x # disable tracing
require_cmd() {
command -v "$1" >/dev/null 2>&1 || { # command exists? hide output/errors
echo "[ERROR] Missing command: $1" >&2 # send error message to stderr
exit 1
}
}
require_cmd tar # verify dependency before main logic
require_cmd gzip
command -v checks if command is available in PATH>/dev/null 2>&1 hides both normal output and errors|| { ... } runs error block when check failsSafer defaults + validation + logging
#!/bin/bash
set -euo pipefail
dest="${1:-/tmp/backups}" # use arg1 or default path
shift || true # drop arg1; '|| true' avoids strict-mode exit when no args
sources=("$@") # remaining args become source array
if [ "${#sources[@]}" -eq 0 ]; then # array length check
sources=("$HOME/Documents" "/etc") # fallback source paths
fi
mkdir -p -- "$dest" # create destination if missing
host="$(hostname -s)" # short hostname
stamp="$(date +%Y-%m-%d-%H%M%S)" # timestamp for unique archive names
archive="$dest/${host}-${stamp}.tgz"
for p in "${sources[@]}"; do
if [ ! -e "$p" ]; then # ! negates test; -e means path exists
echo "[WARN] Skipping missing path: $p" >&2 # warning to stderr
fi
done
tar -czf "$archive" -- "${sources[@]}" # create gzip tar from all sources
echo "[OK] Backup complete: $archive"
${1:-/tmp/backups}: default value when arg 1 is missingshift: drops first positional argument${#sources[@]}: array length! -e: path does not exist>&2: write warning to stderr-czf in tar: create + gzip + filenamechmod +x backup.sh # make script executable
./backup.sh /tmp/my-backups "$HOME/Documents" /etc # pass destination and source paths
/tmp first while learningdest="${1:-/tmp/backups}": default destinationshift || true: consume first arg safely in strict modesources=("$@"): collect remaining args as pathsmkdir -p -- "$dest": ensure destination existshostname -s + date +...: predictable archive names>&2: send warnings to error stream/tmp, then scale up.
bash -n script.shbash -x script.sh/tmp)Suggested exercises
health-check.sh for disk + memory thresholdsbash -x and fix one intentional bug#!/bin/bash in course scriptsset -euo pipefail in scripts"$var", "$@"Next step: build one small script that replaces a manual task you do every day.