Shell Scripting: Automating with Bash

K
Kai··5 min read

When you type the same sequence of commands many times, it's time to write a script. A shell script combines commands (the very ones we've learned all series) into a runnable file, adding variables, conditionals, and loops — turning manual operations into automation. This is the skill that turns "knowing the commands" into "being able to automate".

A minimal script

A script is just a text file containing commands. Create hello.sh:

#!/usr/bin/env bash
echo "Xin chao tu script"

The first line #!/usr/bin/env bash is the shebang — it tells the system to run this file with bash. (You could write #!/bin/bash; env bash is more flexible because it finds bash via PATH.)

Make the file executable (recall Article 7) then run it:

chmod +x hello.sh
./hello.sh

The ./ is needed because the current directory isn't in PATH. Alternatively: bash hello.sh (no chmod +x needed).

Variables

name="Nghia"          # NO spaces around the =
echo "$name"          # use the value: prepend $
echo "${name}_dev"    # braces when you need to delimit the variable name

A classic beginner mistake: writing name = "Nghia" (with spaces) — wrong, bash thinks you're running a command called name. The rule: no spaces around =.

Always quote your variables when using them: "$name". Without quotes, a variable containing spaces or special characters breaks apart — the source of countless script bugs.

Capture a command's output into a variable (command substitution):

today=$(date +%F)
echo "Hom nay la $today"
files=$(ls | wc -l)

Command-line parameters

A script receives parameters when run (./script.sh arg1 arg2):

echo "Tham so 1: $1"        # first parameter
echo "Tham so 2: $2"
echo "Tat ca: $@"           # all parameters
echo "So luong: $#"         # number of parameters
echo "Ten script: $0"

Set a default value when a parameter is empty:

name="${1:-the gioi}"       # if $1 is empty, use "the gioi"

Conditionals: if

if [ -f /etc/os-release ]; then
  echo "co file"
elif [ -d /tmp ]; then
  echo "co thu muc tmp"
else
  echo "khong co"
fi

[ ... ] is the test command. Common checks:

File:    -f (file exists)  -d (directory)  -e (exists)  -r/-w/-x (permission)
String:  "$a" = "$b" (equal)   -z "$a" (empty)   -n "$a" (non-empty)
Number:  -eq (=)  -ne (≠)  -lt (<)  -le (≤)  -gt (>)  -ge (≥)

Note: comparing numbers uses -eq/-lt..., comparing strings uses =. (Bash also has [[ ... ]], which is more powerful — it supports &&, ||, pattern matching — so prefer [[ ]] in bash scripts.)

Loops: for, while

# for: iterate over a list
for i in 1 2 3; do
  echo "lan $i"
done

# for: iterate over files (very commonly used)
for f in *.txt; do
  echo "xu ly $f"
done

# while: loop until the condition is false
n=0
while [ $n -lt 3 ]; do
  echo "n = $n"
  n=$((n + 1))        # arithmetic inside $(( ))
done

for f in *.txt combined with a wildcard (Article 3) is a very common batch-file-processing pattern in operations scripts.

Functions

Bundle reusable logic into a function:

greet() {
  echo "Xin chao, $1"      # $1 is the FUNCTION's parameter (not the script's)
}

greet "Linux"
greet "DevOps"

A function takes parameters via $1, $2... just like a script. Define the function before you call it.

Exit codes: success or failure

Every command returns an exit code: 0 = success, non-0 = error. This is how a script knows whether a command worked.

ls /tmp
echo $?            # print the exit code of the last command (0 if OK)

if grep -q "root" /etc/passwd; then
  echo "tim thay"   # if based on exit code: grep succeeds (0) = found
fi

&& runs the next command if the previous one succeeds; || runs it on failure:

mkdir /tmp/x && echo "tao xong" || echo "tao that bai"

Have a script finish with a given exit code:

exit 0     # report success
exit 1     # report error

Writing safe scripts: set -euo pipefail

Put this line right after the shebang in every serious script:

#!/usr/bin/env bash
set -euo pipefail
  • -e — stop immediately when a command fails (instead of carrying on in a broken state). Verified: a set -e script hitting ls /khong-ton-tai will stop there, not run the following lines.
  • -u — error on the use of an undeclared variable (catches typos in variable names).
  • -o pipefail — a pipeline fails if any command in the pipe fails (by default only the last command counts).

These three flags turn a script that "silently keeps running while broken" into one that "stops and reports right away" — avoiding many baffling problems. It's the mark of a carefully written script.

A complete script

Putting it together — a script that backs up a directory into a date-stamped .tar.gz file (using the knowledge from Article 9):

#!/usr/bin/env bash
set -euo pipefail

src="${1:?Can duong dan thu muc can backup}"   # parameter 1 is required
dest="/backup"
stamp=$(date +%Y%m%d-%H%M%S)
name="$(basename "$src")-$stamp.tar.gz"

mkdir -p "$dest"
tar -czf "$dest/$name" "$src"
echo "Da backup $src -> $dest/$name"

${1:?message} requires the parameter; without it the script stops and prints the message. This is the kind of script you'll write a lot — and Article 17 will make it run automatically on a schedule.

🧹 Cleanup

cd /tmp && rm -f hello.sh demo.sh fail.sh

Wrap-up

A shell script combines commands into a runnable file: shebang + chmod +x, variables (no spaces around =, always "$var"), parameters ($1, $@, ${1:-default}), conditionals (if [[ ]], file/string/number tests), loops (for, while), functions, and exit codes (0 = OK, $?, &&/||). Always open with set -euo pipefail so the script stops on error. This is the tool that turns scattered commands into automation.

The script is written, but you still have to run it by hand. The final article (17) makes them run automatically on a schedule with cron — and wraps up the whole series.