dotlinux guide

Introduction to Linux Shell Scripting for System Admins

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:

VariableDescription
$0Name 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:

OperatorDescription
-eqInteger equality (e.g., 5 -eq 5)
-neInteger inequality
-ltLess than (3 -lt 5)
-gtGreater than
-fFile exists (e.g., -f /etc/passwd)
-dDirectory 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!

References