B: Debugging Reference#

Solutions to the most frequent Bash scripting errors and debugging techniques.

Common Errors and Solutions#

1. “Command not found”#

Causes:

  • Typo in command name

  • Command not in PATH

  • Script file not executable

  • Trying to run system command that doesn’t exist

Solutions:

# Check if command exists
command -v mycommand
which mycommand
type mycommand

# Make script executable
chmod +x script.sh

# Run with explicit interpreter
bash script.sh

2. “Permission denied”#

Causes:

  • Script not executable

  • No execute permissions on parent directories

  • Insufficient permissions for file operation

Solutions:

# Add execute permission
chmod +x script.sh

# Check current permissions
ls -l script.sh

# Make directory traversable
chmod +x /path/to/dir

# Run with sudo if needed (carefully)
sudo ./script.sh

3. “No such file or directory”#

Causes:

  • File doesn’t exist

  • Wrong path (relative vs absolute)

  • File deleted between check and use

  • Typo in filename

Solutions:

# Verify file exists before using
if [[ -f "$file" ]]; then
  cat "$file"
else
  echo "File not found: $file" >&2
  exit 1
fi

# Check if path is correct
pwd
ls "$file"

# Use absolute paths when possible
cat /full/path/to/file

4. Variable is Empty or Unset#

Causes:

  • Variable never assigned

  • Variable in different scope

  • Export not used for child processes

  • Variable cleared during pipeline

Solutions:

# Check if variable is set
if [[ -z "$var" ]]; then
  echo "Variable is empty"
fi

# Provide default value
value="${var:-default}"

# Export for child processes
export MY_VAR="value"
source script.sh  # Uses MY_VAR

# Avoid subshell issues
var="initial"
echo "test" | while read line; do
  var="$line"
done
echo "$var"  # Still "initial"!

# Fix with process substitution
while read line; do
  var="$line"
done < <(echo "test")
echo "$var"  # Now "test"

5. Word Splitting Issues#

Causes:

  • Variables not quoted

  • Command substitution not quoted

  • Array expansion without quotes

Solutions:

# WRONG - word splitting occurs
files=$(ls *.txt)
for f in $files; do
  echo "$f"  # Breaks on spaces in filenames
done

# CORRECT - quote variables
for f in $files; do
  echo "$f"  # Still breaks - use array instead
done

# BEST - use array
mapfile -t files < <(find . -name "*.txt")
for f in "${files[@]}"; do
  echo "$f"  # Works with any filename
done

6. Quote and Escaping Problems#

Causes:

  • Mixed quote types

  • Special characters not escaped

  • Regex not properly quoted

  • Variable expansion when not wanted

Solutions:

# Single quotes prevent all expansion
echo '$VAR'  # Literal: $VAR

# Double quotes allow expansion
echo "$VAR"  # Expanded value

# Escape special characters
echo "Price is \$100"  # Literal: $100
echo "Path: \$HOME"    # Literal: $HOME
echo "Backslash: \\"   # Literal: \

# Use $'...' for special characters
echo $'Line1\nLine2'   # Newline
echo $'Tab\tSeparated' # Tab

7. Exit Code Problems#

Causes:

  • Not checking exit codes

  • Using wrong variable ($? gets overwritten)

  • Pipeline hiding errors

  • Forgetting to check specific commands

Solutions:

# Check immediately after command
if ! command; then
  echo "Command failed with exit code $?"
  exit 1
fi

# Save exit code immediately
command
exit_code=$?
if [[ $exit_code -ne 0 ]]; then
  echo "Failed with code $exit_code"
fi

# Check all commands in pipeline
set -o pipefail
cat file.txt | grep pattern | sort

# Use || and && for simple cases
command && echo "Success" || echo "Failed"

8. Quoting Arguments with Spaces#

Causes:

  • Passing arguments with spaces without quoting

  • Function arguments not quoted

  • Array subscripts not quoted

Solutions:

# WRONG
function copy_file() {
  cp $1 $2  # Breaks if $1 has spaces
}

# CORRECT
function copy_file() {
  cp "$1" "$2"  # Properly handles spaces
}

# WRONG - word splitting in function call
copy_file my file.txt  # Treated as 3 args!

# CORRECT
copy_file "my file.txt" "output file.txt"

9. Iteration Over Arrays#

Causes:

  • Using $array instead of ${array[@]}

  • Not quoting array expansion

  • Using wrong array syntax

Solutions:

arr=(file1.txt "file 2.txt" file3.txt)

