When writing bash scripts that perform potentially destructive operations, implementing a dry-run mode becomes crucial for testing and safety. The core challenge lies in executing commands conditionally while maintaining script readability.
Most developers consider these approaches:
# Option 1: Wrapping each command in if statements
if [[ -z "$DRY_RUN" ]]; then
rm -rf /tmp/old_files
else
echo "Would execute: rm -rf /tmp/old_files"
fi
While functional, this approach leads to code duplication and reduced readability in complex scripts.
Here's an improved version of the function approach:
#!/bin/bash
dry_run() {
if [[ "${DRY_RUN:-false}" == "true" ]]; then
printf "[DRY RUN] %s\n" "$*"
return 0
fi
printf "[EXECUTING] %s\n" "$*"
"$@"
}
# Example usage:
dry_run cp source.txt destination.txt
DRY_RUN=true dry_run mv /old/path /new/path
For production scripts, we should add proper error handling:
dry_run() {
local cmd=("$@")
if [[ "${DRY_RUN:-false}" == "true" ]]; then
printf "[DRY RUN] %s\n" "${cmd[*]}"
return 0
fi
if ! "${cmd[@]}"; then
printf "Error executing: %s\n" "${cmd[*]}" >&2
return 1
fi
return 0
}
The basic approach fails with pipes. Here's a solution:
dry_run_eval() {
local cmd="$*"
if [[ "${DRY_RUN:-false}" == "true" ]]; then
printf "[DRY RUN] %s\n" "$cmd"
return 0
fi
eval "$cmd"
}
# Example with pipe:
dry_run_eval "find . -name '*.tmp' | xargs rm -f"
Bash has a built-in no-execution mode:
#!/bin/bash
if [[ "${DRY_RUN:-false}" == "true" ]]; then
set -n
echo "Running in dry-run mode (no commands will execute)"
fi
# Actual commands follow
rm -rf /tmp/cache
mkdir -p /backups
Consider these factors when implementing dry-run:
- Script complexity
- Need for accurate command preview
- Error handling requirements
- Maintainability
The function wrapper approach generally offers the best balance between flexibility and readability for most use cases.
When building production-grade bash scripts, implementing dry-run capability is crucial for:
- Validating command sequences without execution risk
- Debugging complex scripting logic
- Providing transparency in automation workflows
Here are three professional-grade methods with varying complexity levels:
1. The Function Wrapper Method
Most maintainable solution for medium-to-large scripts:
#!/bin/bash
dry_run=false
function safe_exec() {
if [[ $dry_run == true ]]; then
echo "[DRY-RUN] $@"
else
echo "[EXECUTING] $@"
"$@"
fi
return $?
}
# Usage examples:
safe_exec cp -v source.txt destination.txt
safe_exec rm -rf /tmp/expired_cache
# Enable dry-run mode
dry_run=true
safe_exec touch /var/log/app.log
2. Command Prefix Approach
For simpler scripts where you need minimal changes:
#!/bin/bash
DRY_RUN=${1:-false}
CMD_PREFIX=""
[[ "$DRY_RUN" == "true" ]] && CMD_PREFIX="echo"
# Actual commands become:
$CMD_PREFIX mkdir -p /opt/new_directory
$CMD_PREFIX chown appuser:appgroup /opt/new_directory
3. Advanced Trap-Based Solution
For framework-level implementation:
#!/bin/bash
trap 'dry_run_controller "$BASH_COMMAND"' DEBUG
dry_run_controller() {
local cmd="$1"
[[ $DRY_RUN ]] && { echo "[DRY] $cmd"; return 1; } || return 0
}
# Normal script commands follow:
mv old_file new_location
rsync -avz source/ destination/
Professional implementations often include:
# Color coding for better visibility
function color_echo() {
local color=$1
shift
echo -e "\e[${color}m$@\e[0m"
}
function dry_exec() {
if $DRY_RUN; then
color_echo 33 "DRY: $@"
return 0
else
color_echo 32 "EXEC: $@"
eval "$@"
return $?
fi
}
Consider these edge scenarios:
# 1. Piped commands
dry_run=false
[[ $dry_run ]] && echo "cat log.txt | grep ERROR" || cat log.txt | grep ERROR
# 2. Variable assignments
dry_run=true
[[ ! $dry_run ]] && actual_value=$(date) || echo "SIMULATED: actual_value=\$(date)"
# 3. Conditional logic
if [[ $dry_run ]]; then
echo "Would execute: [ $condition ] && action1 || action2"
else
[ $condition ] && action1 || action2
fi
For brownfield implementations:
#!/bin/bash
# Transform existing script by adding this preamble:
original_script() {
# Original script content goes here
mv source dest
rm -rf /tmp/cache
}
if [[ $1 == "--dry-run" ]]; then
echo "Dry run of original operations:"
typeset -f original_script | tail -n +3 | head -n -1
else
original_script
fi