Post

10 Bash Scripting Constructs Every Engineer Should Know

10 Bash Scripting Constructs Every Engineer Should Know

Bash scripting is a superpower for engineers. Whether automating repetitive tasks, gluing together tools, or managing systems, Bash is always there, simple yet powerful.

But like any power, it requires mastery. Let me walk you through 10 essential Bash constructs through the lens of a plausible scenario.

The Scenario

You’re tasked with analyzing server logs from multiple files, extracting failed login attempts, and generating a report. It’s a routine problem, but with Bash, we’ll make it elegant and reusable.

1. Setting the Stage with a Script

We begin our journey by writing the skeleton of our script:

1
2
3
4
#!/bin/bash

set -e # Exit on errors
trap 'echo "Error on line $LINENO"; exit 1' ERR

Why?:

  • set -e ensures the script stops at the first sign of trouble.
  • trap catches errors, giving us helpful debugging information.

2. Modularize with Functions

Good scripts are modular. Let’s define a function to parse log files:

1
2
3
4
5
6
7
8
9
10
11
12
parse_logs() {

  local file="$1"
  local output="$2"

  while read -r line; do
    if [[ "$line" == *"FAILED LOGIN"* ]]; then
        echo "$line" >> "$output"
    fi

  done < "$file"
}

Why?:

  • Functions make scripts reusable and maintainable.
  • local variables prevent accidental overwrites.

3. Arrays: Managing Multiple Logs

We need to process logs from several servers:

1
2
3
4
5
6
7
8
log_files=("server1.log" "server2.log" "server3.log")
results=()

for file in "${log_files[@]}"; do
    output="${file%.log}_failed.log"
    parse_logs "$file" "$output"
    results+=("$output")
done

Why?:

  • Arrays help manage lists of items efficiently.
  • We append processed results to an array for future steps.

4. Command Substitution: Adding Timestamps

Let’s add timestamps to our output files using date:

1
2
timestamp=$(date "+%Y-%m-%d")
final_report="failed_logins_$timestamp.txt"

Why?:

  • Command substitution integrates dynamic values into scripts seamlessly.

5. String Manipulation

Before combining the logs, we sanitize the output filenames:

1
2
3
4
for file in "${results[@]}"; do
    sanitized_name="${file// /_}"  # Replace spaces with underscores
    mv "$file" "$sanitized_name"
done

Why?:

  • Bash’s parameter expansion simplifies string transformations without external tools.

6. Process Substitution: Combining Files

To merge the logs efficiently:

1
cat "${results[@]}" > "$final_report"

Why?:

  • Process substitution and array expansion enable concise, efficient handling of multiple files.

7. Conditional Logic: Tailoring Reports

Let’s customize the final report based on its content:

1
2
3
4
5
6
if [[ -s "$final_report" ]]; then
    echo "Report generated: $final_report"
else
    echo "No failed logins found."
    rm "$final_report"
fi

Why?:

  • if ensures actions depend on context, such as whether the report is empty.

8. Case Statements: Default Ports

Imagine we need to identify default SSH and HTTPS ports based on server type:

1
2
3
4
5
6
7
8
get_port() {
    local server="$1"
    case "$server" in
        "prod"*) echo 22 ;;
        "staging"*) echo 2222 ;;
        *) echo 80 ;;
    esac
}

Why?:

  • case is ideal for handling multiple specific patterns elegantly.

9. Debugging with set -x

Before deploying the script, let’s debug it:

1
2
3
set -x # Enable debugging
# Run the main script here
set +x # Disable debugging

Why?:

  • Debugging tools like set -x make it easy to trace and fix errors.

10. File Descriptors for Advanced I/O

Let’s imagine we’re reading and processing logs from a special input stream:

1
2
3
4
5
6
7
exec 3<"$final_report"

while read -u3 line; do
    echo "Processed: $line"
done

exec 3<&-

Why?:

  • File descriptors give precise control over inputs and outputs, enabling parallel processing.

The Final Script

Here’s what the polished script might look like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#!/bin/bash

set -e
trap 'echo "Error on line $LINENO"; exit 1' ERR
parse_logs() {
    local file="$1"
    local output="$2"

    while read -r line; do
        if [[ "$line" == *"FAILED LOGIN"* ]]; then
            echo "$line" >> "$output"
        fi
    done < "$file"
}

log_files=("server1.log" "server2.log" "server3.log")

