# Bash Scripting for Automation

> Chapter 80 of the Courseiva PENTEST-PLUS curriculum — https://courseiva.com/learn/pentest-plus/bash-scripting-pt

**Official objective:** 5.2 — Tools Scripts

## Introduction

Tested in CompTIA PenTest+ PT0-002 objective 5.2, bash scripting for automation is a critical skill for penetration testers. Bash scripting allows testers to automate repetitive tasks, process data, and chain tools together efficiently. Approximately 5-10% of exam questions touch on scripting concepts, including variable usage, loops, conditionals, and common automation patterns. Mastery of bash scripting not only speeds up testing but also demonstrates a deep understanding of Linux environments, which is essential for the exam and real-world engagements.

## Bash Scripting: An Automated Task Scheduler

An automated task scheduler for a busy office is a system that automates routine tasks without human intervention. You have a receptionist (the shell) who follows a set of written instructions (the script) to handle routine tasks. The receptionist can read a list of names (variables), look up phone numbers (commands), and make calls (execute programs). If a call fails, she can try again (loops) or check a different list (conditionals). She can also take notes (output redirection) and file them in specific folders (files). The receptionist doesn't think; she just follows the instructions precisely, which is both a strength and a weakness. If the instructions say to call the same person 100 times, she will do it without question. This is like a bash script: it automates repetitive tasks without deviation. However, if the instructions are wrong—like a typo in a phone number—she will keep dialing the wrong number. Similarly, a bash script will faithfully execute incorrect commands. The key is to write clear, tested instructions that handle errors gracefully, just as you would train a receptionist to handle busy signals or wrong numbers.

## Core explanation

### What is Bash Scripting and Why It Exists

Bash (Bourne Again SHell) is both a command-line interpreter and a scripting language. A bash script is a plain text file containing a series of commands that the shell executes sequentially. Scripts automate tasks that would otherwise be performed manually, saving time and reducing errors. For penetration testers, scripts are essential for:
- Automating reconnaissance (e.g., scanning multiple hosts with Nmap)
- Parsing output from tools (e.g., extracting IP addresses from scan results)
- Chaining exploits or post-exploitation commands
- Creating custom tools for specific testing scenarios

### How Bash Scripts Work Internally

When a bash script is executed, the shell reads the file line by line. Each line is parsed into commands, arguments, and operators. The shell then forks a child process to execute each command, unless the command is a built-in (like `echo` or `cd`). Redirection operators (`>`, `>>`, `<`, `|`) modify file descriptors before execution. Environment variables and local variables are expanded using `${}` or `$`. The exit status of each command is stored in `$?`, which can be checked by conditionals.

### Key Components and Defaults

**Shebang:** The first line of a script is `#!/bin/bash`, which tells the system which interpreter to use. If missing, the script runs under the current shell, which may cause compatibility issues.

**Variables:** Declared as `VAR=value` (no spaces around `=`). Accessed as `$VAR` or `${VAR}`. By default, variables are global within the script. Local variables can be declared with `local` inside functions.

**Arrays:** Indexed arrays use `arr=(item1 item2)`; associative arrays (bash 4+) use `declare -A arr`. Access elements with `${arr[index]}`.

**Special Variables:**
- `$0` – script name
- `$1`, `$2`, ... – positional parameters
- `$#` – number of arguments
- `$@` – all arguments as separate words
- `$*` – all arguments as a single string
- `$?` – exit status of last command (0 success, non-zero failure)
- `$$` – process ID of the current script

**Exit Codes:** By convention, exit code 0 means success, 1-255 indicates failure. The script can exit with a specific code using `exit N`.

### Conditionals and Loops

**if-elif-else:**
```bash
if [ condition ]; then
    commands
elif [ condition ]; then
    commands
else
    commands
fi
```
Conditions use `[ ]` (test command) or `[[ ]]` (extended test with regex). Common tests:
- `-f file` – file exists and is regular
- `-d dir` – directory exists
- `-z string` – string is empty
- `-n string` – string is not empty
- `string1 = string2` – string equality
- `num1 -eq num2` – numeric equality

**for loop:**
```bash
for var in list; do
    commands
done
```
The list can be a space-separated list, a range `{1..5}`, or a command substitution `$(command)`.

**while loop:**
```bash
while [ condition ]; do
    commands
done
```
**until loop:**
```bash
until [ condition ]; do
    commands
done
```

### Functions

