File Testing
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.
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 |
&& 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.
Click Run to execute your code
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
}
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!
Enjoying these tutorials?