Introduction
For system administrators (sysadmins), repetitive tasks like monitoring system health, managing users, automating backups, or rotating logs are part of daily life. Manually executing these tasks is time-consuming, error-prone, and scales poorly in large environments. Linux shell scripting emerges as a powerful solution to automate these workflows, enabling sysadmins to save time, enforce consistency, and reduce human error.
This blog introduces the fundamentals of Linux shell scripting tailored for sysadmins. We’ll cover core concepts, practical examples, and best practices to help you write efficient, maintainable, and reliable scripts. By the end, you’ll be equipped to automate common sysadmin tasks and streamline your workflow.
What is a Linux Shell Script?
A shell script is a text file containing a sequence of commands interpreted and executed by a Unix/Linux shell (e.g., Bash, Zsh, or Sh). The shell acts as an intermediary between the user and the operating system, executing commands sequentially as if you typed them directly into the terminal.
Why Bash?
While there are multiple shells (e.g., Zsh, Fish), Bash (Bourne-Again SHell) is the most widely used and default shell on most Linux distributions (e.g., Ubuntu, CentOS, RHEL). We’ll focus on Bash scripting here, as it offers robust features and broad compatibility.
Getting Started: Writing and Executing Your First Script
Step 1: Create a Script File
Use a text editor like nano, vim, or VS Code to create a file with a .sh extension (convention, not requirement). For example:
nano hello_world.sh
Step 2: Add the Shebang Line
The first line of a script, called the shebang, specifies the shell interpreter to use. For Bash, this is:
#!/bin/bash
Without this line, the system may use a different shell (e.g., sh), leading to unexpected behavior.
Step 3: Write Commands
Add commands to the script. For a simple “Hello World” example:
#!/bin/bash
echo "Hello, System Admin!"
Step 4: Make the Script Executable
Scripts are not executable by default. Use chmod to grant execute permissions:
chmod +x hello_world.sh
Step 5: Run the Script
Execute the script using its path:
./hello_world.sh # Runs the script in the current directory
Output:
Hello, System Admin!
Variables and Data Types
Variables store data for reuse in scripts. Bash supports strings, integers, and arrays, with dynamic typing (no need to declare types).
User-Defined Variables
Declare variables with VAR_NAME=value (no spaces around =). Access values with $VAR_NAME or ${VAR_NAME}.
Example:
#!/bin/bash
NAME="Alice"
AGE=30
echo "Name: $NAME, Age: $AGE" # Output: Name: Alice, Age: 30
Special Variables
Bash provides built-in variables for common use cases:
| Variable | Description |
|---|---|
$0 | Name of the script |
$1, $2... | Positional arguments (e.g., $1 = first input) |
$# | Number of positional arguments |
$@ | All positional arguments (as a list) |
$? | Exit status of the last command (0 = success, non-zero = error) |
$$ | Process ID (PID) of the script |
Example: Using Positional Arguments
#!/bin/bash
echo "Script name: $0" # Output: Script name: ./args_demo.sh
echo "First argument: $1" # Output: First argument: server01
echo "Number of arguments: $#" # Output: Number of arguments: 2
Run with: ./args_demo.sh server01 2024
Arrays
Store multiple values in an array:
#!/bin/bash
FRUITS=("apple" "banana" "cherry")
echo "First fruit: ${FRUITS[0]}" # Output: apple
echo "All fruits: ${FRUITS[@]}" # Output: apple banana cherry
echo "Number of fruits: ${#FRUITS[@]}" # Output: 3
Control Structures: Conditionals and Loops
Control structures let you execute commands conditionally or repeatedly—critical for automation logic.
Conditionals (if-else)
Use if-else to run commands based on conditions. Bash uses test expressions (enclosed in [ ] or [[ ]]) to evaluate conditions.
Syntax:
if [ condition ]; then
# Commands if condition is true
elif [ another_condition ]; then
# Commands if first condition is false, second is true
else
# Commands if all conditions are false
fi
Common Test Operators:
| Operator | Description |
|---|---|
-eq | Integer equality (e.g., 5 -eq 5) |
-ne | Integer inequality |
-lt | Less than (3 -lt 5) |
-gt | Greater than |
-f | File exists (e.g., -f /etc/passwd) |
-d | Directory exists |
= | String equality (e.g., "abc" = "abc") |
!= | String inequality |
Example: Check if a File Exists
#!/bin/bash
FILE="/tmp/important.log"
if [ -f "$FILE" ]; then
echo "$FILE exists. Size: $(du -h $FILE | awk '{print $1}')"
else
echo "$FILE not found. Creating it..."
touch "$FILE"
fi
Loops
for Loop: Iterate Over Lists
Use for loops to iterate over arrays, command outputs, or ranges.
Example 1: Iterate Over an Array
#!/bin/bash
SERVERS=("server01" "server02" "server03")
for server in "${SERVERS[@]}"; do
echo "Pinging $server..."
ping -c 1 "$server" > /dev/null 2>&1 # Suppress output
if [ $? -eq 0 ]; then # Check exit status of ping
echo "$server is UP"
else
echo "$server is DOWN"
fi
done
Example 2: Range Loop
#!/bin/bash
echo "Counting from 1 to 5:"
for i in {1..5}; do
echo $i
done
while Loop: Run Until Condition Fails
Use while loops to repeat commands until a condition becomes false.
Example: Countdown Timer
#!/bin/bash
COUNT=5
while [ $COUNT -gt 0 ]; do
echo "Countdown: $COUNT"
COUNT=$((COUNT - 1)) # Decrement count
sleep 1 # Wait 1 second
done
echo "Blast off!"
Functions: Reusable Code Blocks
Functions let you encapsulate logic for reuse across a script (or even multiple scripts). They improve readability and reduce redundancy.
Syntax:
function function_name() {
# Commands here
# Use $1, $2... to access arguments
}
# Or (simpler syntax):
function_name() {
# Commands here
}
Example: Backup Function
#!/bin/bash
# Function to backup a directory
backup_dir() {
local source="$1" # First argument: source directory
local dest="$2" # Second argument: backup destination
# Validate source exists
if [ ! -d "$source" ]; then
echo "Error: Source $source does not exist."
return 1 # Return non-zero exit code for failure
fi
# Create backup filename with timestamp
local timestamp=$(date +%Y%m%d_%H%M%S)
local backup_file="$dest/backup_$timestamp.tar.gz"
# Compress and backup
echo "Backing up $source to $backup_file..."
tar -czf "$backup_file" -C "$source" . # -C: change to source dir before archiving
if [ $? -eq 0 ]; then
echo "Backup successful! Size: $(du -h "$backup_file" | awk '{print $1}')"
return 0 # Success
else
echo "Backup failed."
return 1
fi
}
# Usage: Call the function with source and destination
backup_dir "/var/log" "/backup/logs"
Input/Output Handling
Scripts often need to read user input, process output, or redirect data to files.
Reading User Input
Use the read command to capture input from the user or stdin.
Example: Prompt for User Input
#!/bin/bash
read -p "Enter your name: " NAME # -p: prompt message
read -sp "Enter password: " PASSWORD # -s: silent input (hide typing)
echo -e "\nHello $NAME! Password length: ${#PASSWORD}" # -e: enable escape chars (\n)
Output Redirection
Redirect command output to files instead of the terminal:
>: Overwrite a file (e.g.,echo "Hi" > output.txt)>>: Append to a file (e.g.,echo "Again" >> output.txt)2>: Redirect errors (stderr) (e.g.,ls /invalid 2> error.log)&>: Redirect both stdout and stderr (e.g.,command &> combined.log)
Pipes (|)
Chain commands with pipes to pass output of one command as input to another.
Example: Find Large Log Files
#!/bin/bash
echo "Finding log files over 100MB in /var/log:"
find /var/log -type f -name "*.log" -size +100M | xargs du -h
Common Use Cases for System Admins
Shell scripts shine in automating repetitive sysadmin tasks. Here are practical examples:
1. System Monitoring: Disk Space Alert
Monitor disk usage and send an alert if a partition exceeds 90% usage:
#!/bin/bash
THRESHOLD=90
EMAIL="[email protected]"
# Check disk usage for all mounted partitions
df -h | awk -v threshold="$THRESHOLD" 'NR > 1 {gsub("%", "", $5); if ($5 >= threshold) print $0 " is over " threshold "%"}' | while read -r line; do
echo "High disk usage detected: $line" | mail -s "Disk Usage Alert" "$EMAIL"
done
2. Log Rotation
Compress and archive old logs to save space:
#!/bin/bash
LOG_DIR="/var/log/myapp"
MAX_AGE_DAYS=7 # Keep logs newer than 7 days
# Compress logs older than 7 days
find "$LOG_DIR" -name "*.log" -type f -mtime +$MAX_AGE_DAYS -exec gzip {} \;
# Delete .gz files older than 30 days
find "$LOG_DIR" -name "*.log.gz" -type f -mtime +30 -delete
3. User Management
Bulk-create users from a list (e.g., users.txt with one username per line):
#!/bin/bash
USER_LIST="users.txt"
while IFS= read -r username; do
# Skip empty lines
if [ -z "$username" ]; then continue; fi
# Check if user exists
if id "$username" > /dev/null 2>&1; then
echo "User $username already exists. Skipping."
else
echo "Creating user $username..."
useradd -m -s /bin/bash "$username" # -m: create home dir, -s: set shell
echo "$username:TempPass123!" | chpasswd # Set temp password
chage -d 0 "$username" # Force password change on first login
fi
done < "$USER_LIST"
Best Practices for Robust Scripting
Writing scripts that are reliable, secure, and maintainable requires following best practices:
1. Use the Shebang Line
Always start with #!/bin/bash to ensure consistent execution.
2. Enable Strict Error Checking
Add these lines near the top of scripts to catch bugs early:
set -euo pipefail
-e: Exit immediately if any command fails.-u: Treat undefined variables as errors.-o pipefail: Make pipes return the exit code of the last failed command (not just the last command).
3. Quote Variables
Always quote variables (e.g., "$FILE") to handle spaces in filenames or arguments:
# Bad: Fails if $FILE has spaces
if [ -f $FILE ]; then ...
# Good: Handles spaces
if [ -f "$FILE" ]; then ...
4. Add Comments
Document non-obvious logic to help future you (or other sysadmins) understand the script:
#!/bin/bash
set -euo pipefail
# Purpose: Automate backup of /var/www to /backup
# Usage: ./backup_www.sh
# Dependencies: tar, gzip
5. Validate Inputs
Check that required arguments, files, or dependencies exist before proceeding:
#!/bin/bash
set -euo pipefail
if [ $# -ne 2 ]; then
echo "Usage: $0 <source_dir> <backup_dir>"
exit 1
fi
# Check if tar is installed
if ! command -v tar &> /dev/null; then
echo "Error: tar is not installed."
exit 1
fi
6. Use shellcheck for Linting
shellcheck is a tool that identifies syntax errors, bugs, and bad practices in scripts. Install it (e.g., sudo apt install shellcheck) and run:
shellcheck your_script.sh
7. Limit External Dependencies
Prefer built-in Bash commands (e.g., [[ ]] over [ ], parameter expansion) over external tools like awk or sed when possible, to improve portability.
8. Version Control
Store scripts in Git or another version control system to track changes and revert if needed.
Conclusion
Linux shell scripting is an indispensable skill for sysadmins, enabling automation of repetitive tasks, enforcement of consistency, and reduction of human error. In this blog, we covered core concepts like variables, control structures, functions, and I/O handling, along with practical examples for monitoring, backups, and user management.
By following best practices—strict error checking, quoting variables, validating inputs, and using tools like shellcheck—you can write robust, maintainable scripts that scale with your infrastructure. Start small (e.g., automate a daily task), iterate, and gradually tackle more complex workflows.
Happy scripting!