Functions group commands for reuse:
```bash
function_name() {
    commands
    [return N]
}
```
Arguments are passed as positional parameters inside the function. Use `local` to avoid variable pollution.

### Input and Output Redirection

- `>` – redirect stdout to file (overwrite)
- `>>` – redirect stdout to file (append)
- `2>` – redirect stderr
- `&>` – redirect both stdout and stderr
- `<` – redirect file to stdin
- `|` – pipe stdout of left command to stdin of right command

### Command Substitution

Capture output of a command into a variable:
```bash
current_date=$(date)
# or backticks: `date`
```

### Arithmetic

Integer arithmetic uses `$(( expression ))`:
```bash
a=$(( 5 + 3 * 2 ))
```

### Error Handling

Use `set -e` to exit on error, `set -u` to treat unset variables as errors, `set -o pipefail` to catch errors in pipes. Trap signals with `trap 'commands' SIGNAL`.

### Common Automation Patterns for Penetration Testing

**Mass scanning:**
```bash
#!/bin/bash
for ip in $(cat targets.txt); do
    nmap -sV $ip >> scan_results.txt
done
```

**Parsing Nmap output:**
```bash
#!/bin/bash
# Extract open ports from Nmap grepable output
grep -oP '\d+/open' $1 | cut -d'/' -f1
```

**Automated exploitation:**
```bash
#!/bin/bash
# Run multiple exploits against a target
for exploit in /usr/share/exploitdb/exploits/*.py; do
    python3 $exploit -t $1
    if [ $? -eq 0 ]; then
        echo "Exploit $exploit succeeded"
        break
    fi
done
```

**Data extraction and reporting:**
```bash
#!/bin/bash
# Extract IP:port from probe results
cat probe.txt | awk '{print $1":"$2}' > live_hosts.txt
```

### How Scripting Interacts with Other Technologies

Bash scripts often call other tools (nmap, curl, netcat, etc.) and parse their output. They can also interact with databases via CLI clients, send emails, and generate reports. On the PT0-002 exam, you may be asked to interpret or complete a script that uses common pentesting tools. Understanding how to chain commands and handle output is crucial.

### Best Practices

- Always include a shebang.
- Use meaningful variable names.
- Quote variables to prevent word splitting and globbing: `"$var"`.
- Check exit statuses and handle errors.
- Use functions for reusable code.
- Comment complex logic.
- Test scripts in a safe environment before running on targets.

## Real-world context

In enterprise penetration testing, bash scripting is indispensable for automating reconnaissance and post-exploitation tasks. For example, during a large-scale external assessment, a tester may need to scan hundreds of IP addresses across multiple subnets. Writing a manual Nmap command for each host is impractical. Instead, a bash script can read a list of targets from a file, run Nmap with specific options (e.g., `-sV -p1-1000`), and output results to individual files or a consolidated report. The script can also parse Nmap's grepable output to extract open ports and services, then feed those into further tools like Nikto or SQLmap. This automation reduces the time from hours to minutes.

Another scenario is during internal network testing where a tester needs to enumerate SMB shares, check for null sessions, and test default credentials. A bash script can loop through a list of IPs, run `smbclient -L //ip -N`, check the exit code, and log successful enumerations. If a share is accessible, the script can attempt to mount it and download files. This automation ensures consistent testing across many hosts and reduces the chance of missing a critical finding.

A common mistake in production scripts is failing to handle errors. For instance, if a script uses `nmap -oG - $target` and the target is unreachable, the output may be empty, causing downstream commands to fail. Without error checking, the script may continue with corrupted data. Similarly, not quoting variables can lead to word splitting issues when filenames or IPs contain spaces. Performance considerations include running too many parallel processes (e.g., backgrounding multiple Nmap scans) which can overload the network or the testing machine. Using `xargs -P` or GNU parallel can control concurrency. Finally, scripts should be idempotent where possible, so re-running them doesn't cause unintended side effects.

## Exam focus

For PT0-002 objective 5.2, the exam tests your ability to read, interpret, and write bash scripts for automation. You will not be asked to write a full script from scratch, but you may be given a script snippet and asked to identify its purpose, correct an error, or predict the output. Common exam traps include:

1. **Confusing `$@` and `$*`:** Candidates often think they are identical. `$@` preserves word boundaries (e.g., `"$@"` expands each argument as a separate quoted string), while `$*` combines all arguments into a single string. The exam may ask which one correctly handles arguments with spaces.

