0% found this document useful (0 votes)
2 views25 pages

Bash Shell Tutorial

This document is a comprehensive tutorial on Bash shell scripting, covering topics from basic concepts to advanced control structures. It includes sections on creating scripts, using variables, handling user input, performing arithmetic and string operations, and implementing conditional statements and loops. The tutorial is designed for users of GNU/Linux and macOS, providing practical examples and best practices throughout.

Uploaded by

amosmaritim123
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views25 pages

Bash Shell Tutorial

This document is a comprehensive tutorial on Bash shell scripting, covering topics from basic concepts to advanced control structures. It includes sections on creating scripts, using variables, handling user input, performing arithmetic and string operations, and implementing conditional statements and loops. The tutorial is designed for users of GNU/Linux and macOS, providing practical examples and best practices throughout.

Uploaded by

amosmaritim123
Copyright
© All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd

Bash Shell Scripting

A Complete Tutorial — From Basics to Advanced Control Structures

All scripts use #!/bin/bash and run on GNU/Linux & macOS

■ Table of Contents
1. Introduction to Shell Scripting
2. Your First Bash Script
3. Variables & Data Types
4. User Input & Arguments
5. Arithmetic & String Operations
6. Conditional Statements (if / elif / else / case)
7. Loops — for, while, until, select
8. Functions
9. Arrays
10. File & Directory Operations
11. Regular Expressions & Text Processing
12. Process Management & Signal Handling
13. Advanced Topics & Best Practices
1. Introduction to Shell Scripting
A shell is a command-line interpreter that provides a user interface to the Unix/Linux operating system. Bash
(Bourne Again SHell) is the most widely used shell on Linux systems and is the default on most distributions and
macOS. A shell script is simply a text file containing a sequence of shell commands that are executed one after
another.

Why Learn Bash Scripting?


• Automate repetitive system administration tasks

• Manage files, processes, and system resources

• Write deployment and build pipelines (CI/CD)

• Parse logs and transform text data

• Schedule jobs with cron and at

• Glue together other programs and tools

The Shebang Line


Every Bash script should start with a shebang line. This special first line tells the operating system which interpreter
to use when the file is executed directly.

#!/bin/bash
#
# Every script in this tutorial starts with the line above.
# The '#!' is called a 'shebang' or 'hashbang'.
# /bin/bash is the absolute path to the Bash interpreter.

■ Note: Always use /bin/bash (not /bin/sh) to ensure Bash-specific features work correctly. On some systems sh is
a different shell (dash, ksh, etc.).

Making a Script Executable

# Create the file


touch [Link]

# Add execute permission for the owner


chmod +x [Link]

# Run it
./[Link]

# Or run it explicitly with bash (no chmod needed)


bash [Link]
2. Your First Bash Script
Let's write a classic Hello World script and then progressively make it more interesting.

#!/bin/bash
# Script: [Link]
# Description: Our very first Bash script

echo 'Hello, World!'


echo 'Welcome to Bash scripting!'
echo "Today is: $(date '+%A, %B %d %Y')"
echo "You are running as: $(whoami)"
echo "Current directory: $(pwd)"

Comments
Lines beginning with # are comments and are ignored by Bash. Good comments explain why, not what.

#!/bin/bash
# ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■
# Script : [Link]
# Author : Your Name
# Date : 2025-01-01
# Purpose : Demonstrate comments and basic output
# ■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■

# This is a single-line comment


echo 'Running [Link] ...' # Inline comment

: '
This is a multi-line comment block.
Everything between the colon-quote and the closing
quote is treated as a no-op string.
'
echo 'Done!'

echo vs printf

#!/bin/bash

# echo — simple, adds newline automatically


echo 'Simple output'
echo -n 'No newline at end' # -n suppresses newline
echo -e 'Tab:\there Newline:\nhere' # -e enables escapes

# printf — powerful, C-style formatting


printf 'Name: %-15s Age: %d\n' 'Alice' 30
printf 'Pi is approximately %.4f\n' 3.14159265
printf '%05d\n' 42 # Pad with zeros → 00042
3. Variables & Data Types
Bash variables are untyped by default — they store strings. Variable names are case-sensitive and may contain
letters, digits, and underscores (but must not start with a digit).

#!/bin/bash

