Comprehensive Reference Guide β File tests, permissions, stat command, timestamps, and file type detection
Bash provides extensive file testing capabilities through test operators ([ ] and [[ ]]) and the stat command. These tools let you check file existence, type, permissions, ownership, size, and timestampsβessential for writing robust scripts that handle files safely.
Existence & Type: Check if files exist and what type they are (regular, directory, symlink, etc.)
Permissions: Test read, write, execute permissions and special bits
Comparison: Compare files by modification time and identity
Attributes: Get detailed metadata with stat
| Test | Description | Example |
|---|---|---|
-e file |
File exists (any type) | [[ -e /etc/passwd ]] |
-f file |
Regular file exists | [[ -f config.txt ]] |
-d file |
Directory exists | [[ -d /var/log ]] |
-L file |
Symbolic link exists | [[ -L /usr/bin/python ]] |
-h file |
Symbolic link (same as -L) | [[ -h link ]] |
-b file |
Block device exists | [[ -b /dev/sda ]] |
-c file |
Character device exists | [[ -c /dev/tty ]] |
-p file |
Named pipe (FIFO) exists | [[ -p /tmp/mypipe ]] |
-S file |
Socket exists | [[ -S /var/run/docker.sock ]] |
| Test | Description | Example |
|---|---|---|
-r file |
File is readable | [[ -r file.txt ]] |
-w file |
File is writable | [[ -w file.txt ]] |
-x file |
File is executable | [[ -x script.sh ]] |
-u file |
Set-user-ID bit set | [[ -u /usr/bin/passwd ]] |
-g file |
Set-group-ID bit set | [[ -g file ]] |
-k file |
Sticky bit set | [[ -k /tmp ]] |
-O file |
Owned by effective UID | [[ -O myfile ]] |
-G file |
Owned by effective GID | [[ -G myfile ]] |
| Test | Description | Example |
|---|---|---|
-s file |
File exists and size > 0 | [[ -s data.txt ]] |
-t fd |
File descriptor is terminal | [[ -t 0 ]] (stdin) |
| Test | Description | Example |
|---|---|---|
file1 -nt file2 |
file1 newer than file2 | [[ a.txt -nt b.txt ]] |
file1 -ot file2 |
file1 older than file2 | [[ a.txt -ot b.txt ]] |
file1 -ef file2 |
Same device and inode (hard link) | [[ a -ef b ]] |
[[ ]] for file testsβit handles spaces in filenames better and doesn't require quoting variables. [ ] is POSIX but more error-prone.
The stat command provides detailed file attributes beyond what test operators offer.
| Format | Description | Example Output |
|---|---|---|
%n | File name | myfile.txt |
%s | Size in bytes | 1234 |
%b | Blocks allocated | 8 |
%B | Block size | 512 |
%F | File type | regular file |
%a | Octal permissions | 644 |
%A | Human permissions | -rw-r--r-- |
%u | Owner UID | 1000 |
%U | Owner name | craig |
%g | Group GID | 1000 |
%G | Group name | craig |
%h | Hard link count | 1 |
%i | Inode number | 12345678 |
%X | Access time (epoch) | 1731958245 |
%Y | Modify time (epoch) | 1731958245 |
%Z | Change time (epoch) | 1731958245 |
%W | Birth time (epoch) | 1731958245 |
%x | Access time (human) | 2025-11-18 14:30:45 |
%y | Modify time (human) | 2025-11-18 14:30:45 |
%z | Change time (human) | 2025-11-18 14:30:45 |
# Default output
stat filename
# Specific format
stat -c '%s' filename # Size only
stat -c '%a %U:%G' filename # Permissions and owner
stat -c '%Y' filename # Mtime as epoch
# Multiple files
stat -c '%n: %s bytes' *.txt
Essential tests for checking if files exist and what type they are.
#!/bin/bash
# Basic file existence and type checking
# Create test files for demonstration
mkdir -p /tmp/test_files
touch /tmp/test_files/regular.txt
mkdir /tmp/test_files/directory
ln -sf /tmp/test_files/regular.txt /tmp/test_files/symlink
mkfifo /tmp/test_files/pipe 2>/dev/null
echo "=== File Existence Tests ==="
# Test if file exists (any type)
file="/tmp/test_files/regular.txt"
if [[ -e "$file" ]]; then
echo "$file exists"
else
echo "$file does not exist"
fi
# Test for non-existent file
file="/tmp/test_files/nonexistent"
if [[ ! -e "$file" ]]; then
echo "$file does not exist"
fi
echo ""
echo "=== File Type Tests ==="
test_type() {
local path=$1
local name=$(basename "$path")
if [[ -f "$path" ]]; then
echo "$name: regular file"
elif [[ -d "$path" ]]; then
echo "$name: directory"
elif [[ -L "$path" ]]; then
echo "$name: symbolic link"
elif [[ -p "$path" ]]; then
echo "$name: named pipe"
elif [[ -b "$path" ]]; then
echo "$name: block device"
elif [[ -c "$path" ]]; then
echo "$name: character device"
elif [[ -S "$path" ]]; then
echo "$name: socket"
else
echo "$name: unknown type"
fi
}
for item in /tmp/test_files/*; do
test_type "$item"
done
echo ""
echo "=== Check Before Operations ==="
# Safe file creation
file="/tmp/test_files/newfile.txt"
if [[ -e "$file" ]]; then
echo "File already exists: $file"
else
touch "$file"
echo "Created: $file"
fi
# Safe directory creation
dir="/tmp/test_files/newdir"
if [[ -d "$dir" ]]; then
echo "Directory already exists: $dir"
else
mkdir "$dir"
echo "Created directory: $dir"
fi
echo ""
echo "=== Handle Symlinks Carefully ==="
# -L tests the link itself, -f follows it
link="/tmp/test_files/symlink"
if [[ -L "$link" ]]; then
echo "$link is a symlink"
# Check if target exists
if [[ -e "$link" ]]; then
echo " Target exists: $(readlink -f "$link")"
else
echo " Broken link: target doesn't exist"
fi
fi
# -f follows symlink
if [[ -f "$link" ]]; then
echo "$link points to a regular file"
fi
echo ""
echo "=== Practical: Config File Check ==="
check_config() {
local config=$1
if [[ ! -e "$config" ]]; then
echo "ERROR: Config not found: $config"
return 1
fi
if [[ -d "$config" ]]; then
echo "ERROR: Config is a directory: $config"
return 1
fi
if [[ ! -f "$config" ]]; then
echo "ERROR: Config is not a regular file: $config"
return 1
fi
echo "OK: Config file valid: $config"
return 0
}
check_config "/etc/passwd"
check_config "/tmp"
check_config "/nonexistent"
# Cleanup
rm -rf /tmp/test_files
Checking read, write, execute permissions and special bits.
#!/bin/bash
# Permission testing
# Create test files
mkdir -p /tmp/perm_test
touch /tmp/perm_test/readable.txt
chmod 444 /tmp/perm_test/readable.txt
touch /tmp/perm_test/writable.txt
chmod 666 /tmp/perm_test/writable.txt
touch /tmp/perm_test/executable.sh
chmod 755 /tmp/perm_test/executable.sh
touch /tmp/perm_test/noaccess.txt
chmod 000 /tmp/perm_test/noaccess.txt
echo "=== Basic Permission Tests ==="
test_perms() {
local file=$1
local name=$(basename "$file")
printf "%-20s " "$name:"
[[ -r "$file" ]] && printf "r" || printf "-"
[[ -w "$file" ]] && printf "w" || printf "-"
[[ -x "$file" ]] && printf "x" || printf "-"
echo ""
}
for f in /tmp/perm_test/*; do
test_perms "$f"
done
echo ""
echo "=== Permission Checks Before Operations ==="
# Check before reading
file="/tmp/perm_test/readable.txt"
if [[ -r "$file" ]]; then
echo "Can read $file"
# cat "$file"
else
echo "Cannot read $file"
fi
# Check before writing
file="/tmp/perm_test/noaccess.txt"
if [[ -w "$file" ]]; then
echo "Can write to $file"
else
echo "Cannot write to $file (no permission)"
fi
# Check before executing
file="/tmp/perm_test/executable.sh"
if [[ -x "$file" ]]; then
echo "Can execute $file"
else
echo "Cannot execute $file"
fi
echo ""
echo "=== Special Permission Bits ==="
# Check setuid
if [[ -u /usr/bin/passwd ]]; then
echo "/usr/bin/passwd has setuid bit"
fi
# Check sticky bit on /tmp
if [[ -k /tmp ]]; then
echo "/tmp has sticky bit"
fi
# Check setgid
check_setgid() {
local file=$1
if [[ -g "$file" ]]; then
echo "$file has setgid bit"
else
echo "$file does not have setgid bit"
fi
}
echo ""
echo "=== Ownership Tests ==="
# Check if current user owns file
file="/tmp/perm_test/readable.txt"
if [[ -O "$file" ]]; then
echo "You own $file"
fi
# Check if current group owns file
if [[ -G "$file" ]]; then
echo "Your group owns $file"
fi
echo ""
echo "=== Practical: Safe File Operations ==="
safe_write() {
local file=$1
local content=$2
# Check parent directory
local dir=$(dirname "$file")
if [[ ! -d "$dir" ]]; then
echo "ERROR: Directory doesn't exist: $dir"
return 1
fi
if [[ ! -w "$dir" ]]; then
echo "ERROR: Cannot write to directory: $dir"
return 1
fi
# Check file if exists
if [[ -e "$file" ]]; then
if [[ ! -w "$file" ]]; then
echo "ERROR: File not writable: $file"
return 1
fi
if [[ -d "$file" ]]; then
echo "ERROR: Is a directory: $file"
return 1
fi
fi
echo "$content" > "$file"
echo "OK: Written to $file"
return 0
}
safe_write "/tmp/perm_test/output.txt" "Hello World"
safe_write "/tmp/perm_test/noaccess.txt" "Should fail"
echo ""
echo "=== Practical: Script Self-Check ==="
script_check() {
local script=$1
if [[ ! -f "$script" ]]; then
echo "ERROR: Script not found"
return 1
fi
if [[ ! -r "$script" ]]; then
echo "ERROR: Script not readable"
return 1
fi
if [[ ! -x "$script" ]]; then
echo "WARNING: Script not executable"
echo " Run: chmod +x $script"
fi
echo "Script checks passed"
return 0
}
# Cleanup
rm -rf /tmp/perm_test
Testing file size, checking for empty files, and size comparisons.
#!/bin/bash
# File size and content tests
# Create test files
mkdir -p /tmp/size_test
echo "Hello World" > /tmp/size_test/small.txt
dd if=/dev/zero of=/tmp/size_test/large.bin bs=1M count=5 2>/dev/null
touch /tmp/size_test/empty.txt
printf "No newline" > /tmp/size_test/nonewline.txt
echo "=== Empty File Test (-s) ==="
for f in /tmp/size_test/*; do
name=$(basename "$f")
if [[ -s "$f" ]]; then
echo "$name: has content"
else
echo "$name: empty"
fi
done
echo ""
echo "=== Get File Sizes ==="
# Using stat
for f in /tmp/size_test/*; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
printf "%-20s %10d bytes\n" "$name" "$size"
done
echo ""
echo "=== Human-Readable Sizes ==="
human_size() {
local bytes=$1
if ((bytes >= 1073741824)); then
echo "$(echo "scale=2; $bytes/1073741824" | bc) GB"
elif ((bytes >= 1048576)); then
echo "$(echo "scale=2; $bytes/1048576" | bc) MB"
elif ((bytes >= 1024)); then
echo "$(echo "scale=2; $bytes/1024" | bc) KB"
else
echo "$bytes bytes"
fi
}
for f in /tmp/size_test/*; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
human=$(human_size $size)
printf "%-20s %s\n" "$name" "$human"
done
echo ""
echo "=== Size Comparisons ==="
file="/tmp/size_test/large.bin"
size=$(stat -c '%s' "$file")
# Check size thresholds
if ((size > 1048576)); then
echo "$file is larger than 1 MB"
fi
if ((size < 10485760)); then
echo "$file is smaller than 10 MB"
fi
echo ""
echo "=== Practical: Log Rotation Check ==="
check_log_size() {
local log=$1
local max_mb=$2
local max_bytes=$((max_mb * 1048576))
if [[ ! -f "$log" ]]; then
echo "$log: not found"
return
fi
local size=$(stat -c '%s' "$log")
local size_mb=$(echo "scale=2; $size/1048576" | bc)
if ((size > max_bytes)); then
echo "$log: ${size_mb}MB - ROTATE NEEDED (max ${max_mb}MB)"
else
echo "$log: ${size_mb}MB - OK (max ${max_mb}MB)"
fi
}
check_log_size "/tmp/size_test/large.bin" 10
check_log_size "/tmp/size_test/large.bin" 2
echo ""
echo "=== Practical: Disk Space Check ==="
# Check if file can be created with size
check_space() {
local dir=$1
local needed_mb=$2
local avail_kb=$(df -k "$dir" | awk 'NR==2 {print $4}')
local avail_mb=$((avail_kb / 1024))
if ((avail_mb >= needed_mb)); then
echo "OK: ${avail_mb}MB available, need ${needed_mb}MB"
return 0
else
echo "ERROR: Only ${avail_mb}MB available, need ${needed_mb}MB"
return 1
fi
}
check_space "/tmp" 100
echo ""
echo "=== Count Lines in File ==="
file="/tmp/size_test/small.txt"
lines=$(wc -l < "$file")
chars=$(wc -c < "$file")
words=$(wc -w < "$file")
echo "File: $file"
echo " Lines: $lines"
echo " Words: $words"
echo " Chars: $chars"
# Cleanup
rm -rf /tmp/size_test
Working with modification, access, and change times using stat and test operators.
#!/bin/bash
# File timestamp operations
# Create test files with different times
mkdir -p /tmp/time_test
touch -d "2025-01-01 10:00:00" /tmp/time_test/january.txt
touch -d "2025-06-15 12:00:00" /tmp/time_test/june.txt
touch /tmp/time_test/today.txt # Current time
echo "=== Get File Timestamps ==="
for f in /tmp/time_test/*; do
name=$(basename "$f")
# Human readable
mtime=$(stat -c '%y' "$f")
echo "$name:"
echo " Modified: ${mtime%.*}"
done
echo ""
echo "=== Timestamp as Epoch ==="
for f in /tmp/time_test/*; do
name=$(basename "$f")
epoch=$(stat -c '%Y' "$f")
echo "$name: $epoch"
done
echo ""
echo "=== All Three Timestamps ==="
file="/tmp/time_test/today.txt"
echo "File: $file"
echo " Access: $(stat -c '%x' "$file" | cut -d. -f1)"
echo " Modify: $(stat -c '%y' "$file" | cut -d. -f1)"
echo " Change: $(stat -c '%z' "$file" | cut -d. -f1)"
echo ""
echo "=== Compare File Times (-nt, -ot) ==="
file1="/tmp/time_test/january.txt"
file2="/tmp/time_test/june.txt"
file3="/tmp/time_test/today.txt"
# Newer than
if [[ "$file2" -nt "$file1" ]]; then
echo "june.txt is newer than january.txt"
fi
if [[ "$file3" -nt "$file2" ]]; then
echo "today.txt is newer than june.txt"
fi
# Older than
if [[ "$file1" -ot "$file2" ]]; then
echo "january.txt is older than june.txt"
fi
echo ""
echo "=== Files Modified in Time Range ==="
# Find files modified in the last N days
find_recent() {
local dir=$1
local days=$2
echo "Files modified in last $days days in $dir:"
find "$dir" -maxdepth 1 -type f -mtime -$days -printf " %f (%.19TY-%Tm-%Td %TH:%TM)\n"
}
find_recent /tmp/time_test 30
echo ""
echo "=== Practical: Check If File Is Stale ==="
is_stale() {
local file=$1
local max_age_secs=$2
if [[ ! -f "$file" ]]; then
echo "File not found: $file"
return 2
fi
local now=$(date +%s)
local mtime=$(stat -c '%Y' "$file")
local age=$((now - mtime))
if ((age > max_age_secs)); then
local age_days=$((age / 86400))
echo "$file is stale (${age_days} days old)"
return 0
else
local age_hours=$((age / 3600))
echo "$file is fresh (${age_hours} hours old)"
return 1
fi
}
is_stale /tmp/time_test/january.txt 86400 # 1 day
is_stale /tmp/time_test/today.txt 86400
echo ""
echo "=== Practical: Conditional Rebuild ==="
# Only rebuild if source is newer than target
needs_rebuild() {
local source=$1
local target=$2
# Target doesn't exist - needs build
if [[ ! -e "$target" ]]; then
echo "$target doesn't exist - needs build"
return 0
fi
# Source is newer - needs rebuild
if [[ "$source" -nt "$target" ]]; then
echo "$source is newer than $target - needs rebuild"
return 0
fi
echo "$target is up to date"
return 1
}
# Simulate source and target
touch /tmp/time_test/source.c
sleep 0.1
touch /tmp/time_test/target.o
needs_rebuild /tmp/time_test/source.c /tmp/time_test/target.o
# Modify source
touch /tmp/time_test/source.c
needs_rebuild /tmp/time_test/source.c /tmp/time_test/target.o
echo ""
echo "=== Format Timestamps ==="
file="/tmp/time_test/today.txt"
epoch=$(stat -c '%Y' "$file")
echo "Various formats for $file:"
echo " Epoch: $epoch"
echo " ISO: $(date -d @$epoch '+%Y-%m-%dT%H:%M:%S')"
echo " Human: $(date -d @$epoch '+%B %d, %Y %I:%M %p')"
# Cleanup
rm -rf /tmp/time_test
Extracting comprehensive file metadata with the stat command.
#!/bin/bash
# Detailed file attributes with stat
# Create test file
mkdir -p /tmp/stat_test
echo "Test content" > /tmp/stat_test/sample.txt
chmod 644 /tmp/stat_test/sample.txt
file="/tmp/stat_test/sample.txt"
echo "=== Default stat Output ==="
stat "$file"
echo ""
echo "=== Custom Format Output ==="
echo "File: $file"
echo " Size: $(stat -c '%s' "$file") bytes"
echo " Blocks: $(stat -c '%b' "$file")"
echo " IO Block: $(stat -c '%o' "$file")"
echo " Type: $(stat -c '%F' "$file")"
echo " Inode: $(stat -c '%i' "$file")"
echo " Links: $(stat -c '%h' "$file")"
echo " Permissions: $(stat -c '%a' "$file") ($(stat -c '%A' "$file"))"
echo " Owner: $(stat -c '%U' "$file") ($(stat -c '%u' "$file"))"
echo " Group: $(stat -c '%G' "$file") ($(stat -c '%g' "$file"))"
echo ""
echo "=== Parse Specific Attributes ==="
# Get permissions as octal
perms=$(stat -c '%a' "$file")
echo "Octal permissions: $perms"
# Parse permission bits
owner=$((perms / 100))
group=$(((perms / 10) % 10))
other=$((perms % 10))
echo " Owner: $owner Group: $group Other: $other"
# Check specific permissions from octal
if ((owner & 4)); then echo " Owner can read"; fi
if ((owner & 2)); then echo " Owner can write"; fi
if ((owner & 1)); then echo " Owner can execute"; fi
echo ""
echo "=== Multiple Files ==="
touch /tmp/stat_test/file{1,2,3}.txt
echo "File listing with attributes:"
printf "%-15s %8s %10s %s\n" "Name" "Size" "Perms" "Modified"
printf "%-15s %8s %10s %s\n" "----" "----" "-----" "--------"
for f in /tmp/stat_test/*.txt; do
name=$(basename "$f")
size=$(stat -c '%s' "$f")
perms=$(stat -c '%a' "$f")
mtime=$(stat -c '%y' "$f" | cut -d. -f1)
printf "%-15s %8d %10s %s\n" "$name" "$size" "$perms" "$mtime"
done
echo ""
echo "=== Device Information ==="
if [[ -b /dev/sda ]] || [[ -c /dev/null ]]; then
echo "Block device /dev/sda (if exists):"
stat -c ' Device: %t,%T Inode: %i' /dev/sda 2>/dev/null || echo " Not available"
echo "Character device /dev/null:"
stat -c ' Device: %t,%T Inode: %i' /dev/null
fi
echo ""
echo "=== Practical: File Report Function ==="
file_report() {
local file=$1
if [[ ! -e "$file" ]]; then
echo "File not found: $file"
return 1
fi
echo "=== File Report: $file ==="
stat -c "
Type: %F
Size: %s bytes
Permissions: %A (%a)
Owner: %U:%G
Inode: %i
Hard Links: %h
Modified: %y
Accessed: %x
Changed: %z" "$file"
}
file_report "$file"
echo ""
echo "=== Export as JSON-like ==="
stat -c '{
"name": "%n",
"size": %s,
"permissions": "%a",
"owner": "%U",
"group": "%G",
"mtime": %Y
}' "$file"
# Cleanup
rm -rf /tmp/stat_test
Comparing files by identity, hard links, and content.
#!/bin/bash
# File comparison tests
# Create test files
mkdir -p /tmp/compare_test
echo "Content A" > /tmp/compare_test/file_a.txt
echo "Content A" > /tmp/compare_test/file_a_copy.txt
echo "Content B" > /tmp/compare_test/file_b.txt
# Create hard link
ln /tmp/compare_test/file_a.txt /tmp/compare_test/file_a_hardlink.txt
# Create symlink
ln -s /tmp/compare_test/file_a.txt /tmp/compare_test/file_a_symlink.txt
echo "=== Same File Test (-ef) ==="
# -ef tests if files are the same (same device and inode)
file1="/tmp/compare_test/file_a.txt"
file2="/tmp/compare_test/file_a_hardlink.txt"
if [[ "$file1" -ef "$file2" ]]; then
echo "$file1 and $file2 are the same file (hard link)"
fi
# Different files with same content
file3="/tmp/compare_test/file_a_copy.txt"
if [[ "$file1" -ef "$file3" ]]; then
echo "Same file"
else
echo "$file1 and $file3 are different files (even with same content)"
fi
echo ""
echo "=== Inode Comparison ==="
echo "Inodes:"
for f in /tmp/compare_test/*.txt; do
name=$(basename "$f")
inode=$(stat -c '%i' "$f")
printf " %-25s inode: %s\n" "$name" "$inode"
done
echo ""
echo "=== Content Comparison ==="
# Compare by content (not bash built-in, uses cmp/diff)
compare_content() {
local file1=$1
local file2=$2
if cmp -s "$file1" "$file2"; then
echo "$(basename "$file1") and $(basename "$file2"): identical content"
else
echo "$(basename "$file1") and $(basename "$file2"): different content"
fi
}
compare_content /tmp/compare_test/file_a.txt /tmp/compare_test/file_a_copy.txt
compare_content /tmp/compare_test/file_a.txt /tmp/compare_test/file_b.txt
echo ""
echo "=== Checksum Comparison ==="
echo "MD5 checksums:"
for f in /tmp/compare_test/*.txt; do
if [[ -f "$f" && ! -L "$f" ]]; then
name=$(basename "$f")
md5=$(md5sum "$f" | cut -d' ' -f1)
printf " %-25s %s\n" "$name" "$md5"
fi
done
echo ""
echo "=== Practical: Detect Duplicates ==="
find_duplicates() {
local dir=$1
declare -A checksums
declare -a duplicates
for file in "$dir"/*; do
[[ -f "$file" && ! -L "$file" ]] || continue
local md5=$(md5sum "$file" | cut -d' ' -f1)
if [[ -n "${checksums[$md5]}" ]]; then
echo "Duplicate: $(basename "$file") = $(basename "${checksums[$md5]}")"
else
checksums[$md5]="$file"
fi
done
}
find_duplicates /tmp/compare_test
echo ""
echo "=== Hard Link Detection ==="
find_hardlinks() {
local file=$1
local dir=$2
local inode=$(stat -c '%i' "$file")
echo "Hard links to $(basename "$file") (inode $inode):"
find "$dir" -inum "$inode" -printf " %p\n"
}
find_hardlinks /tmp/compare_test/file_a.txt /tmp/compare_test
echo ""
echo "=== Symlink Resolution ==="
link="/tmp/compare_test/file_a_symlink.txt"
echo "Symlink: $link"
echo " Points to: $(readlink "$link")"
echo " Resolved: $(readlink -f "$link")"
# Check if symlink target exists
if [[ -e "$link" ]]; then
echo " Target exists"
else
echo " Broken link"
fi
# Cleanup
rm -rf /tmp/compare_test
Testing file descriptors and terminal connections for interactive scripts.
#!/bin/bash
# Terminal and file descriptor tests
echo "=== Terminal Tests (-t) ==="
# Test if file descriptor is a terminal
if [[ -t 0 ]]; then
echo "stdin (fd 0) is a terminal"
else
echo "stdin is NOT a terminal (piped or redirected)"
fi
if [[ -t 1 ]]; then
echo "stdout (fd 1) is a terminal"
else
echo "stdout is NOT a terminal (piped or redirected)"
fi
if [[ -t 2 ]]; then
echo "stderr (fd 2) is a terminal"
else
echo "stderr is NOT a terminal"
fi
echo ""
echo "=== Practical: Interactive Detection ==="
is_interactive() {
# Check if both stdin and stdout are terminals
[[ -t 0 && -t 1 ]]
}
if is_interactive; then
echo "Running interactively"
else
echo "Running non-interactively (script/pipe)"
fi
echo ""
echo "=== Practical: Colorized Output ==="
# Only use colors if outputting to terminal
if [[ -t 1 ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo -e "${GREEN}Success message${NC}"
echo -e "${RED}Error message${NC}"
else
# No colors for pipes/files
echo "Success message"
echo "Error message"
fi
echo ""
echo "=== Practical: Progress Display ==="
show_progress() {
local current=$1
local total=$2
if [[ -t 1 ]]; then
# Terminal: update in place
printf "\rProgress: %d/%d (%d%%)" "$current" "$total" "$((current * 100 / total))"
else
# Not terminal: new line each time (or suppress)
echo "Progress: $current/$total"
fi
}
# Demo (would normally be in a loop)
echo "Progress display:"
show_progress 50 100
echo ""
echo ""
echo "=== Practical: Prompt Only If Interactive ==="
get_confirmation() {
local message=$1
if [[ -t 0 ]]; then
# Can read from user
read -p "$message (y/n) " response
[[ "$response" =~ ^[Yy] ]]
else
# Non-interactive: assume yes or default
echo "$message - assuming yes (non-interactive)"
return 0
fi
}
# get_confirmation "Continue?" # Would wait for input
echo ""
echo "=== Check Specific File Descriptors ==="
# Test if fd 3 is open
if [[ -e /dev/fd/3 ]]; then
echo "File descriptor 3 is open"
else
echo "File descriptor 3 is not open"
fi
# Open fd 3 and test
exec 3>/tmp/fd3_test.txt
if [[ -e /dev/fd/3 ]]; then
echo "File descriptor 3 is now open"
fi
exec 3>&- # Close fd 3
rm -f /tmp/fd3_test.txt
echo ""
echo "=== Practical: Logging with Redirection ==="
log_message() {
local level=$1
local message=$2
local timestamp=$(date '+%Y-%m-%d %H:%M:%S')
# Always log to stderr (or log file)
echo "[$timestamp] [$level] $message" >&2
# Also show on terminal if interactive
if [[ -t 1 ]]; then
case "$level" in
ERROR) echo -e "\033[31m$message\033[0m" ;;
WARNING) echo -e "\033[33m$message\033[0m" ;;
INFO) echo "$message" ;;
esac
fi
}
log_message "INFO" "Application started"
log_message "WARNING" "Configuration not found, using defaults"
[[ -t 1 ]] to detect if output is going to a terminal, so you can enable colors, progress bars, and interactive features only when appropriate.
Testing directories, checking contents, and traversing paths.
#!/bin/bash
# Directory operations
# Create test structure
mkdir -p /tmp/dir_test/subdir1/nested
mkdir -p /tmp/dir_test/subdir2
mkdir -p /tmp/dir_test/empty_dir
touch /tmp/dir_test/file.txt
touch /tmp/dir_test/subdir1/file1.txt
touch /tmp/dir_test/subdir1/nested/file2.txt
echo "=== Directory Tests ==="
dir="/tmp/dir_test"
if [[ -d "$dir" ]]; then
echo "$dir is a directory"
fi
if [[ -d "$dir/file.txt" ]]; then
echo "file.txt is a directory"
else
echo "file.txt is not a directory"
fi
echo ""
echo "=== Check If Directory Is Empty ==="
is_empty_dir() {
local dir=$1
if [[ ! -d "$dir" ]]; then
echo "$dir is not a directory"
return 2
fi
if [[ -z "$(ls -A "$dir")" ]]; then
echo "$dir is empty"
return 0
else
echo "$dir is not empty"
return 1
fi
}
is_empty_dir /tmp/dir_test/empty_dir
is_empty_dir /tmp/dir_test/subdir1
echo ""
echo "=== Count Directory Contents ==="
count_contents() {
local dir=$1
local files=$(find "$dir" -maxdepth 1 -type f | wc -l)
local dirs=$(find "$dir" -maxdepth 1 -type d | wc -l)
((dirs--)) # Subtract the directory itself
echo "$dir contains: $files files, $dirs subdirectories"
}
count_contents /tmp/dir_test
count_contents /tmp/dir_test/subdir1
echo ""
echo "=== Directory Permissions ==="
# Check if can enter directory
dir="/tmp/dir_test"
if [[ -x "$dir" ]]; then
echo "Can enter $dir (execute permission)"
fi
# Check if can list directory
if [[ -r "$dir" ]]; then
echo "Can list $dir (read permission)"
fi
# Check if can create files in directory
if [[ -w "$dir" ]]; then
echo "Can create files in $dir (write permission)"
fi
echo ""
echo "=== Safe Directory Creation ==="
safe_mkdir() {
local dir=$1
if [[ -e "$dir" ]]; then
if [[ -d "$dir" ]]; then
echo "Directory already exists: $dir"
return 0
else
echo "ERROR: Path exists but is not a directory: $dir"
return 1
fi
fi
local parent=$(dirname "$dir")
if [[ ! -d "$parent" ]]; then
echo "ERROR: Parent directory doesn't exist: $parent"
return 1
fi
if [[ ! -w "$parent" ]]; then
echo "ERROR: Cannot write to parent: $parent"
return 1
fi
mkdir "$dir"
echo "Created directory: $dir"
return 0
}
safe_mkdir /tmp/dir_test/newdir
safe_mkdir /tmp/dir_test/file.txt # Should fail
echo ""
echo "=== Traverse Directory Tree ==="
traverse() {
local dir=$1
local indent=${2:-0}
local prefix=""
for ((i=0; i<indent; i++)); do
prefix+=" "
done
for item in "$dir"/*; do
[[ -e "$item" ]] || continue
local name=$(basename "$item")
if [[ -d "$item" ]]; then
echo "${prefix}π $name/"
traverse "$item" $((indent + 1))
else
echo "${prefix}π $name"
fi
done
}
echo "Directory tree:"
traverse /tmp/dir_test
echo ""
echo "=== Practical: Ensure Directory Structure ==="
ensure_structure() {
local base=$1
shift
for dir in "$@"; do
local path="$base/$dir"
if [[ ! -d "$path" ]]; then
mkdir -p "$path"
echo "Created: $path"
fi
done
}
ensure_structure /tmp/dir_test "logs" "cache" "config"
# Cleanup
rm -rf /tmp/dir_test
Complex file testing with AND, OR, NOT, and nested conditions.
#!/bin/bash
# Combining tests with logic
# Create test files
mkdir -p /tmp/logic_test
echo "content" > /tmp/logic_test/readable.txt
chmod 644 /tmp/logic_test/readable.txt
touch /tmp/logic_test/empty.txt
chmod 444 /tmp/logic_test/empty.txt
touch /tmp/logic_test/executable.sh
chmod 755 /tmp/logic_test/executable.sh
echo "=== Combining Tests with && and || ==="
file="/tmp/logic_test/readable.txt"
# AND: both must be true
if [[ -f "$file" && -r "$file" ]]; then
echo "$file is a readable file"
fi
# OR: at least one true
if [[ -f "$file" || -d "$file" ]]; then
echo "$file exists (file or directory)"
fi
# NOT
if [[ ! -d "$file" ]]; then
echo "$file is not a directory"
fi
echo ""
echo "=== Complex Conditions ==="
check_file() {
local f=$1
local name=$(basename "$f")
# File must exist, be readable, not empty, and not a directory
if [[ -e "$f" && -r "$f" && -s "$f" && ! -d "$f" ]]; then
echo "$name: valid data file"
else
echo -n "$name: invalid ("
[[ ! -e "$f" ]] && echo -n "missing, "
[[ -e "$f" && ! -r "$f" ]] && echo -n "not readable, "
[[ -e "$f" && ! -s "$f" ]] && echo -n "empty, "
[[ -d "$f" ]] && echo -n "is directory, "
echo ")"
fi
}
check_file /tmp/logic_test/readable.txt
check_file /tmp/logic_test/empty.txt
check_file /tmp/logic_test/nonexistent.txt
echo ""
echo "=== Nested Conditions ==="
analyze_file() {
local f=$1
if [[ ! -e "$f" ]]; then
echo "File does not exist: $f"
return 1
fi
if [[ -d "$f" ]]; then
echo "Is a directory"
if [[ -r "$f" && -x "$f" ]]; then
echo " Can list contents"
fi
elif [[ -f "$f" ]]; then
echo "Is a regular file"
if [[ -s "$f" ]]; then
echo " Has content"
else
echo " Is empty"
fi
if [[ -r "$f" ]]; then
if [[ -w "$f" ]]; then
echo " Readable and writable"
else
echo " Readable only"
fi
else
echo " Not readable"
fi
if [[ -x "$f" ]]; then
echo " Is executable"
fi
elif [[ -L "$f" ]]; then
echo "Is a symbolic link"
else
echo "Is a special file"
fi
}
echo "Analyzing files:"
echo ""
echo "/tmp/logic_test/readable.txt:"
analyze_file /tmp/logic_test/readable.txt
echo ""
echo "/tmp/logic_test/executable.sh:"
analyze_file /tmp/logic_test/executable.sh
echo ""
echo "=== Practical: Validate Input File ==="
validate_input() {
local file=$1
local required_perms=$2 # r, w, rw, rx, etc.
# Must exist
if [[ ! -e "$file" ]]; then
echo "ERROR: File not found: $file"
return 1
fi
# Must be regular file
if [[ ! -f "$file" ]]; then
echo "ERROR: Not a regular file: $file"
return 1
fi
# Check required permissions
local missing=""
if [[ "$required_perms" == *r* && ! -r "$file" ]]; then
missing+="read "
fi
if [[ "$required_perms" == *w* && ! -w "$file" ]]; then
missing+="write "
fi
if [[ "$required_perms" == *x* && ! -x "$file" ]]; then
missing+="execute "
fi
if [[ -n "$missing" ]]; then
echo "ERROR: Missing permissions ($missing): $file"
return 1
fi
# Must have content for input files
if [[ "$required_perms" == *r* && ! -s "$file" ]]; then
echo "WARNING: File is empty: $file"
fi
echo "OK: $file (permissions: $required_perms)"
return 0
}
validate_input /tmp/logic_test/readable.txt "r"
validate_input /tmp/logic_test/empty.txt "rw"
validate_input /tmp/logic_test/executable.sh "rx"
# Cleanup
rm -rf /tmp/logic_test
A reusable library of file testing utility functions.
#!/bin/bash
# Complete file testing library
#######################################
# Existence and Type
#######################################
file_exists() {
[[ -e "$1" ]]
}
is_file() {
[[ -f "$1" ]]
}
is_dir() {
[[ -d "$1" ]]
}
is_link() {
[[ -L "$1" ]]
}
is_empty_file() {
[[ -f "$1" && ! -s "$1" ]]
}
is_empty_dir() {
[[ -d "$1" ]] && [[ -z "$(ls -A "$1")" ]]
}
#######################################
# Permissions
#######################################
is_readable() {
[[ -r "$1" ]]
}
is_writable() {
[[ -w "$1" ]]
}
is_executable() {
[[ -x "$1" ]]
}
is_owned() {
[[ -O "$1" ]]
}
#######################################
# Comparisons
#######################################
is_newer() {
[[ "$1" -nt "$2" ]]
}
is_older() {
[[ "$1" -ot "$2" ]]
}
is_same_file() {
[[ "$1" -ef "$2" ]]
}
#######################################
# Attributes
#######################################
get_size() {
stat -c '%s' "$1" 2>/dev/null
}
get_perms() {
stat -c '%a' "$1" 2>/dev/null
}
get_owner() {
stat -c '%U' "$1" 2>/dev/null
}
get_group() {
stat -c '%G' "$1" 2>/dev/null
}
get_mtime() {
stat -c '%Y' "$1" 2>/dev/null
}
get_type() {
stat -c '%F' "$1" 2>/dev/null
}
#######################################
# Utilities
#######################################
# Human-readable size
human_size() {
local bytes=$(get_size "$1")
[[ -z "$bytes" ]] && return 1
if ((bytes >= 1073741824)); then
echo "$(echo "scale=1; $bytes/1073741824" | bc)G"
elif ((bytes >= 1048576)); then
echo "$(echo "scale=1; $bytes/1048576" | bc)M"
elif ((bytes >= 1024)); then
echo "$(echo "scale=1; $bytes/1024" | bc)K"
else
echo "${bytes}B"
fi
}
# File age in seconds
file_age() {
local mtime=$(get_mtime "$1")
[[ -z "$mtime" ]] && return 1
echo $(($(date +%s) - mtime))
}
# Describe file
describe_file() {
local f=$1
if [[ ! -e "$f" ]]; then
echo "Not found"
return 1
fi
local type=$(get_type "$f")
local size=$(human_size "$f")
local perms=$(get_perms "$f")
local owner=$(get_owner "$f")
echo "$type, $size, mode $perms, owner $owner"
}
# Validate file for operation
validate_file() {
local f=$1
local mode=$2 # r=read, w=write, x=exec, d=dir, f=file
[[ ! -e "$f" ]] && { echo "not found"; return 1; }
[[ "$mode" == *f* && ! -f "$f" ]] && { echo "not a file"; return 1; }
[[ "$mode" == *d* && ! -d "$f" ]] && { echo "not a directory"; return 1; }
[[ "$mode" == *r* && ! -r "$f" ]] && { echo "not readable"; return 1; }
[[ "$mode" == *w* && ! -w "$f" ]] && { echo "not writable"; return 1; }
[[ "$mode" == *x* && ! -x "$f" ]] && { echo "not executable"; return 1; }
echo "ok"
return 0
}
#######################################
# Demo
#######################################
echo "========================================"
echo " File Testing Library Demo"
echo "========================================"
# Create test files
mkdir -p /tmp/lib_test
echo "Hello World" > /tmp/lib_test/sample.txt
chmod 644 /tmp/lib_test/sample.txt
touch /tmp/lib_test/empty.txt
mkdir /tmp/lib_test/subdir
echo ""
echo "=== Type Tests ==="
echo "sample.txt:"
is_file /tmp/lib_test/sample.txt && echo " is_file: yes"
is_dir /tmp/lib_test/sample.txt || echo " is_dir: no"
echo "subdir:"
is_dir /tmp/lib_test/subdir && echo " is_dir: yes"
echo ""
echo "=== Empty Tests ==="
is_empty_file /tmp/lib_test/empty.txt && echo "empty.txt is empty"
is_empty_file /tmp/lib_test/sample.txt || echo "sample.txt is not empty"
is_empty_dir /tmp/lib_test/subdir && echo "subdir is empty"
echo ""
echo "=== Permissions ==="
echo "sample.txt permissions:"
is_readable /tmp/lib_test/sample.txt && echo " readable"
is_writable /tmp/lib_test/sample.txt && echo " writable"
is_executable /tmp/lib_test/sample.txt || echo " not executable"
echo ""
echo "=== Attributes ==="
echo "sample.txt:"
echo " Size: $(get_size /tmp/lib_test/sample.txt) bytes"
echo " Human: $(human_size /tmp/lib_test/sample.txt)"
echo " Perms: $(get_perms /tmp/lib_test/sample.txt)"
echo " Owner: $(get_owner /tmp/lib_test/sample.txt)"
echo " Age: $(file_age /tmp/lib_test/sample.txt) seconds"
echo ""
echo "=== Describe ==="
echo "sample.txt: $(describe_file /tmp/lib_test/sample.txt)"
echo "subdir: $(describe_file /tmp/lib_test/subdir)"
echo ""
echo "=== Validate ==="
echo "sample.txt (rf): $(validate_file /tmp/lib_test/sample.txt rf)"
echo "sample.txt (rx): $(validate_file /tmp/lib_test/sample.txt rx)"
echo "subdir (dr): $(validate_file /tmp/lib_test/subdir dr)"
# Cleanup
rm -rf /tmp/lib_test
echo ""
echo "========================================"
# Existence
[[ -e file ]] # Exists (any type)
[[ -f file ]] # Regular file
[[ -d file ]] # Directory
[[ -L file ]] # Symbolic link
# Permissions
[[ -r file ]] # Readable
[[ -w file ]] # Writable
[[ -x file ]] # Executable
# Size
[[ -s file ]] # Size > 0
# Comparison
[[ f1 -nt f2 ]] # f1 newer than f2
[[ f1 -ot f2 ]] # f1 older than f2
[[ f1 -ef f2 ]] # Same file (hard link)
# Terminal
[[ -t 0 ]] # stdin is terminal
[[ -t 1 ]] # stdout is terminal
stat -c '%s' file # Size in bytes
stat -c '%a' file # Octal permissions (644)
stat -c '%A' file # Human permissions (-rw-r--r--)
stat -c '%U:%G' file # Owner:Group
stat -c '%Y' file # Mtime as epoch
stat -c '%y' file # Mtime human readable
| Task | Code |
|---|---|
| Check before read | [[ -f "$f" && -r "$f" ]] |
| Check before write | [[ -f "$f" && -w "$f" ]] |
| Safe create | [[ ! -e "$f" ]] && touch "$f" |
| Get file size | stat -c '%s' "$f" |
| Check age | $(($(date +%s) - $(stat -c '%Y' "$f"))) |
| Empty dir check | [[ -z "$(ls -A "$dir")" ]] |
[[ ]] instead of [ ] for file tests[[ -f "$file" ]]-L-f for existence (use -e)-f follows symlinks, -L doesn't-w means file exists