2. **Forgetting to quote variables:** In a script like `for file in $(ls *.txt)`, if a filename contains a space, the loop will split it. The correct approach is `for file in *.txt` (no command substitution) or using `find -print0` with `xargs -0`.

3. **Misunderstanding exit codes:** The exam may show a script that uses `if [ $? -eq 0 ]` after a command, but candidates forget that `$?` is overwritten by the test command itself. The correct pattern is to store `$?` in a variable immediately after the command.

4. **Ignoring the shebang:** A script without `#!/bin/bash` may run under a different shell (e.g., sh) which lacks features like `[[ ]]` or arrays. The exam may test whether a script will work as expected on a system where bash is not the default.

5. **Arithmetic vs. string comparison:** Using `-eq` for strings or `=` for numbers will cause errors. The exam expects you to know the correct operators.

Specific values to memorize: `$?` holds exit code, `$0` is script name, `$#` is argument count. The `test` command (`[ ]`) requires spaces around brackets. The `-z` test checks for empty string. `set -e` causes exit on error. `$(())` is for arithmetic. `$()` is preferred over backticks for command substitution.

To eliminate wrong answers, trace the script step by step, keeping track of variable values and exit codes. Look for missing spaces, incorrect operators, or unquoted variables. Remember that bash is whitespace-sensitive in many contexts.

## Step by step

1. **Create the script file** — Use a text editor like vim or nano to create a new file, e.g., `myscript.sh`. The `.sh` extension is conventional but not required. The file must be executable: `chmod +x myscript.sh`. The first line should be `#!/bin/bash` to ensure the script runs with bash. Without the shebang, the script will run under the user's current shell, which may differ (e.g., sh, zsh) and cause syntax errors. On many systems, bash is not the default shell, so the shebang is critical for portability.
2. **Define variables and functions** — Variables store data like target IPs, usernames, or paths. Example: `target="192.168.1.1"`. Functions encapsulate reusable logic. For instance, a function `scan_host()` could run an Nmap scan and return results. Use `local` inside functions to avoid overwriting global variables. Variables are case-sensitive and should be lowercase by convention to avoid conflict with environment variables (which are uppercase).
3. **Implement logic with conditionals and loops** — Conditionals (`if`, `case`) allow branching based on conditions like file existence or command success. Loops (`for`, `while`) iterate over lists or until a condition is met. For example, a `for` loop can iterate over IP addresses from a file: `for ip in $(cat targets.txt); do ... done`. A `while` loop can read a file line by line: `while read line; do ... done < file`. Always quote variables in conditions to handle spaces correctly.
4. **Execute commands and capture output** — Use command substitution `$(command)` to capture output into a variable. For example, `ports=$(nmap -p- $target | grep ^[0-9] | cut -d'/' -f1)`. This allows further processing. Redirections (`>`, `>>`, `2>`) can save output to files. Pipes (`|`) chain commands. For error handling, check `$?` after each command. The script can also use `set -e` to abort on any error, but this may be too aggressive for some tasks.
5. **Test and debug the script** — Run the script with `bash -x myscript.sh` to enable debug mode, which prints each command before execution. This helps identify syntax errors or logic flaws. Use `echo` statements to print variable values. Check for common mistakes like missing spaces around brackets in `[ ]`, unquoted variables, or incorrect arithmetic syntax. Test with sample data before using on actual targets. Also, ensure the script handles edge cases like empty files or missing arguments gracefully.

## Comparisons

### Bash Scripting vs Python Scripting

**Bash Scripting:**
- Native to Unix/Linux, no additional runtime required
- Best for simple automation and command chaining
- String manipulation can be cumbersome
- Less verbose, but syntax can be cryptic
- Limited data structures (associative arrays only in bash 4+)

**Python Scripting:**
- Cross-platform, requires Python interpreter
- Better for complex logic, data parsing, and networking
- Rich string and list manipulation methods
- More readable and maintainable for large scripts
- Full object-oriented programming support

## Common misconceptions