results=()

for file in "${log_files[@]}"; do
    output="${file%.log}_failed.log"
    parse_logs "$file" "$output"
    results+=("$output")
done

timestamp=$(date "+%Y-%m-%d")

final_report="failed_logins_$timestamp.txt"

cat "${results[@]}" > "$final_report"

if [[ -s "$final_report" ]]; then
    echo "Report generated: $final_report"
else
    echo "No failed logins found."
    rm "$final_report"
fi

Takeaways

This script ties together “almost” everything an engineer needs for professional Bash scripting: modularity, error handling, efficient data processing, and debugging tools.

By mastering these constructs, you’ll not only write better scripts but also transform mundane tasks into elegant solutions.

But, there is one more thing (maybe 5). I am feeling too excited now and I will include 5 more extra that I find myself using quite often:

Five More Bash Constructs Every Engineer Should Know

Here are five additional constructs.

11. Associative Arrays

What They Are: Associative arrays are key-value pairs in Bash, available starting with Bash 4. They allow efficient lookups and data organization.

Example: Imagine you’re mapping server names to their IP addresses:

1
2
3
4
5
6
7
8
9
10
declare -A servers
servers=( ["web"]="192.168.1.10" ["db"]="192.168.1.20" ["cache"]="192.168.1.30" )

# Access values
echo "Web server IP: ${servers[web]}"

# Iterate over keys
for key in "${!servers[@]}"; do
    echo "$key -> ${servers[$key]}"
done

Why Use Them:

  • Associative arrays provide a natural way to handle structured data without relying on external tools like awk or sed.
  • Useful for configurations, lookups, and organizing data dynamically in scripts.

12. Heredocs for Multi-line Input

What They Are: Heredocs allow multi-line strings or input directly in your scripts, improving readability when dealing with templates or bulk data.

Example: Generating an email template dynamically:

1
2
3
4
5
6
7
8
9
10
11
email_body=$(cat <<EOF
Hello Team,

This is a reminder for the upcoming deployment at midnight.

Regards,
DevOps
EOF
)

echo "$email_body" | mail -s "Deployment Reminder" [email protected]

Why Use Them:

  • They eliminate the need for complex string concatenations or external files.
  • Heredocs simplify handling multi-line content, like logs, templates, or commands, directly within your script.

13. eval for Dynamic Command Execution

What It Is: The eval command lets you execute a dynamically constructed string as a Bash command.

Example: Suppose you need to execute a command stored in a variable:

1
2
cmd="ls -l"
eval "$cmd"

Or dynamically set variables:

1
2
3
var_name="greeting"
eval "$var_name='Hello, World!'"
echo "$greeting"

Why Use It:

  • eval provides flexibility for handling dynamically generated commands or input.
  • Use with caution: While powerful, improper use of eval can lead to security risks if handling untrusted input.

14. Subshells for Isolated Execution

What They Are: A subshell is a child process in which commands can be executed without affecting the parent shell.

Example: Suppose you want to temporarily change directories and execute commands:

1
2
3
4
5
(current_dir=$(pwd)
cd /tmp
echo "Now in $(pwd)"
)
echo "Back in $current_dir"

Why Use Them:

  • Subshells allow temporary changes to variables, environments, or directories without impacting the main shell.
  • Ideal for running isolated operations that don’t pollute or modify the parent environment.

15. Named Pipes (FIFOs)

What They Are: Named pipes (or FIFOs) are special files that facilitate inter-process communication by acting as a buffer between commands.

Example: Let’s create a named pipe to transfer data between processes:

1
2
3
4
5
6
7
8
9
10
mkfifo my_pipe

# In one terminal: Write to the pipe
echo "Hello from process 1" > my_pipe

# In another terminal: Read from the pipe
cat < my_pipe

# Clean up
rm my_pipe

Why Use Them:

  • Named pipes enable asynchronous communication between processes, allowing data to flow without temporary files.
  • Useful for real-time processing scenarios, such as feeding logs or streaming data between commands.

Conclusion

These additional constructs—associative arrays, heredocs, eval, subshells, and named pipes—expand your Bash scripting toolkit to tackle more complex tasks.

By mastering these constructs, you’ll write more elegant, efficient, and maintainable scripts tailored to real-world engineering challenges.

Happy scripting! 🖥️

This post is licensed under CC BY 4.0 by the author.