# WRONG - only first element
for f in $arr; do
  echo "$f"  # Only: file1.txt
done

# WRONG - word splitting
for f in ${arr}; do
  echo "$f"  # Breaks on spaces
done

# CORRECT - iterate all elements
for f in "${arr[@]}"; do
  echo "$f"  # All files properly handled
done

# Correct with indirect variable
mapfile -t lines < file.txt
for line in "${lines[@]}"; do
  process "$line"
done

10. Function Return Values#

Causes:

  • Using return for output (only sets exit code)

  • Not capturing function output

  • Mixing stdout and return codes

Solutions:

# WRONG - return is for exit codes only
function get_name() {
  return "John"  # Sets exit code to "John" (invalid)
}

# For output, use echo
function get_name() {
  echo "John"
}
name=$(get_name)

# For exit codes, use return
function is_valid() {
  [[ -f "$1" ]] && return 0 || return 1
}

# Combine both
function process_file() {
  [[ -f "$1" ]] || return 1  # Return code
  echo "$(cat "$1")"         # Output
}

output=$(process_file file.txt)
if [[ $? -eq 0 ]]; then
  echo "$output"
fi

Debugging Techniques#

1. Verbose Mode#

# Set at top of script
set -x  # Print commands as executed

# Or run script with -x flag
bash -x script.sh

# Disable verbose in sections
set +x  # Turn off
SOME_COMMANDS
set -x  # Turn back on

2. Strict Mode#

#!/bin/bash
set -euo pipefail

# e = exit on any error
# u = error on undefined variables
# o pipefail = pipe fails if any command fails

# Add trap for better error reporting
trap 'echo "Error: Script failed at line $LINENO"' ERR

4. Validate Input Early#

validate_args() {
  if [[ $# -lt 2 ]]; then
    echo "Usage: script.sh FILE OUTPUT" >&2
    exit 1
  fi
  
  if [[ ! -f "$1" ]]; then
    echo "Error: File not found: $1" >&2
    exit 1
  fi
  
  if [[ -z "$2" ]]; then
    echo "Error: Output filename cannot be empty" >&2
    exit 1
  fi
}

validate_args "$@"

5. Test Commands Interactively#

# Use bash's test facilities
$ bash -n script.sh  # Syntax check (no execution)
$ bash -x script.sh  # Execute with trace

# Test conditions
$ [[ "string" =~ pattern ]] && echo "Match" || echo "No match"
$ (( 5 > 3 )) && echo "True"

# Check exit code
$ false
$ echo $?  # Prints: 1

6. Use shellcheck#

# Install shellcheck
apt-get install shellcheck  # Debian/Ubuntu
brew install shellcheck     # macOS

# Run on script
shellcheck script.sh

# Fix issues
shellcheck -f diff script.sh

# Suppress specific warnings
# shellcheck disable=SC2086  # Disable word splitting warning

7. Break Script into Testable Functions#

# Instead of one long script, break into functions
process_file() {
  # Can test independently
  echo "Processing: $1"
}

validate_input() {
  [[ -f "$1" ]]
}

main() {
  for file in "$@"; do
    if validate_input "$file"; then
      process_file "$file"
    fi
  done
}

# Test individual functions
# source script.sh
# validate_input "test.txt"

Error Message Best Practices#

# Good error messages go to stderr (>&2)
echo "Error: Input file not found" >&2

# Include context
echo "Error: Cannot find config at $CONFIG_FILE" >&2

# Suggest solution
echo "Error: Missing required argument. Usage: script.sh FILE" >&2

# Use consistent formatting
error() {
  echo "ERROR: $*" >&2
  exit 1
}

warning() {
  echo "WARNING: $*" >&2
}

info() {
  echo "INFO: $*"
}

# Usage
[[ -f "$file" ]] || error "File not found: $file"

Common Gotchas#

Issue

Problem

Solution

$var=value

Tries to execute var=value as command

Use var=value without $

[ $var ]

Ambiguous when var is empty

Use [ -n "$var" ] explicitly

for x in $list

Breaks on spaces

Use for x in "${arr[@]}"

$((...)) vs $((...)

Extra parenthesis breaks syntax

Use $((expr)) only

if grep ...

Pipeline hides error

Use if ! grep ... with set -e

; before then

Syntax error in some forms

Use newline: then on new line

Comments in strings

# doesn’t start comment in strings

Only comments are truly outside strings

cd in subshell

Directory change lost

Save path or use pushd/popd