- **Misconception:** Bash scripts must have a .sh extension to run. **Reality:** The .sh extension is a convention, not a requirement. The script can be named anything, but it must be executable (`chmod +x`) and have a correct shebang. The system uses the shebang to determine the interpreter, not the file extension.
- **Misconception:** Using `$*` and `$@` is the same. **Reality:** They differ when quoted. `"$*"` expands to a single string (e.g., `"arg1 arg2 arg3"`), while `"$@"` expands to separate quoted strings (e.g., `"arg1" "arg2" "arg3"`). This affects how arguments with spaces are handled.
- **Misconception:** The `if` statement must use `[ ]` for conditions. **Reality:** `[ ]` is the test command. You can also use `[[ ]]` (extended test) which supports regex and pattern matching, and is safer with empty variables. Additionally, you can use `(( ))` for arithmetic conditions without needing `$` on variables.
- **Misconception:** A script will always use bash as the interpreter. **Reality:** If the shebang is missing or incorrect, the script runs under the user's current shell, which could be sh, dash, or zsh. These shells may not support bash-specific features like arrays or `[[ ]]`. Always include `#!/bin/bash` for portability.
- **Misconception:** Exit code 0 always means success. **Reality:** By convention, 0 means success, but a command or script can return any exit code. Some tools use non-zero codes for specific types of failures (e.g., 1 for general error, 2 for misuse). Always check the documentation.

## Key takeaways

- Always include `#!/bin/bash` as the first line to ensure the script runs with bash.
- Quote variables (`"$var"`) to prevent word splitting and globbing.
- Use `$()` for command substitution; backticks are deprecated.
- Check exit codes with `$?` immediately after the command, before it gets overwritten.
- Use `set -e` to exit on error, but be aware it can cause unexpected exits.
- For loops over files, use `for file in *.txt` instead of `for file in $(ls *.txt)`.
- Use `[[ ]]` for extended test conditions with regex support.
- Functions should use `local` variables to avoid side effects.
- Debug scripts with `bash -x script.sh`.
- Make scripts executable with `chmod +x script.sh`.

## FAQ

**How do I pass arguments to a bash script?**

Arguments are accessed via positional parameters: `$1`, `$2`, etc. `$0` is the script name, `$#` is the count of arguments, and `$@` or `$*` represent all arguments. For example, if you run `./script.sh arg1 arg2`, then `$1` is 'arg1', `$2` is 'arg2'. Use `"$@"` to pass all arguments to another command while preserving spaces.

**What is the difference between `[ ]` and `[[ ]]` in bash?**

`[ ]` is the standard `test` command; it requires spaces around brackets and does not support regex. `[[ ]]` is a bash keyword that supports pattern matching, regex (`=~`), and logical operators (`&&`, `||`). It also handles empty variables safely. For example, `[[ "$var" =~ ^[0-9]+$ ]]` checks if var is numeric. Use `[[ ]]` in bash scripts for more robust conditionals.

**How do I read a file line by line in bash?**

Use a while loop with the `read` command: `while IFS= read -r line; do echo "$line"; done < file.txt`. The `-r` prevents backslash interpretation, and `IFS=` preserves leading/trailing whitespace. This is safer than `for line in $(cat file.txt)` because it handles spaces and special characters correctly.

**How can I run multiple commands in parallel in a bash script?**

Use `&` to background a command: `command1 & command2 & wait`. The `wait` command waits for all background jobs to finish. For more control, use `xargs -P` or GNU `parallel`. Example: `cat targets.txt | xargs -P 5 -I {} nmap {} -oN {}.nmap` runs up to 5 Nmap scans in parallel.

**What is the purpose of the `set -e` command in bash?**

`set -e` causes the script to exit immediately if any command returns a non-zero exit code (i.e., fails). This is useful for catching errors early, but it can be too aggressive if a command is expected to fail (e.g., `grep` may return 1 if no match). Use `set +e` to disable it temporarily, or use `|| true` to allow failure.

**How do I create an array in bash and iterate over it?**

Declare an indexed array: `arr=('item1' 'item2' 'item3')`. Iterate: `for item in "${arr[@]}"; do echo "$item"; done`. For associative arrays (bash 4+): `declare -A assoc; assoc[key1]='value1';` iterate keys with `for key in "${!assoc[@]}"; do echo "${assoc[$key]}"; done`.

**How can I check if a file exists in bash?**

Use the `-f` test: `if [ -f "$file" ]; then echo "File exists"; fi`. For directory, use `-d`. You can also check for non-existence with `! -f`. The `-e` test checks for any file type (including directories, symlinks). Always quote the variable to handle spaces.

---

Interactive version with quiz and diagrams: https://courseiva.com/learn/pentest-plus/bash-scripting-pt