# ■■ Assigning variables (NO spaces around '=') ■■


name='Alice'
age=30
city='Nairobi'
pi=3.14159

# ■■ Accessing variables with $ ■■


echo "Name : $name"
echo "Age : $age"
echo "City : $city"

# ■■ Curly braces disambiguate ■■


fruit='apple'
echo "I have ${fruit}s" # → I have apples
echo "I have $fruits" # → error: fruits undefined

# ■■ Command substitution ■■
today=$(date '+%Y-%m-%d')
files=$(ls | wc -l)
echo "Date: $today | Files: $files"

# ■■ Read-only (constant) variables ■■


readonly MAX_SIZE=1024
readonly DB_HOST='localhost'

Variable Scope — local vs global

#!/bin/bash

GLOBAL_VAR='I am global'

my_function() {
local LOCAL_VAR='I am local'
echo "Inside: GLOBAL=$GLOBAL_VAR"
echo "Inside: LOCAL=$LOCAL_VAR"
}

my_function
echo "Outside: GLOBAL=$GLOBAL_VAR"
echo "Outside: LOCAL=${LOCAL_VAR:-'(empty)'}"
# LOCAL_VAR is empty here because it was declared local
Special / Automatic Variables
Variable Meaning

$0 Name of the script

$1 … $9 Positional parameters (arguments)

$@ All arguments as separate words

$* All arguments as a single word

$# Number of arguments

$? Exit status of last command (0 = success)

$$ PID of the current shell / script

$! PID of last background process

$_ Last argument of the previous command

$RANDOM Random integer 0–32767

$LINENO Current line number in the script

$HOME Current user's home directory

$PATH Executable search path

$USER Current username

$HOSTNAME Machine hostname

$OLDPWD Previous working directory


4. User Input & Arguments

Reading User Input with read

#!/bin/bash

# Basic read
echo -n 'Enter your name: '
read name
echo "Hello, $name!"

# read with prompt flag (-p)


read -p 'Enter your age: ' age
echo "You are $age years old."

# read with timeout (-t) and silent mode (-s for passwords)
read -s -p 'Enter password: ' password
echo # newline after silent input
echo "Password length: ${#password} chars"

# read multiple values at once


read -p 'Enter first and last name: ' first last
echo "First: $first Last: $last"

# read into an array


read -a colours -p 'Enter colours (space-separated): '
echo "First colour: ${colours[0]}"

Command-Line Arguments
#!/bin/bash
# Usage: ./[Link] Alice 30 'Software Engineer'

echo "Script name : $0"


echo "1st arg : $1"
echo "2nd arg : $2"
echo "3rd arg : $3"
echo "All args : $@"
echo "Arg count : $#"

# Loop through all arguments


echo '--- All arguments ---'
for arg in "$@"; do
echo " → $arg"
done

# Shift — discard $1 and shift remaining args left


echo "Before shift: \$1=$1"
shift
echo "After shift: \$1=$1"

getopts — Parsing Flags

#!/bin/bash
# Usage: ./[Link] -n Alice -a 30 -v

verbose=false
name=''
age=''

while getopts ':n:a:v' opt; do


case $opt in
n) name=$OPTARG ;;
a) age=$OPTARG ;;
v) verbose=true ;;
:) echo "Option -$OPTARG requires an argument"; exit 1 ;;
?) echo "Unknown option: -$OPTARG"; exit 1 ;;
esac
done

$verbose && echo '[verbose mode ON]'


echo "Name: $name Age: $age"
5. Arithmetic & String Operations

Integer Arithmetic

#!/bin/bash

a=10 b=3

# Method 1: $(( )) — arithmetic expansion (preferred)


echo "Add : $((a + b))"
echo "Subtract : $((a - b))"
echo "Multiply : $((a * b))"
echo "Divide : $((a / b))" # integer division
echo "Modulo : $((a % b))"
echo "Power : $((a ** b))"

# Increment / decrement
counter=0
((counter++)) # post-increment
((counter+=5)) # add 5
echo "Counter: $counter"

# Method 2: expr (older, slower)


result=$(expr $a \* $b) # escape * to avoid glob
echo "expr result: $result"

# Method 3: let
let 'result = a * b + 1'
echo "let result: $result"

# Floating-point requires bc or awk


