Learning Objectives
- Write and run Bash scripts with a correct shebang and permissions.
- Use variables, environment variables, user input, and command-line arguments.
- Interpret exit status correctly (including command-specific non-zero codes).
- Use conditionals, file tests, and
casefor decision making. - Write
for,while, anduntilloops, plus functions. - Debug with
bash -x/bash -nand understandset -e,set -u, andpipefail. - Schedule scripts with cron and build a practical disk-usage monitor.
What is Shell Scripting?
A shell script is a text file of commands run in sequence by a shell interpreter. Admins use scripts for repetitive work such as backups, monitoring, patching, health checks, log cleanup, and user management.
Shell Architecture
User ──► Shell (bash) ──► Linux Kernel ──► Hardware
Common Linux Shells
| Shell | Description |
|---|---|
| sh | Bourne Shell |
| bash | Bourne Again Shell (Default) |
| ksh | Korn Shell (Common on AIX) |
| zsh | Advanced interactive shell |
| fish | Friendly Interactive Shell |
Your First Script
touch hello.sh
vi hello.sh
#!/bin/bash
echo "Welcome to Linux Shell Scripting"
Run a Script
chmod +x hello.sh
./hello.sh
bash hello.sh
Prefer ./hello.sh (uses the shebang) or bash hello.sh. Avoid sh hello.sh on many Linux systems: sh is often dash, which does not support Bash-only features (arrays, [[ ]], bashisms in general) and can fail silently or with cryptic errors.
Understanding the Shebang
#!/bin/bash
The shebang tells Linux which interpreter should execute the script.
Comments
# This is a comment
Comments improve readability and maintenance.
Variables
NAME="Ashutosh"
echo "$NAME"
Best Practice
Do not place spaces around the '=' operator while assigning variables. Quote expansions ("$NAME") so spaces and glob characters stay safe.
Environment Variables
echo "$HOME"
echo "$USER"
echo "$PATH"
echo "$SHELL"
echo "$0"
ps -p $$ -o comm=
env
| Variable | Description |
|---|---|
| HOME | User home directory |
| USER | Current user |
| PATH | Executable search path |
| SHELL | Login shell (from the account database), not necessarily the shell running right now |
$SHELL is your configured login shell (for example /bin/bash). The interpreter currently running the script or interactive session is separate: check echo "$0", or ps -p $$ -o comm=, for the process that is actually executing.
User Input
read -r NAME
echo "Welcome $NAME"
Command-Line Arguments
#!/bin/bash
# save as script.sh, then: ./script.sh John Delhi
echo "Script: $0"
echo "Name: $1"
echo "City: $2"
echo "Argument count: $#"
echo "All args as one string: $*"
printf 'Arg: %s\n' "$@"
| Parameter | Meaning |
|---|---|
| $0 | Script name |
| $1 | First argument |
| $2 | Second argument |
| $# | Argument count |
| "$*" | All arguments as one string (IFS-joined) |
| "$@" | All arguments as separate words (preserve when looping or passing on) |
With ./script.sh John Delhi, $1 is John and $2 is Delhi. Prefer "$@" when forwarding arguments; use "$*" when you intentionally want one string. Avoid echo "All args: $@" — inside a larger double-quoted string, $@ does not safely expand as separate arguments.
Exit Status
ls /tmp
echo $?
grep "nomatch" /etc/passwd
echo $? # often 1 — pattern not found, not a shell crash
Exit code 0 means success. A non-zero value is defined by the command: it often signals failure, but not always an error in the everyday sense. For example, grep returns 1 when no lines match, and 2 for real errors (bad options, missing files).
Command Substitution
TODAY=$(date)
HOST=$(hostname)
echo "$TODAY"
echo "$HOST"
Arithmetic
A=10
B=20
echo $((A+B))
echo $((A-B))
echo $((A*B))
echo $((A/B))
$((A/B)) uses integer division: $((10/20)) is 0, not 0.5. For floating-point math, use tools such as bc or awk.
Useful Commands
echo
printf
pwd
hostname
whoami
date
clear
exit
Quick System Info Script
#!/bin/bash
echo "Hostname: $(hostname)"
echo "User: $(whoami)"
echo "Date: $(date)"
echo "Uptime: $(uptime -p)"
Decision Making
Conditionals let a script take different actions based on tests. Always quote variables in [ ] tests so empty or spaced values do not break the test.
if Statement
#!/bin/bash
AGE=20
if [ "$AGE" -ge 18 ]
then
echo "Eligible"
fi
if...else
MARKS=45
if [ "$MARKS" -ge 50 ]
then
echo "Pass"
else
echo "Fail"
fi
if...elif...else
SCORE=82
if [ "$SCORE" -ge 90 ]; then
echo "Grade A"
elif [ "$SCORE" -ge 75 ]; then
echo "Grade B"
else
echo "Grade C"
fi
Comparison Operators
| Operator | Meaning |
|---|---|
| -eq | Equal |
| -ne | Not Equal |
| -gt | Greater Than |
| -ge | Greater Than or Equal |
| -lt | Less Than |
| -le | Less Than or Equal |
String Comparisons
NAME="linux"
if [ "$NAME" = "linux" ]
then
echo "Matched"
fi
| Operator | Description |
|---|---|
| = | Equal |
| != | Not Equal |
| -z | Empty String |
| -n | Not Empty |
case Statement
read -r OPTION
case "$OPTION" in
1) echo "Start";;
2) echo "Stop";;
3) echo "Restart";;
*) echo "Invalid";;
esac
Enterprise Insight
case is a common pattern in service wrappers and simple menu-driven admin scripts.
Looping Constructs (for, while, until)
for Loop
for USER in user1 user2 user3
do
echo "$USER"
done
while Loop
COUNT=1
while [ "$COUNT" -le 5 ]
do
echo "$COUNT"
COUNT=$((COUNT+1))
done
until Loop
COUNT=1
until [ "$COUNT" -gt 5 ]
do
echo "$COUNT"
COUNT=$((COUNT+1))
done
until runs while the condition is false — the opposite of while.
Loop Controls (break, continue)
# break example
for i in {1..10}; do
[ "$i" -eq 5 ] && break
echo "$i"
done
# continue example
for i in {1..5}; do
[ "$i" -eq 3 ] && continue
echo "$i"
done
Functions & Reusable Blocks
greet() {
echo "Welcome $1"
}
greet "Linux"
Hands-on Practice
- Create your first Bash script.
- Display today's date and print hostname.
- Accept user input and use command-line arguments.
- Display HOME and PATH variables.
- Perform arithmetic calculations.
Common Mistakes
- Forgetting the shebang.
- Not making the script executable.
- Using spaces around '='.
- Missing quotes around values with spaces.
- Using the wrong interpreter.
File Tests
| Test | Description |
|---|---|
| -f | Regular file |
| -d | Directory |
| -r | Readable |
| -w | Writable |
| -x | Executable |
| -e | Exists |
f="/etc/passwd"
if [ -f "$f" ]; then
echo "$f is a regular file"
fi
Text Processing inside Scripts (grep, awk, sed, cut)
grep root /etc/passwd # Search for a pattern
cut -d: -f1 /etc/passwd # Extract fields from delimited text
awk -F: '{print $1,$3}' /etc/passwd # Print selected fields
sed 's/error/ERROR/g' app.log # Replace text in a stream
Debugging Shell Scripts
bash -x script.sh # Trace each command as it runs
bash -n script.sh # Syntax check only (no execution)
set -e # Exit when a command fails (see caveats below)
set -u # Error on unset variables
set -o pipefail # Pipeline fails if any stage fails
set -e exits when a simple command fails, but failures inside if/while/until tests, or on the left of &&/||, are usually exempt. In a pipeline without pipefail, only the last command’s status matters. set -u catches typos in variable names early, but breaks intentional optional parameters unless you guard them. pipefail is stricter and safer for pipelines, yet can surprise scripts that ignored mid-pipe failures before.
Scheduling Tasks with Cron
crontab -e
# Use absolute paths; cron has a minimal PATH. Redirect output for logging.
0 2 * * * /opt/scripts/backup.sh >>/var/log/backup.log 2>&1
Practical Script Example — Disk Usage Monitor
Prefer machine-readable columns when available. On GNU coreutils, df --output=pcent,target avoids parsing human-oriented fields. The portable fallback below uses df -P and joins mount fields from column 6 to the end of the line so paths with spaces still work.
#!/bin/bash
set -euo pipefail
THRESHOLD=80
# Preferred on GNU df:
# df --output=pcent,target | …
if ! DF_OUT=$(df -P 2>/dev/null); then
echo "Error: df failed" >&2
exit 1
fi
# Process substitution keeps the loop in this shell; || true ignores EOF from read
while read -r USE MOUNT; do
PCT="${USE%\%}"
case "$PCT" in
''|*[!0-9]*) continue ;;
esac
case "$MOUNT" in
/proc|/sys|/dev|/run|/proc/*|/sys/*|/dev/*|/run/*) continue ;;
esac
if [ "$PCT" -ge "$THRESHOLD" ]; then
echo "Warning: $MOUNT is ${PCT}% full"
fi
done < <(echo "$DF_OUT" | awk 'NR>1 {
use=$5
mount=""
for (i=6; i<=NF; i++) mount = (mount ? mount " " : "") $i
print use, mount
}') || true
Commands Covered in This Chapter
bash -n/bash -x— syntax check and execution traceset -e/set -u/set -o pipefail— stricter error handling (with trade-offs)crontab— schedule recurring jobsgrep/awk/sed/cut— search and process textchmod— make a script executabledf -P— portable filesystem usage for monitoring scripts