rubyguides

Kernel#test

test(mode, file1 [, file2]) -> boolean or integer

The test method checks file characteristics using single-character operators. It’s Ruby’s direct access to file test operations similar to Perl’s -e, -f, etc.

File test operators

Basic checks

# -e: File exists
test(?e, "file.txt")   # => true/false
File.exist?("file.txt") # Equivalent

# -f: Regular file
test(?f, "file.txt")

# -d: Directory
test(?d, "/path")

# -l: Symbolic link
test(?l, "link")

The single-character operators map directly to their shell equivalents. The ?e syntax produces a one-character string "e", which test interprets as “check existence.” Each operator answers a specific yes/no question about the file at the given path: does it exist, is it a regular file, is it a directory, is it a symbolic link. These are the four most commonly used operators and cover the majority of everyday file checks.

Permission tests

# -r: Readable
test(?r, "file.txt")

# -w: Writable  
test(?w, "file.txt")

# -x: Executable
test(?x, "script.rb")

Permission operators check what the current process is allowed to do with the file, not what the file’s permission bits say in isolation. The result depends on the user and group the Ruby process runs under, so test(?w, "/etc/passwd") may return false even if the file technically has write bits set, because the process typically runs as a non-root user without write access to system files.

Type tests

# -b: Block device
# -c: Character device
# -p: Named pipe
# -S: Socket
test(?b, "/dev/sda")
test(?c, "/dev/tty")
test(?p, "/tmp/fifo")
test(?S, "/tmp/socket")

Type operators distinguish between different kinds of filesystem entries. A block device (-b) is a storage device like a hard drive partition; a character device (-c) is a terminal or serial port; a named pipe (-p) is a FIFO for inter-process communication; and a socket (-S) is a Unix domain socket. These operators are less common in everyday Ruby code but appear in system scripts that need to enumerate or verify device nodes.

Binary operators (two arguments)

Compare files

# =: Same file (inode)
test(?=, "file1.txt", "file2.txt")

# -N: Newer than
test(?N, "new.txt", "old.txt")

# -O: Owned by current process
test(?O, "file.txt")

# -G: Group matches current process
test(?G, "file.txt")

Binary operators take two file paths and compare them. The = operator checks whether both paths refer to the same inode (the same physical file, even if accessed via different names or symlinks). The -N operator compares modification timestamps and returns true if the first file was modified more recently than the second. Ownership operators -O and -G check whether the current process’s effective user or group matches the file’s owner or group.

Practical examples

File existence check

def ensure_file_exists(path)
  unless test(?e, path)
    File.write(path, "")
    puts "Created: #{path}"
  end
end

The guard clause checks whether the file already exists before attempting to create it. If the file is absent, File.write(path, "") creates an empty file and prints a confirmation. This pattern avoids overwriting existing content and gives the caller feedback about what happened. For production code, consider also checking test(?w, File.dirname(path)) to ensure the parent directory is writable before attempting to create the file.

Permission checking

def run_script?(path)
  test(?e, path) && test(?x, path)
end

# Usage
puts run_script?("deploy.sh")  # true if exists and executable

The short-circuit && ensures that the executable check only runs if the file exists. If test(?e, path) returns false, Ruby skips the second test(?x, path) call entirely, avoiding an unnecessary system call. This combined check is a concise way to validate that a script is both present and runnable before attempting to execute it.

Safe file operations

def read_if_exists(path)
  return nil unless test(?f, path) && test(?r, path)
  File.read(path)
end

The guard clause returns nil early if the file is not a regular file or is not readable. The return nil unless pattern keeps the happy path at the method’s base indentation level, making it clear what the method does when everything is in order. This is a defensive approach that prevents Errno::ENOENT or permission errors from propagating to the caller.

Directory creation

def ensure_directory(path)
  return true if test(?d, path)
  Dir.mkdir(path)
end

The method checks whether a directory already exists before creating it. If the directory is present, it returns true immediately. Otherwise, Dir.mkdir creates the directory with default permissions. This is an idempotent operation — running it multiple times on the same path will not raise an error or change anything after the first successful creation.

Block device detection

# Check for devices
def device?(path)
  test(?b, path) || test(?c, path)
end

device?("/dev/null")    # => true
device?("/dev/zero")    # => true
device?("/dev/sda")    # => true (block)

The || operator lets you check for multiple device types in a single expression. /dev/null and /dev/zero are character devices, while /dev/sda is a block device representing a physical disk. This method returns true for either type, which is useful when you need to identify any device node regardless of its specific category.

String shortcut form

# Can also use character directly
?e  # => "e"
test(?e, "file")  # Same as test("e", "file")

The ?e syntax is Ruby’s character literal notation — it produces a single-character string "e". You can also pass the operator as a plain string: test("e", "file"). The ?x form is more compact and reads like the shell test flags it emulates (e.g., test -e file in bash), which makes it the preferred style in scripts that bridge Ruby and shell conventions.

Ruby 3.0+ modern alternative

# Modern methods preferred
File.exist?(path)
File.readable?(path)
File.executable?(path)
File.directory?(path)

While test is powerful and concise, modern Ruby code typically uses the more readable File. methods. The test method remains useful for scripting and when you need compact file tests.

choosing test for quick file checks

test still earns its place when the code needs a tiny file probe and the operator itself says everything the reader needs to know. That can be useful in scripts, shell-style helpers, and one-off maintenance tasks where a compact check is easier to scan than a longer chain of helper calls. The tradeoff is readability, so it helps to keep the surrounding code simple and use the modern File methods when the intent needs to be obvious at a glance.

It also helps to think about what the result means before you choose the operator. A file check is often only the first step in a larger decision, such as whether to create a file, skip a task, or move on to a different path. When that is the case, the code is easier to follow if the file test sits right next to the branch it controls. That keeps the condition and the action tied together, which makes the script feel more direct and easier to maintain later.

The method is most readable when the file type, the permission, and the action are all close together. That way the reader can see the question and the consequence without jumping around the file. If the check starts to drive several branches, it may be worth extracting a helper that names the decision more clearly. The core idea stays simple: ask about the file, then act on the answer.

See Also