Web Analytics

File Testing

Intermediate ~20 min read

Robust scripts always validate files before operating on them. This lesson dives deeper into file testing patterns, teaching you to build validation functions that handle edge cases gracefully. You'll learn to write defensive code that prevents errors and provides meaningful feedback!

Comprehensive File Testing

While we covered basic file tests in the Operators module, let's explore them in the context of real validation scenarios.

Output
Click Run to execute your code

Complete File Test Reference

Test True When Use Case
-e file File exists (any type) General existence check
-f file Regular file exists Before reading/writing files
-d dir Directory exists Before cd or listing
-L file Symbolic link Detecting symlinks
-r file File is readable Before cat, grep, etc.
-w file File is writable Before echo >, modification
-x file File is executable Before running scripts
-s file File size > 0 Detect empty files
-O file You own the file Permission checks
-G file Your group owns file Group permission checks
Best Practice: Chain tests with && for safety: [[ -f "$file" && -r "$file" ]] && cat "$file". This ensures you only read if the file exists AND is readable.

Building Validation Functions

Encapsulate file validation logic into reusable functions for cleaner, more maintainable code.

Output
Click Run to execute your code
Pro Tip: Use return codes in validation functions: return 0 for success, non-zero for failure. This lets callers use if validate_file "$f"; then ... naturally.

Safe File Operation Patterns

Combine tests into patterns that prevent errors and handle edge cases gracefully.

# Pattern 1: Safe read with fallback
safe_cat() {
    local file="$1"
    local default="$2"
    if [[ -f "$file" && -r "$file" ]]; then
        cat "$file"
    else
        echo "$default"
    fi
}
config=$(safe_cat "/etc/app.conf" "default_config")

# Pattern 2: Require file or exit
require_file() {
    local file="$1"
    if [[ ! -f "$file" ]]; then
        echo "ERROR: Required file not found: $file" >&2
        exit 1
    fi
}
require_file "/etc/critical.conf"

# Pattern 3: Wait for file with timeout
wait_for_file() {
    local file="$1"
    local timeout="${2:-30}"
    local count=0
    while [[ ! -f "$file" && $count -lt $timeout ]]; do
        sleep 1
        ((count++))
    done
    [[ -f "$file" ]]
}
wait_for_file "/tmp/ready.flag" 60 || exit 1

# Pattern 4: Process only if newer
process_if_newer() {
    local source="$1"
    local target="$2"
    if [[ "$source" -nt "$target" ]] || [[ ! -f "$target" ]]; then
        process "$source" > "$target"
    fi
}
Race Conditions: Be aware that file state can change between testing and using. For critical operations, use atomic operations like flock or write-to-temp-then-move patterns.

Common Mistakes

1. Testing without quoting

# Wrong - fails with spaces or special chars
[[ -f $filename ]]

# Correct - always quote
[[ -f "$filename" ]]

2. Assuming test order doesn't matter

# Wrong - -r test may error if file doesn't exist
[[ -r "$file" && -f "$file" ]]

# Correct - test existence first
[[ -f "$file" && -r "$file" ]]

3. Not handling empty variables

# Dangerous - if file is unset, tests current dir
[[ -d $file ]]

# Safer - use parameter expansion
[[ -d "${file:?File not specified}" ]]

# Or explicit check
[[ -n "$file" && -d "$file" ]]

Exercise: Config File Validator

Task: Create a function that validates configuration files!

Requirements:

  • Check file exists and is readable
  • Verify file is not empty
  • Check for required keys (e.g., "host", "port")
  • Return appropriate exit codes and messages
Show Solution
#!/bin/bash
# Config File Validator

validate_config() {
    local config_file="$1"
    local required_keys=("host" "port" "user")
    local errors=0

    echo "Validating: $config_file"

    # Check existence
    if [[ ! -f "$config_file" ]]; then
        echo "  ERROR: File not found"
        return 1
    fi

    # Check readable
    if [[ ! -r "$config_file" ]]; then
        echo "  ERROR: File not readable"
        return 1
    fi

    # Check not empty
    if [[ ! -s "$config_file" ]]; then
        echo "  ERROR: File is empty"
        return 1
    fi

    # Check required keys
    for key in "${required_keys[@]}"; do
        if ! grep -q "^$key=" "$config_file"; then
            echo "  ERROR: Missing required key: $key"
            ((errors++))
        fi
    done

    if [[ $errors -eq 0 ]]; then
        echo "  SUCCESS: Config is valid"
        return 0
    else
        echo "  FAILED: $errors error(s) found"
        return 1
    fi
}

# Test
config="/tmp/test.conf"
cat > "$config" << 'EOF'
host=localhost
port=8080
user=admin
EOF

validate_config "$config"
rm -f "$config"

Summary

  • Always Test: Check file existence and permissions before operations
  • Test Order: Check existence (-f) before permissions (-r, -w)
  • Quote Variables: Always quote paths in tests: [[ -f "$file" ]]
  • Use Functions: Encapsulate validation logic for reusability
  • Return Codes: Use 0 for success, non-zero for failure
  • Error Messages: Write to stderr: echo "error" >&2

What's Next?

Now let's explore Working with Directories. You'll learn to traverse directories, process files recursively, and use the find command for powerful directory operations!