echo 'scale=4; 22/7' | bc # → 3.1428
awk 'BEGIN{printf "%.6f\n", 22/7}'

String Operations
#!/bin/bash

str='Hello, World!'

# Length
echo "Length : ${#str}" # → 13

# Substring ${var:start:length}
echo "Substr : ${str:7:5}" # → World
echo "From end : ${str: -6:5}" # → World

# Case conversion (Bash 4+)


echo "Upper : ${str^^}"
echo "Lower : ${str,,}"
echo "First upper: ${str^}"

# Pattern removal
file='/home/alice/[Link]'
echo "Remove prefix : ${file#*/}" # home/alice/[Link]
echo "Remove all : ${file##*/}" # [Link] (basename)
echo "Remove suffix : ${file%.*}" # /home/alice/report
echo "Remove ext : ${file%%.*}" # /home/alice/report

# Substitution ${var/pattern/replacement}
echo "Replace first : ${str/l/L}" # HeLlo, World!
echo "Replace all : ${str//l/L}" # HeLLo, WorLd!

# Default values
echo "Value or default: ${unset_var:-'default'}"
echo "Set if empty : ${var:='assigned'}"

# Concatenation
greeting='Hello'
name='Alice'
full="${greeting}, ${name}!"
echo "$full"
6. Conditional Statements
Bash offers several ways to make decisions: if/elif/else, case, and short-circuit operators && and ||.

if / elif / else

#!/bin/bash

read -p 'Enter a number: ' num

if [[ $num -gt 100 ]]; then


echo 'Greater than 100'
elif [[ $num -gt 50 ]]; then
echo 'Greater than 50'
elif [[ $num -gt 0 ]]; then
echo 'Positive number'
elif [[ $num -eq 0 ]]; then
echo 'Zero'
else
echo 'Negative number'
fi

Test Operators
Operator Type Meaning

-eq Integer Equal

-ne Integer Not equal

-lt Integer Less than

-le Integer Less than or equal

-gt Integer Greater than

-ge Integer Greater than or equal

= String Equal

!= String Not equal

-z String Empty (zero length)

-n String Non-empty

=~ String Matches regex (inside [[ ]])

-e File File/dir exists

-f File Regular file

-d File Directory

-r / -w / -x File Readable / Writable / Executable


Operator Type Meaning

-s File File size > 0

-L File Symbolic link

! expr Logic NOT

&& / || Logic AND / OR

case Statement

#!/bin/bash

read -p 'Enter a fruit name: ' fruit

case $fruit in
apple | Apple)
echo 'A red or green fruit'
;;
banana)
echo 'A yellow fruit'
;;
mango | papaya)
echo 'A tropical fruit'
;;
[0-9]*)
echo 'That looks like a number, not a fruit!'
;;
*)
echo "Unknown fruit: $fruit"
;;
esac

Short-circuit Operators

#!/bin/bash

# && runs right side ONLY if left side succeeds (exit 0)


mkdir -p /tmp/mydir && echo 'Directory created'

# || runs right side ONLY if left side fails (exit != 0)


rm /tmp/no_such_file 2>/dev/null || echo 'File not found — OK'

# Combine to mimic if/else on one line


[[ -f '/etc/passwd' ]] && echo 'passwd exists' || echo 'Missing!'
7. Loops — for, while, until, select

for Loop

#!/bin/bash

# ■■ Classic list-style for ■■


for colour in red green blue yellow; do
echo "Colour: $colour"
done

# ■■ C-style for (like C/Java) ■■


for ((i=1; i<=5; i++)); do
printf 'Square of %d = %d\n' $i $((i*i))
done

# ■■ Brace expansion (sequence) ■■


for n in {1..10}; do
echo -n "$n "
done; echo

# ■■ Step in brace expansion {start..end..step} ■■


for n in {0..20..5}; do
echo -n "$n "
done; echo

# ■■ Loop over files ■■


