funwithlinux guide

A Deep Dive into Linux Shell Scripting

In the world of Linux, efficiency and automation are paramount. Whether you’re a system administrator managing servers, a developer automating workflows, or a power user streamlining daily tasks, **shell scripting** is an indispensable skill. A shell script is a text file containing a sequence of commands that the Linux shell interprets and executes, enabling you to automate repetitive tasks, combine multiple commands, and build complex workflows with minimal effort. This blog will take you on a comprehensive journey through Linux shell scripting, starting from the basics and progressing to advanced concepts. By the end, you’ll have the knowledge to write robust, efficient scripts to tackle real-world challenges.

Table of Contents

  1. What is a Linux Shell?
  2. Getting Started with Shell Scripting
  3. Variables in Shell Scripting
  4. Input/Output Handling
  5. Control Structures: Conditionals and Loops
  6. Functions in Shell Scripts
  7. Arrays: Managing Collections of Data
  8. Error Handling and Debugging
  9. Advanced Topics
  10. Best Practices for Writing Shell Scripts
  11. Real-World Examples
  12. References

What is a Linux Shell?

The shell is a command-line interpreter that acts as an interface between the user and the Linux kernel. It reads user input (commands), executes them, and returns output. While graphical user interfaces (GUIs) are user-friendly, the shell offers unparalleled control and automation capabilities.

Common Shells

  • Bash (Bourne-Again SHell): The most popular shell, default on most Linux distributions (Ubuntu, Fedora, Debian). It extends the original Bourne Shell (sh) with features like command history, tab completion, and scripting support.
  • Zsh (Z Shell): A modern shell with enhanced features (themes, plugins, better globbing) popular among developers.
  • Fish (Friendly Interactive SHell): Designed for simplicity and user-friendliness, with auto-suggestions and syntax highlighting.
  • Ksh (Korn Shell): Combines features of Bash and C Shell (csh), used in some enterprise environments.

For this blog, we’ll focus on Bash, as it’s the de facto standard for scripting.

Getting Started with Shell Scripting

1. Shebang Line

