Solid base for Bash scripting

Bash scripting is a powerful tool for automating tasks, managing your system operations on Unix-like environment. This guide covers essential Bash concepts.

Shebang

First thing in your script the shebang #! is used at the beginning of a script to specify the interpreter that should execute the script. You should prefer #!/usr/bin/env bash to ensure portability on different systems, as it locates the bash interpreter using the env command. #!/bin/bash can cause issues and do launch your script.

#!/usr/bin/env bash
#!/usr/bin/env python3

Command Line Arguments

Command line arguments allow you to pass data to your script when executing it.

Example:

#!/usr/bin/env bash

first_name=$1
last_name=$2

echo "Hello, my name is $first_name $last_name."

Explanation:

  • $1 and $2 represent the first and second command line arguments, respectively.
  • You can run the script as ./script.sh John Doe to output: Hello, my name is John Doe.

Handling Quotes and Variables:

  • Single Quotes ('): Preserve the literal value of each character within the quotes. Variables are not expanded.

    echo 'Hello, $name'  # Outputs: Hello, $name
    
  • Double Quotes ("): Allow variable expansion and interpretation of certain escape sequences.

    echo "Hello, $name"  # Outputs: Hello, John
    

Using ${} for Variable Manipulation:

The ${} syntax allows for more complex variable operations within strings.

name=""
echo "Hello, ${name:-Anonymous}"  # Outputs: Hello, Anonymous

# Setting the variable if it's unset or empty
echo "Hello, ${name:="Anonymous"}"  # Sets name to "Anonymous" and outputs: Hello, Anonymous

Explanation:

  • ${variable:-default}: Uses default if variable is unset or empty.
  • ${variable:=default}: Sets variable to default if it’s unset or empty and then uses it.

Subshells

A subshell allows you to execute commands in a separate shell instance. Changes made in a subshell do not affect the parent shell.

Example:

#!/usr/bin/env bash

echo "Current Directory: $(pwd)"
(cd ..; echo "Subshell Directory: $(pwd)")
echo "Back to Directory: $(pwd)"

Output:

Current Directory: /home/user/dev
Subshell Directory: /home/user
Back to Directory: /home/user/dev

Explanation:

  • (cd ..; pwd): Changes directory to the parent in a subshell and prints it.
  • The parent shell’s directory remains unchanged.

Command Substitution

Command substitution allows the output of a command to replace the command itself within a shell command.

Syntax:

variable=$(command)

Example:

#!/usr/bin/env bash

current_dir=$(pwd)
echo "You are in: $current_dir"

Output:

You are in: /home/user

Explanation:

  • $(pwd): Executes the pwd command and assigns its output to current_dir.

Process Substitution

Process substitution allows you to use the output of a process as if it were a file. This is particularly useful for commands that require file inputs.

Example:

#!/usr/bin/env bash

diff <(ls ./folder1) <(ls ./folder2)

Explanation:

  • <(ls ./folder1) and <(ls ./folder2) create temporary file descriptors for the outputs of the ls commands.
  • diff compares the contents of these directories.

Arithmetic Operations

Bash supports arithmetic operations using the $(()) syntax.

Example:

#!/usr/bin/env bash

a=5
b=3
sum=$((a + b))
echo "Sum: $sum"  # Outputs: Sum: 8

Supported Operations:

  • Addition (+)
  • Subtraction (-)
  • Multiplication (*)
  • Division (/)
  • Modulus (%)
  • Exponentiation (**)

Conditional Statements

Conditional statements allow your script to make decisions based on certain conditions.

Basic if Statement:

#!/usr/bin/env bash

if [ "$1" -gt 0 ]; then
    echo "The number is positive."
else
    echo "The number is not positive."
fi

Explanation:

  • [ "$1" -gt 0 ]: Checks if the first argument is greater than 0.
  • Executes the corresponding block based on the condition.

Comparison Types:

  • String Comparison: =, !=, <, >
  • Numeric Comparison: -eq, -ne, -lt, -le, -gt, -ge
  • File Tests: -e (exists), -f (file), -d (directory), -r (readable), -w (writable), -x (executable)

Exit Codes

Scripts and commands return an exit code to indicate success or failure.

  • 0: Success
  • n (where n is a non-zero value): Failure or specific error codes

Example:

#!/usr/bin/env bash

if [ -f "$1" ]; then
    echo "File exists."
    exit 0
else
    echo "File does not exist."
    exit 1
fi

Usage:

  • Check the exit code using echo $? after running a script or command.

Conditional Execution

Bash allows commands to be executed based on the success or failure of previous commands using && and ||.

Example:

#!/usr/bin/env bash

mkdir new_directory || { echo "Failed to create directory."; exit 1; }
echo "Directory created successfully."

Explanation:

  • mkdir new_directory || { ...; }: If mkdir fails, execute the commands within {}.

Another Example:

false || true
echo "Continue executing the script..."

Output:

Continue executing the script...

Explanation:

  • false returns a non-zero exit code, triggering the execution of true.
  • The script continues despite the false command failing.

Useful Commands

Sleep

The sleep command pauses the execution of a script for a specified duration.

#!/usr/bin/env bash

echo "Waiting for 5 seconds..."
sleep 5
echo "Done waiting."

Usage:

  • sleep 5: Pauses for 5 seconds.
  • sleep 1m: Pauses for 1 minute.

Read

The read command takes input from the user during script execution.

#!/usr/bin/env bash

read -p "Enter your name: " name
echo "Hello, $name!"

Explanation:

  • -p: Displays a prompt message.
  • read assigns the user input to the variable name.

Strict Mode

Enabling strict mode makes your script more robust by enforcing better error handling and variable usage.

#!/usr/bin/env bash
set -euo pipefail

Explanation:

  • set -e: Exit immediately if a command exits with a non-zero status.
  • set -u: Treat unset variables as an error and exit immediately.
  • set -o pipefail: Causes a pipeline to return the exit status of the last command in the pipe that failed.

Loops

Loops allow you to execute a block of code multiple times.

For Loop

Example: Iterating Over an Array

#!/usr/bin/env bash

array=(1 2 3 4)

for item in "${array[@]}"; do
    echo "Item: $item"
done

Output:

Item: 1
Item: 2
Item: 3
Item: 4

Explanation:

  • ${array[@]}: Expands to all elements in the array.
  • The loop iterates over each element, assigning it to item.

Example: Pattern Matching

#!/usr/bin/env bash

for file in ./content/*.md; do
    cat "$file"
done

Explanation:

  • Iterates over all Markdown files in the ./content directory and displays their contents.

While Loop

Example: Waiting for a Kubernetes Pod to be Running

#!/usr/bin/env bash

POD_NAME="my-pod"
NAMESPACE="default"

echo "Waiting for pod $POD_NAME to be Running..."

while true; do
    status=$(kubectl get pod "$POD_NAME" -n "$NAMESPACE" -o jsonpath='{.status.phase}')
    if [ "$status" = "Running" ]; then
        echo "Pod is running."
        break
    fi
    echo "Current status: $status. Waiting..."
    sleep 5
done

Explanation:

  • Continuously checks the status of a Kubernetes pod every 5 seconds until it is Running.

Script Structuring

Splitting Scripts

Breaking down your script into multiple files can improve readability and maintainability.

Executing Another Script:

  • Same Shell:

    #!/usr/bin/env bash
    
    # main.sh
    . ./networkSetup.sh arg1 arg2
    

    Explanation:

    • The . (dot) command sources networkSetup.sh in the current shell, allowing it to share variables and functions.
  • Subshell:

    #!/usr/bin/env bash
    
    # main.sh
    ./networkSetup.sh arg1 arg2
    

    Explanation:

    • Executes networkSetup.sh in a subshell, isolating its environment from the main script.

Functions

Functions allow you to reuse code blocks within your script.

Example:

#!/usr/bin/env bash

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

greet "Alice"
greet "Bob"

Output:

Hello, Alice!
Hello, Bob!

Explanation:

  • greet is a function that takes one argument and prints a greeting.
  • local: Limits the scope of the name variable to within the function.

Temporary Files

Creating temporary files and directories is essential for handling intermediate data securely.

Example:

#!/usr/bin/env bash

# Create a temporary file
tempfile=$(mktemp)
trap "rm -f $tempfile" EXIT

echo "Hello, YouTube!" > "$tempfile"
echo "Content written to $tempfile."

# Create a temporary directory
tempdir=$(mktemp -d)
trap "rm -rf $tempdir" EXIT

echo "Hello, YouTube (from inside a tempdir)!" > "$tempdir/hello"
echo "Content written to $tempdir/hello."

Explanation:

  • mktemp: Creates a unique temporary file or directory.
  • trap: Ensures that the temporary file and directory are deleted when the script exits, even if interrupted.

Naming Conventions

Adopting consistent naming conventions enhances the readability and organization of your scripts.

Examples:

  • Script Files:

    • Use descriptive names like backup.sh, deploy_app.sh, or launch.sh.
  • Conventions:

    • Prefix scripts that start processes with launch, e.g., launch_server.sh.
    • Place scripts in appropriate directories, such as bin/ for executable scripts.

PATH Environment Variable

The PATH environment variable specifies directories where the shell looks for executable files.

Viewing PATH:

echo $PATH

Example Output:

/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin

Adding a Directory to PATH:

To make your scripts executable from anywhere, add their directory to PATH by editing your shell configuration file (~/.bashrc or ~/.zshrc).

export PATH="$PATH:/path/to/your/scripts"

Steps:

  1. Open ~/.bashrc or ~/.zshrc in a text editor.

  2. Add the export line above, replacing /path/to/your/scripts with your script directory.

  3. Reload the configuration:

    source ~/.bashrc
    # or
    source ~/.zshrc
    

Process of Script Creation

Creating an effective Bash script involves several steps to ensure it meets your needs and handles various scenarios gracefully.

  1. Figure Out What You Want to Do:

    • Define the goal of your script.
    • Break down the task into smaller steps.
  2. Identify Tools You’ll Use:

    • Determine which Unix commands and utilities are needed.
  3. Sketch It Out in the Terminal:

    • Experiment with commands directly in the terminal to understand their behavior.
  4. Copy It into a Script:

    • Create a new .sh file and add your commands.
  5. Pull Out Variables and Inputs:

    • Replace hard-coded values with variables and command line arguments.
  6. Add Checks (Guards, etc.):

    • Implement error handling and input validation.
  7. Add Loops and Other Advanced Functionality:

    • Incorporate loops, conditionals, and functions as needed.
  8. Break into Multiple Files if Necessary:

    • If the script becomes too large, split it into modular scripts and source them.

Cheatsheet

For quick reference, check out the Bash Scripting Cheatsheet.