for file in /etc/*.conf; do
echo "Config: $(basename $file)"
done

# ■■ Loop over command output ■■


for user in $(cut -d: -f1 /etc/passwd | head -5); do
echo "User: $user"
done

while Loop
#!/bin/bash

# ■■ Count from 1 to 5 ■■
counter=1
while [[ $counter -le 5 ]]; do
echo "Counter: $counter"
((counter++))
done

# ■■ Read a file line by line ■■


while IFS= read -r line; do
echo "Line: $line"
done < /etc/hostname

# ■■ Infinite loop with break ■■


while true; do
read -p 'Type quit to exit: ' input
[[ $input == 'quit' ]] && break
echo "You typed: $input"
done
echo 'Goodbye!'

# ■■ while reading pipeline ■■


ls -1 /tmp | while read -r fname; do
echo "Found: $fname"
done

until Loop

#!/bin/bash
# until runs while condition is FALSE (opposite of while)

n=10
until [[ $n -le 0 ]]; do
echo -n "$n "
((n--))
done
echo 'Blast off!'

# Wait until a file appears


until [[ -f /tmp/[Link] ]]; do
echo 'Waiting for /tmp/[Link] ...'
sleep 2
done
echo 'Flag found — continuing!'

Loop Control — break & continue


#!/bin/bash

# break — exit the loop immediately


for i in {1..10}; do
[[ $i -eq 6 ]] && break
echo -n "$i "
done; echo # → 1 2 3 4 5

# continue — skip to next iteration


for i in {1..10}; do
(( i % 2 == 0 )) && continue # skip even numbers
echo -n "$i "
done; echo # → 1 3 5 7 9

# break N — break out of N nested loops


for i in 1 2 3; do
for j in A B C; do
[[ $i -eq 2 && $j == 'B' ]] && break 2
echo "$i-$j"
done
done
echo 'After nested break'

select Loop (Interactive Menus)

#!/bin/bash

PS3='Choose your shell: ' # custom prompt for select


options=('bash' 'zsh' 'fish' 'ksh' 'Quit')

select shell in "${options[@]}"; do


case $shell in
Quit)
echo 'Exiting.'
break
;;
'')
echo 'Invalid selection.'
;;
*)
echo "You chose: $shell"
;;
esac
done
8. Functions
Functions let you encapsulate reusable logic. In Bash, functions are defined before they are called and share the
global environment unless variables are declared local.

#!/bin/bash

# ■■ Two equivalent syntaxes ■■


greet() {
echo "Hello, $1!"
}

function farewell {
echo "Goodbye, $1!"
}

greet 'Alice'
farewell 'Bob'

# ■■ Return values ■■
# Bash functions return an exit code (0-255), not a value.
# To return data, use stdout + command substitution.

add() {
echo $(( $1 + $2 ))
}
result=$(add 7 13)
echo "7 + 13 = $result"

# ■■ Return exit code ■■


is_even() {
(( $1 % 2 == 0 ))
}

is_even 4 && echo '4 is even' || echo '4 is odd'


is_even 7 && echo '7 is even' || echo '7 is odd'

Advanced Function Patterns


#!/bin/bash

# ■■ Default parameter values ■■


greet() {
local name=${1:-'World'}
local greeting=${2:-'Hello'}
echo "$greeting, $name!"
}

greet # → Hello, World!


greet 'Alice' # → Hello, Alice!
greet 'Bob' 'Howdy' # → Howdy, Bob!

# ■■ Recursive functions ■■
factorial() {
local n=$1
[[ $n -le 1 ]] && echo 1 && return
echo $(( n * $(factorial $((n - 1))) ))
}
echo "5! = $(factorial 5)" # → 120

# ■■ Function libraries — source other files ■■


# In [Link]: log() { echo "[$(date '+%H:%M:%S')] $*"; }

# source ./[Link] # loads the library


# . ./[Link] # identical short form
# log 'Script started'
9. Arrays

Indexed Arrays

#!/bin/bash

# ■■ Declaration & assignment ■■


fruits=('apple' 'banana' 'cherry' 'date')
declare -a colours # explicit declaration (optional)
colours[0]='red'
colours[1]='green'
colours[2]='blue'

# ■■ Access elements ■■
echo "${fruits[0]}" # apple
echo "${fruits[2]}" # cherry
echo "${fruits[-1]}" # date (last element, Bash 4.3+)

# ■■ All elements ■■
echo "${fruits[@]}" # apple banana cherry date
echo "${fruits[*]}" # same but affected by IFS

# ■■ Array length ■■
echo "${#fruits[@]}" # 4

# ■■ Slice ${arr[@]:start:length} ■■
echo "${fruits[@]:1:2}" # banana cherry

# ■■ Append ■■
fruits+=('elderberry' 'fig')
echo "${fruits[@]}"

# ■■ Iterate ■■
for fruit in "${fruits[@]}"; do
echo " - $fruit"
done

# ■■ Iterate with index ■■


for i in "${!fruits[@]}"; do
printf '[%d] %s\n' $i "${fruits[$i]}"
done

# ■■ Delete element ■■
unset 'fruits[1]'
echo "After delete: ${fruits[@]}"

Associative Arrays (Dictionaries)


#!/bin/bash
# Requires Bash 4+

declare -A person
person['name']='Alice'
person['age']=30
person['city']='Nairobi'

# Access
echo "Name: ${person['name']}"
echo "Age : ${person['age']}"

# All keys
echo "Keys : ${!person[@]}"

# All values
echo "Values: ${person[@]}"

# Iterate
for key in "${!person[@]}"; do
printf '%-10s: %s\n' "$key" "${person[$key]}"
done
10. File & Directory Operations

#!/bin/bash

# ■■ Check and create ■■


TARGET='/tmp/demo_dir'

[[ -d $TARGET ]] || mkdir -p "$TARGET"


echo 'Directory ready'

# ■■ Write to a file ■■
cat > "$TARGET/[Link]" << 'EOF'
Line 1: Hello from heredoc
Line 2: This overwrites the file
EOF

# Append
echo 'Line 3: Appended' >> "$TARGET/[Link]"

# ■■ Read file ■■
echo '--- File contents ---'
while IFS= read -r line; do
echo " $line"
done < "$TARGET/[Link]"

# ■■ Copy, move, delete ■■


cp "$TARGET/[Link]" "$TARGET/[Link]"
mv "$TARGET/[Link]" "$TARGET/[Link]"
rm "$TARGET/[Link]"

# ■■ Find files ■■
find /etc -name '*.conf' -type f 2>/dev/null | head -5

# ■■ Permissions ■■
chmod 755 [Link] # rwxr-xr-x
chmod u+x,go-w [Link] # symbolic mode
chown user:group [Link] # change owner

# ■■ Disk usage ■■
du -sh /var/log # human-readable size of directory
df -h # file system usage

Heredoc & Herestring


#!/bin/bash

# Heredoc — feed multiline string to a command


cat << 'HEREDOC'
This text is passed verbatim to cat.
No variable expansion when delimiter is quoted.
HEREDOC

name='Alice'
cat << HEREDOC
Hello, $name! # variable IS expanded here
Today: $(date) # command substitution works too
HEREDOC

# Indented heredoc (strip leading tabs with <<-)


cat <<- INDENTED
Line with leading tab stripped
Another indented line
INDENTED

# Herestring — single string


grep 'root' <<< 'root:x:0:0:root:/root:/bin/bash'
11. Regular Expressions & Text Processing

#!/bin/bash

# ■■ Regex match with [[ =~ ]] ■■


email='user@[Link]'
regex='^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'

if [[ $email =~ $regex ]]; then


echo "Valid email: $email"
echo "Username: ${BASH_REMATCH[0]}"
else
echo 'Invalid email'
fi

# ■■ grep ■■
grep -E '^root' /etc/passwd # extended regex
grep -c 'error' /var/log/syslog # count matches
grep -v 'debug' [Link] # invert match
grep -rn 'TODO' ./src/ # recursive + line numbers

# ■■ sed — stream editor ■■


echo 'Hello World' | sed 's/World/Bash/' # substitute
sed -i 's/foo/bar/g' [Link] # in-place, all
sed -n '5,10p' [Link] # print lines 5-10
sed '/^#/d' [Link] # delete comment lines

# ■■ awk — pattern scanning & processing ■■


awk '{print $1, $3}' [Link] # print cols 1 and 3
awk -F: '{print $1}' /etc/passwd # custom delimiter
awk '$3 > 1000 {print $1, $3}' /etc/passwd # conditional print
awk '{sum += $1} END {print "Sum:", sum}' [Link]

# ■■ tr — translate characters ■■
echo 'hello world' | tr 'a-z' 'A-Z' # uppercase
echo 'a:b:c:d' | tr ':' '\n' # split on colon
echo 'hello world' | tr -s ' ' # squeeze spaces
12. Process Management & Signal Handling

#!/bin/bash

# ■■ Background jobs ■■
sleep 60 & # run in background
bg_pid=$! # capture PID
echo "Started sleep with PID $bg_pid"

jobs # list background jobs


wait $bg_pid # wait for specific job
wait # wait for ALL background jobs

# ■■ kill / killall ■■
kill $bg_pid # send SIGTERM (graceful)
kill -9 $bg_pid # send SIGKILL (force)
killall sleep # kill all 'sleep' processes

# ■■ trap — catch signals ■■


cleanup() {
echo 'Caught signal — cleaning up...'
rm -f /tmp/[Link]
exit 1
}

trap cleanup SIGINT SIGTERM # Ctrl+C or kill


trap 'echo "Script finished"' EXIT # always runs on exit

echo $$ > /tmp/[Link]


echo 'Working... press Ctrl+C to interrupt'
sleep 30

# ■■ Subshells ■■
(cd /tmp && ls) # runs in subshell — cwd unchanged
echo "Still in: $(pwd)"

# ■■ Process substitution ■■
diff <(ls dir1) <(ls dir2)
while read -r line; do
echo "$line"
done < <(find /etc -name '*.conf' 2>/dev/null)
13. Advanced Topics & Best Practices

Strict Mode — Fail Fast

#!/bin/bash
set -euo pipefail
# -e exit immediately on error
# -u treat unset variables as errors
# -o pipefail pipeline fails if any command fails

IFS=$'\n\t' # safer word splitting

# Use || true to allow a command to fail without exiting


rm /tmp/optional_file 2>/dev/null || true

Logging & Debugging

#!/bin/bash

# ■■ Logging functions ■■
LOG_FILE='/var/log/[Link]'

log() { echo "[INFO] $(date '+%F %T') $*" | tee -a "$LOG_FILE"; }


warn() { echo "[WARN] $(date '+%F %T') $*" | tee -a "$LOG_FILE" >&2; }
error(){ echo "[ERROR] $(date '+%F %T') $*" | tee -a "$LOG_FILE" >&2; }

log 'Script started'


warn 'This is a warning'
error 'Something went wrong'

# ■■ Debug mode ■■
set -x # print each command before executing
echo 'debug on'
set +x # turn off debug

# Run entire script in debug: bash -x [Link]


# Debug specific section: PS4='+(${BASH_SOURCE}:${LINENO}): '

Error Handling Patterns


#!/bin/bash
set -euo pipefail

# ■■ die function ■■
die() { echo "ERROR: $*" >&2; exit 1; }

# ■■ Validate inputs ■■
[[ $# -eq 2 ]] || die 'Usage: [Link] <src> <dst>'
[[ -f $1 ]] || die "Source file not found: $1"
[[ -d $(dirname $2) ]]|| die "Destination dir missing"

# ■■ Temp files cleaned on exit ■■


TMPFILE=$(mktemp /tmp/[Link])
trap 'rm -f "$TMPFILE"' EXIT

# ■■ Lockfile to prevent multiple instances ■■


LOCKFILE='/tmp/[Link]'
exec 9>"$LOCKFILE"
flock -n 9 || die 'Another instance is running'
trap 'flock -u 9; rm -f $LOCKFILE' EXIT

Best Practices Summary


Always use #!/bin/bash — Never rely on /bin/sh for Bash-specific features.

Quote your variables — Use "$var" and "${arr[@]}" to avoid word-splitting issues.

Use [[ ]] over [ ] — [[ ]] is a Bash keyword — safer, supports &&, ||, =~.

Enable strict mode — set -euo pipefail at the top of every production script.

Prefer local variables — Declare function variables local to avoid namespace pollution.

Validate all inputs — Check argument count, types, and file existence early.

Trap EXIT for cleanup — Use trap '...' EXIT to always clean up temp files/locks.

Log to stderr for errors — echo 'error msg' >&2 so it doesn't pollute stdout pipelines.

Use shellcheck — Run shellcheck [Link] to catch common bugs automatically.

Avoid parsing ls output — Use globs (for f in /tmp/*.txt) or find instead.

■ Congratulations! You now have a solid foundation in Bash scripting. Practice by automating real tasks:
backups, log rotation, system health checks, or build your own CLI tools. Happy scripting!

You might also like