PT0-002Chapter 80 of 104Objective 5.2

Bash Scripting for Automation

This chapter covers bash scripting for automation, a critical skill for penetration testers as defined in CompTIA PenTest+ PT0-002 objective 5.2. 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.

25 min read
Intermediate
Updated May 31, 2026

Bash Scripting: An Automated Task Scheduler

Think of bash scripting as an automated task scheduler for a busy office. 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.

How It Actually Works

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:

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:

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:

while [ condition ]; do
    commands
done

until loop:

until [ condition ]; do
    commands
done

Functions

Functions group commands for reuse:

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:

current_date=$(date)
# or backticks: `date`

Arithmetic

Integer arithmetic uses $(( expression )):

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:

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

Parsing Nmap output:

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

Automated exploitation:

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

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

Walk-Through

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.

What This Looks Like on the Job

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.

How PT0-002 Actually Tests This

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.

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`.

Easy to Mix Up

These come up on the exam all the time. Here's how to tell them apart.

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

Watch Out for These

Mistake

Bash scripts must have a .sh extension to run.

Correct

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.

Mistake

Using `$*` and `$@` is the same.

Correct

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.

Mistake

The `if` statement must use `[ ]` for conditions.

Correct

`[ ]` 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.

Mistake

A script will always use bash as the interpreter.

Correct

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.

Mistake

Exit code 0 always means success.

Correct

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.

Do You Actually Know This?

Reveal each answer, then mark whether you got it right. Score 60%+ to unlock the next chapter.

Frequently Asked Questions

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.

Terms Worth Knowing

Ready to put this to the test?

You've just covered Bash Scripting for Automation — now see how well it sticks with free PT0-002 practice questions. Full explanations included, no account needed.

Done with this chapter?