Overview
FileTest provides class methods for testing various properties of files and directories in Ruby. The module contains methods that check file existence, type, permissions, and other filesystem attributes without requiring file handles or raising exceptions for missing files.
Ruby implements FileTest as a module mixed into the File class, making its methods available both as FileTest.method_name
and File.method_name
. All FileTest methods accept string paths as arguments and return boolean values, with a few exceptions that return numeric values or nil.
The module handles cross-platform filesystem differences transparently, abstracting platform-specific file attribute checking into consistent Ruby methods. FileTest methods use the underlying operating system's file stat system calls to gather file information efficiently.
# Basic file existence check
FileTest.exist?('/etc/passwd')
# => true
# Check if path is a regular file
FileTest.file?('/home/user/document.txt')
# => true
# Test directory status
FileTest.directory?('/usr/local')
# => true
FileTest methods never modify the filesystem - they perform read-only inspections of file metadata. This makes them safe for use in conditional logic and validation scenarios where file state verification is required without side effects.
Basic Usage
The most frequently used FileTest methods check file existence and type. The exist?
method returns true if a path exists regardless of type, while file?
and directory?
verify specific path types.
path = '/home/user/config.yml'
if FileTest.exist?(path)
puts "Path exists"
if FileTest.file?(path)
puts "It's a regular file"
elsif FileTest.directory?(path)
puts "It's a directory"
end
end
Permission testing methods check read, write, and execute access for the current process. These methods respect the effective user and group IDs of the running process, making them accurate for access control decisions.
config_file = '/etc/application.conf'
# Check permissions before attempting operations
if FileTest.readable?(config_file)
content = File.read(config_file)
end
if FileTest.writable?('/var/log/application.log')
File.open('/var/log/application.log', 'a') do |log|
log.puts "Application started"
end
end
# Verify executable status for scripts
script_path = '/usr/local/bin/deploy.sh'
if FileTest.executable?(script_path)
system(script_path)
else
puts "Script lacks execute permissions"
end
Size-related methods provide file dimensions and emptiness checks. The size
method returns the file size in bytes, while zero?
tests for empty files. The size?
method returns the size for non-empty files or nil for empty files.
log_file = '/var/log/application.log'
# Get exact file size
file_size = FileTest.size(log_file)
puts "Log file is #{file_size} bytes"
# Check for empty files
if FileTest.zero?(log_file)
puts "Log file is empty"
end
# size? returns nil for empty files, size for non-empty
actual_size = FileTest.size?(log_file)
if actual_size
puts "File contains #{actual_size} bytes of data"
else
puts "File is empty"
end
Symbolic link detection requires the symlink?
method, which returns true only for symbolic links themselves, not their targets. This distinction becomes critical when processing directory trees that may contain links.
# Create test symbolic link
File.symlink('/etc/hosts', '/tmp/hosts_link')
puts FileTest.symlink?('/tmp/hosts_link') # => true
puts FileTest.symlink?('/etc/hosts') # => false
puts FileTest.file?('/tmp/hosts_link') # => true (follows link)
Error Handling & Debugging
FileTest methods handle missing files and permission issues gracefully by returning false instead of raising exceptions. This behavior differs from File class methods that typically raise Errno
exceptions for filesystem errors.
# FileTest never raises for missing files
puts FileTest.exist?('/nonexistent/path') # => false
puts FileTest.file?('/missing/file.txt') # => false
puts FileTest.readable?('/forbidden/file') # => false
# Compare with File methods that raise exceptions
begin
File.size('/nonexistent/file')
rescue Errno::ENOENT => e
puts "File.size raised: #{e.message}"
end
Race conditions occur when files change between FileTest checks and subsequent operations. The time gap between testing and using a file creates windows where filesystem state can change.
# Problematic pattern - race condition possible
if FileTest.exist?(path) && FileTest.readable?(path)
content = File.read(path) # File might be deleted here
end
# Better approach - handle exceptions during actual operations
begin
content = File.read(path)
rescue Errno::ENOENT
puts "File not found when attempting to read"
rescue Errno::EACCES
puts "Permission denied when reading file"
end
Permission checks reflect the effective user ID of the current process, not the actual file permissions. This means FileTest results may differ from ls -l
output when running as different users or with setuid programs.
# Running as non-root user
file = '/root/.bashrc'
# File exists but not readable by current user
puts FileTest.exist?(file) # => true
puts FileTest.readable?(file) # => false
# Verify with actual file stat information
stat = File.stat(file)
printf "Permissions: %o\n", stat.mode & 0777
puts "Owner: #{stat.uid}, Group: #{stat.gid}"
puts "Current process: #{Process.uid}"
Network filesystems and special file types can produce unexpected results with FileTest methods. Remote filesystems may have different permission models or caching behaviors that affect test results.
# Special files may not behave as expected
device_files = ['/dev/null', '/dev/random', '/proc/cpuinfo']
device_files.each do |device|
puts "#{device}:"
puts " exists: #{FileTest.exist?(device)}"
puts " file: #{FileTest.file?(device)}"
puts " readable: #{FileTest.readable?(device)}"
puts " size: #{FileTest.size(device) rescue 'unknown'}"
end
Debugging FileTest issues requires understanding the underlying filesystem behavior. The File.stat
method provides detailed information that can explain FileTest results when behavior seems unexpected.
def debug_file_status(path)
begin
stat = File.stat(path)
puts "Path: #{path}"
puts " Type: #{stat.ftype}"
puts " Mode: #{sprintf('%o', stat.mode)}"
puts " Size: #{stat.size}"
puts " Owner: #{stat.uid}:#{stat.gid}"
puts " Times: #{stat.mtime} (modified)"
puts "FileTest results:"
puts " exist?: #{FileTest.exist?(path)}"
puts " file?: #{FileTest.file?(path)}"
puts " directory?: #{FileTest.directory?(path)}"
puts " readable?: #{FileTest.readable?(path)}"
puts " writable?: #{FileTest.writable?(path)}"
rescue SystemCallError => e
puts "Error accessing #{path}: #{e.message}"
end
end
debug_file_status('/etc/passwd')
debug_file_status('/tmp/nonexistent')
Common Pitfalls
Symbolic link handling creates confusion because FileTest methods follow links by default. The symlink?
method only returns true for the link itself, while other methods test the link target.
# Create symbolic link to demonstrate behavior
File.symlink('/etc/passwd', '/tmp/passwd_link')
# These test the link target (/etc/passwd)
puts FileTest.file?('/tmp/passwd_link') # => true
puts FileTest.readable?('/tmp/passwd_link') # => true
puts FileTest.size('/tmp/passwd_link') # => size of /etc/passwd
# Only this tests the link itself
puts FileTest.symlink?('/tmp/passwd_link') # => true
# Broken symbolic links cause unexpected behavior
File.symlink('/nonexistent/target', '/tmp/broken_link')
puts FileTest.exist?('/tmp/broken_link') # => false (target doesn't exist)
puts FileTest.symlink?('/tmp/broken_link') # => true (link exists)
The size
method behaves differently from size?
for empty files and non-existent files. This inconsistency catches developers who assume similar method names have similar error handling.
# Create empty file for testing
File.write('/tmp/empty_file', '')
# size returns 0 for empty files, raises for missing files
puts FileTest.size('/tmp/empty_file') # => 0
begin
FileTest.size('/tmp/missing_file')
rescue Errno::ENOENT => e
puts "size raised: #{e.message}"
end
# size? returns 0 for empty files, nil for missing files
puts FileTest.size?('/tmp/empty_file') # => nil
puts FileTest.size?('/tmp/missing_file') # => nil
Permission testing becomes unreliable in environments with complex access control systems like SELinux or ACLs. FileTest methods only check traditional Unix permissions, ignoring extended attributes.
# Traditional permission check might pass
puts FileTest.writable?('/var/log/secure') # => false (correctly)
# But extended attributes might block access even if permissions allow
# SELinux contexts, ACLs, and file attributes affect actual access
system('lsattr /var/log/secure') # Check extended attributes
system('getfacl /var/log/secure') # Check ACLs if available
Time-of-check-to-time-of-use (TOCTOU) vulnerabilities emerge when FileTest results become stale between checking and acting. Concurrent processes or signal handlers can modify files after FileTest returns.
# Vulnerable pattern - file might change between check and use
if FileTest.writable?(config_file)
# Another process could modify permissions here
File.open(config_file, 'w') do |f| # Might fail with EACCES
f.write(new_config)
end
end
# Safer approach - attempt operation and handle failures
begin
File.open(config_file, 'w') do |f|
f.write(new_config)
end
rescue Errno::EACCES
puts "Cannot write to #{config_file}: permission denied"
rescue Errno::ENOENT
puts "Configuration file #{config_file} not found"
end
Directory traversal vulnerabilities can occur when using FileTest with user-provided paths. Attackers might use "../" sequences or symbolic links to access files outside intended directories.
# Dangerous - user input used directly
user_filename = params[:filename] # Could be "../../../etc/passwd"
full_path = File.join('/safe/directory', user_filename)
if FileTest.readable?(full_path)
content = File.read(full_path) # Might read sensitive files
end
# Safer approach - validate and constrain paths
def safe_file_access(base_dir, filename)
# Reject paths with directory traversal attempts
return nil if filename.include?('..')
full_path = File.join(base_dir, filename)
real_path = File.realpath(full_path) rescue nil
return nil unless real_path
# Ensure resolved path stays within base directory
base_real = File.realpath(base_dir)
return nil unless real_path.start_with?(base_real)
FileTest.readable?(real_path) ? real_path : nil
end
Case sensitivity handling varies between filesystems, causing portable code to behave differently on different platforms. macOS uses case-insensitive filesystems by default, while Linux uses case-sensitive filesystems.
# Create file for testing
File.write('/tmp/TestFile.txt', 'content')
# Results vary by filesystem case sensitivity
puts FileTest.exist?('/tmp/TestFile.txt') # => true (always)
puts FileTest.exist?('/tmp/testfile.txt') # => depends on filesystem
puts FileTest.exist?('/tmp/TESTFILE.TXT') # => depends on filesystem
# Portable filename comparison requires normalization
def portable_file_exists?(path)
# On case-insensitive systems, check exact case match
dir = File.dirname(path)
basename = File.basename(path)
return false unless FileTest.exist?(path)
# Verify exact case on case-insensitive filesystems
actual_files = Dir.entries(dir) rescue []
actual_files.include?(basename)
end
Reference
File Existence and Type Methods
Method | Parameters | Returns | Description |
---|---|---|---|
exist?(path) |
path (String) | Boolean |
True if path exists (file, directory, or special file) |
exists?(path) |
path (String) | Boolean |
Deprecated alias for exist? |
file?(path) |
path (String) | Boolean |
True if path is a regular file |
directory?(path) |
path (String) | Boolean |
True if path is a directory |
symlink?(path) |
path (String) | Boolean |
True if path is a symbolic link |
pipe?(path) |
path (String) | Boolean |
True if path is a named pipe (FIFO) |
socket?(path) |
path (String) | Boolean |
True if path is a socket |
blockdev?(path) |
path (String) | Boolean |
True if path is a block device |
chardev?(path) |
path (String) | Boolean |
True if path is a character device |
Permission Test Methods
Method | Parameters | Returns | Description |
---|---|---|---|
readable?(path) |
path (String) | Boolean |
True if file is readable by effective user |
readable_real?(path) |
path (String) | Boolean |
True if file is readable by real user |
writable?(path) |
path (String) | Boolean |
True if file is writable by effective user |
writable_real?(path) |
path (String) | Boolean |
True if file is writable by real user |
executable?(path) |
path (String) | Boolean |
True if file is executable by effective user |
executable_real?(path) |
path (String) | Boolean |
True if file is executable by real user |
Size and Content Methods
Method | Parameters | Returns | Description |
---|---|---|---|
size(path) |
path (String) | Integer |
File size in bytes, raises if file missing |
size?(path) |
path (String) | Integer or nil |
File size in bytes, nil if missing or empty |
zero?(path) |
path (String) | Boolean |
True if file exists and is empty |
Special Attribute Methods
Method | Parameters | Returns | Description |
---|---|---|---|
setuid?(path) |
path (String) | Boolean |
True if file has setuid bit set |
setgid?(path) |
path (String) | Boolean |
True if file has setgid bit set |
sticky?(path) |
path (String) | Boolean |
True if file has sticky bit set |
owned?(path) |
path (String) | Boolean |
True if file is owned by effective user |
grpowned?(path) |
path (String) | Boolean |
True if file is owned by effective group |
File Type Constants
Constant | Value | Description |
---|---|---|
S_IFMT |
0170000 | File type mask |
S_IFREG |
0100000 | Regular file |
S_IFDIR |
0040000 | Directory |
S_IFLNK |
0120000 | Symbolic link |
S_IFBLK |
0060000 | Block device |
S_IFCHR |
0020000 | Character device |
S_IFIFO |
0010000 | Named pipe |
S_IFSOCK |
0140000 | Socket |
Permission Constants
Constant | Octal | Description |
---|---|---|
S_ISUID |
04000 | Set user ID on execution |
S_ISGID |
02000 | Set group ID on execution |
S_ISVTX |
01000 | Sticky bit |
S_IRUSR |
00400 | Owner read permission |
S_IWUSR |
00200 | Owner write permission |
S_IXUSR |
00100 | Owner execute permission |
S_IRGRP |
00040 | Group read permission |
S_IWGRP |
00020 | Group write permission |
S_IXGRP |
00010 | Group execute permission |
S_IROTH |
00004 | Other read permission |
S_IWOTH |
00002 | Other write permission |
S_IXOTH |
00001 | Other execute permission |
Method Availability and Aliases
FileTest methods are available in three ways:
FileTest.method_name(path)
- Direct module method callFile.method_name(path)
- Through File class inclusiontest(?char, path)
- Single character test operators (deprecated)
Error Handling Behavior
All FileTest methods return false for missing files instead of raising exceptions. This differs from File class methods:
# FileTest methods return false for missing files
FileTest.exist?('/missing') # => false
FileTest.readable?('/missing') # => false
FileTest.size?('/missing') # => nil
# File methods raise exceptions
File.size('/missing') # raises Errno::ENOENT
File.read('/missing') # raises Errno::ENOENT
Platform Differences
Feature | Unix/Linux | Windows | macOS |
---|---|---|---|
Case sensitivity | Sensitive | Insensitive | Configurable |
Symbolic links | Full support | Limited support | Full support |
Execute permissions | Proper bits | File extensions | Proper bits |
Special files | All types | Limited types | Most types |
Setuid/setgid | Supported | Not applicable | Supported |