Every shell script starts with a shebang line (#!), which tells the system which interpreter to use. For Bash scripts, this is:

#!/bin/bash

Save the script with a .sh extension (e.g., script.sh).

2. Writing Your First Script

Let’s create a simple “Hello World” script:

#!/bin/bash
# This is a comment (ignored by the shell)
echo "Hello, World!"  # Print text to the console

3. Executing the Script

To run the script:

  1. Make it executable with chmod +x script.sh (sets the executable permission).
  2. Execute it with ./script.sh (the ./ ensures the current directory is searched).

Alternatively, run it directly with the Bash interpreter: bash script.sh (no need for chmod in this case).

Variables in Shell Scripting

Variables store data for reuse. Bash supports two types of variables: local variables (visible only in the script) and environment variables (system-wide, inherited by child processes).

1. Declaring Variables

  • No spaces around the = sign.
  • Variable names: Start with a letter/underscore, followed by letters, numbers, or underscores.
name="Alice"  # String variable
age=30        # Numeric variable

2. Accessing Variables

Use $variable_name to access a variable’s value:

echo "Name: $name"  # Output: Name: Alice
echo "Age: $age"    # Output: Age: 30

3. Environment Variables

These are predefined by the system (or set by the user) and control system behavior. Examples:

  • PATH: Directories searched for executable commands.
  • HOME: User’s home directory (e.g., /home/alice).
  • USER: Current username.
  • PWD: Current working directory.

Access them like local variables:

echo "Home: $HOME"   # Output: Home: /home/alice
echo "PATH: $PATH"   # Output: List of directories

4. Setting Environment Variables

Use export to make a local variable an environment variable:

export MY_VAR="Hello"  # Now accessible to child processes

Input/Output Handling

Shell scripts interact with users and files through input/output (I/O) operations.

1. Output: echo and printf

  • echo: Prints text to the console. Use -e to enable escape sequences (\n for newline, \t for tab):

    echo "Hello\nWorld"       # Output: Hello\nWorld (no escape)
    echo -e "Hello\nWorld"    # Output: Hello (newline) World
  • printf: For formatted output (similar to C’s printf). Supports placeholders like %s (string), %d (integer), %f (float):

    printf "Name: %s, Age: %d\n" "Alice" 30  # Output: Name: Alice, Age: 30

2. Input: read Command

Use read to capture user input. Add -p to display a prompt:

#!/bin/bash
read -p "Enter your name: " name  # Prompt user for input
echo "Hello, $name!"              # Output: Hello, [name]!

3. Redirection

Redirect output to files or input from files using operators:

  • >: Overwrite a file (e.g., echo "Hi" > file.txt).
  • >>: Append to a file (e.g., echo "Again" >> file.txt).
  • <: Read input from a file (e.g., sort < file.txt).
  • 2>: Redirect errors to a file (e.g., command 2> errors.log).
  • &>: Redirect both output and errors (e.g., command &> output.log).

4. Pipes (|)

Chain commands with | to pass output of one command as input to another:

ls -l | grep ".sh"  # List all .sh files (ls output → grep input)

Control Structures: Conditionals and Loops

Control structures let you execute code conditionally or repeatedly.

1. If-Else Statements

Check conditions and execute code blocks accordingly.

Syntax:

if [ condition ]; then
  # Code if condition is true
elif [ another_condition ]; then
  # Code if first condition is false, second is true
else
  # Code if all conditions are false
fi

Conditions:

  • Numeric: -eq (equal), -ne (not equal), -lt (less than), -gt (greater than).
  • String: =, != (equality/inequality), -z (empty string), -n (non-empty string).
  • File: -f (file exists), -d (directory exists), -x (executable).

Example: Check if a file exists:

#!/bin/bash
file="data.txt"
if [ -f "$file" ]; then
  echo "$file exists."
else
  echo "$file does NOT exist."
fi

2. Loops

For Loop

Iterate over a list of items (files, numbers, strings):

#!/bin/bash
# Loop through a list of fruits
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
  echo "I like $fruit"
done

While Loop

Run code as long as a condition is true:

#!/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!"

Until Loop

Run code until a condition is true (opposite of while):

#!/bin/bash
count=1
until [ $count -gt 5 ]; do
  echo "Count: $count"
  count=$((count + 1))
done

Functions in Shell Scripts

Functions group reusable code into modular blocks.

1. Defining a Function

Syntax:

function_name() {
  # Code here
  local local_var="I'm local"  # Local variable (scope limited to function)
  echo "$local_var"
}

2. Parameters and Return Values

  • Access parameters with $1, $2, … (first, second argument).
  • Return values via return (exit code, 0-255) or output (use echo and capture with $()).

Example:

#!/bin/bash
greet() {
  local name=$1  # First parameter
  echo "Hello, $name!"
  return 0       # Success exit code
}

greet "Bob"  # Output: Hello, Bob!

Arrays: Managing Collections of Data

Arrays store multiple values in a single variable.

1. Declaring Arrays

# Method 1: Explicit declaration
fruits=("apple" "banana" "cherry")

# Method 2: Indexed assignment
fruits[0]="apple"
fruits[1]="banana"

2. Accessing Elements

  • ${array[index]}: Access element at index (starts at 0).
  • ${array[@]}: All elements.
  • ${#array[@]}: Number of elements.

Example:

echo "${fruits[1]}"    # Output: banana
echo "${fruits[@]}"    # Output: apple banana cherry
echo "${#fruits[@]}"   # Output: 3

3. Looping Through Arrays

for fruit in "${fruits[@]}"; do
  echo "$fruit"
done

Error Handling and Debugging

1. Exit Codes

Every command returns an exit code (0 = success, non-zero = failure). Check with $?:

ls non_existent_file
echo "Exit code: $?"  # Output: 2 (failure)

2. set Options

  • set -e: Exit the script if any command fails.
  • set -u: Treat undefined variables as errors.
  • set -o pipefail: Exit if any command in a pipe fails.

Add these at the top of your script for strict error checking:

#!/bin/bash
set -euo pipefail  # Exit on errors, undefined vars, or pipe failures

3. Debugging

  • bash -x script.sh: Run the script in debug mode (prints each command before execution).
  • set -x in the script: Enable debugging temporarily.
  • set +x: Disable debugging.

Advanced Topics

1. Command Substitution

Capture output of a command into a variable with $() or backticks `:

current_date=$(date +%Y-%m-%d)  # Get current date
echo "Today is $current_date"   # Output: Today is 2024-05-20

2. Here Documents

Embed multi-line text directly in the script with <<EOF (end with EOF):

cat <<EOF > report.txt
Report generated on: $(date)
User: $USER
EOF

3. Traps

Catch signals (e.g., Ctrl+C) or script exit to clean up resources:

#!/bin/bash
cleanup() {
  echo "Cleaning up temp files..."
  rm -f temp.txt
}

trap cleanup EXIT  # Run cleanup when script exits
trap 'echo "Aborted!"; exit 1' SIGINT  # Run on Ctrl+C

Best Practices for Writing Shell Scripts

  • Shebang Line: Always start with #!/bin/bash.
  • Comments: Explain complex logic (use #).
  • Quoting: Use "$var" to handle spaces in variables (e.g., file="$1" instead of file=$1).
  • Error Checking: Validate inputs and command success (use set -e or if ! command; then ...).
  • Modularity: Use functions for reusable code.
  • Testing: Test scripts with different inputs and edge cases.
  • Version Control: Store scripts in Git for tracking changes.

Real-World Examples

Example 1: Backup Script

Automatically back up a directory to a compressed file:

#!/bin/bash
set -euo pipefail

SOURCE_DIR="/home/user/documents"
BACKUP_DIR="/mnt/backup"
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$BACKUP_DIR/docs_backup_$TIMESTAMP.tar.gz"

# Check if source exists
if [ ! -d "$SOURCE_DIR" ]; then
  echo "Error: Source directory $SOURCE_DIR does not exist."
  exit 1
fi

# Create backup
echo "Creating backup: $BACKUP_FILE"
tar -czf "$BACKUP_FILE" -C "$SOURCE_DIR" .

echo "Backup completed successfully!"

Example 2: System Info Script

Display CPU, memory, and disk usage:

#!/bin/bash
echo "=== System Info ==="
echo "CPU Usage: $(top -bn1 | grep "Cpu(s)" | awk '{print $2 + $4}')%"
echo "Memory Usage: $(free -m | awk '/Mem:/ {print $3 "MB / " $2 "MB"}')"
echo "Disk Usage: $(df -h / | awk '/\// {print $3 " / " $2}')"

References

By mastering shell scripting, you’ll unlock the full potential of Linux automation. Start small, experiment, and gradually build more complex scripts—your future self (and sysadmin team) will thank you! 🚀