skip to content

Directories & Pathname

Building paths with Dir.glob, File.join and expand_path, the Pathname class and its / operator, and FileUtils for copies and deletes. Interviewers probe portable path code and temp-dir cleanup.

on this pageshow

explore

questions

5

In Ruby, why does a relative path like "config/settings.yml" break when a script runs from another directory, and how do __dir__ and File.expand_path fix it?

level: juniorimportance: must knowfreq 60%

answer

  1. relative to the working directory
  2. Dir.pwd, not the source file
  3. __dir__: absolute, symlinks resolved
  4. File.expand_path(name, __dir__)
  5. File.join inserts the separators

basics

~10 s

Relative paths resolve against the process's current working directory, not the source file's location. dir returns the absolute directory of the current file, so File.expand_path("config/settings.yml", dir) or File.join(dir, "config", "settings.yml") works from anywhere.

solid answer

~40 s

Ruby hands a relative path to the operating system, which resolves it against the process's **current working directory** (`Dir.pwd`) — wherever the command was launched, or a cron job's home, not the directory holding the `.rb` file. `__dir__` returns the canonical absolute directory of the file that calls it, with symlinks resolved. Anchor paths to it: `File.expand_path("config/settings.yml", __dir__)` returns an absolute path and normalises `..` segments, and `File.join(__dir__, "config", "settings.yml")` glues segments with `/`, collapsing duplicate separators at the joins. The reverse operations are `File.basename`, `File.extname` and `File.dirname`. String concatenation like `dir + "/" + name` is the thing to avoid: it doubles or drops separators and hides intent.

code

ruby · 15 lines
ruby
# lib/report.rb, run as `cd /tmp && ruby /app/lib/report.rb`
Dir.pwd                     # => "/tmp"
__dir__                     # => "/app/lib"

# File.read("config/settings.yml")  looks in /tmp/config and raises Errno::ENOENT

settings = File.expand_path("../config/settings.yml", __dir__)
# => "/app/config/settings.yml"

File.join(__dir__, "templates", "invoice.erb")
# => "/app/lib/templates/invoice.erb"

File.basename("/srv/img/photo.jpg", ".*")  # => "photo"
File.extname("archive.tar.gz")             # => ".gz"
File.dirname("/srv/img/photo.jpg")         # => "/srv/img"

go deeper

for a junior

Recall that relative paths follow Dir.pwd, that dir is the current file's directory, and that File.join and File.expand_path build paths safely.

for a middle

Explain what expand_path normalises, how File.join collapses separators, why neither touches the disk, and the extname and basename edge cases.

for a senior

Anchor every code-owned path to dir in scripts, jobs and services that run under unknown working directories, and log absolute paths when a file is missing.

for a principal

Set a convention for where runtime paths come from (code-relative, configured, or temporary) so deployment changes never depend on the launch directory.

## Where a relative path is resolved When you pass `"config/settings.yml"` to any file method, Ruby does not look next to the source file. The operating system resolves a relative path against the **current working directory** of the process, which Ruby exposes as `Dir.pwd`. That directory is: - wherever the shell was when someone ran `ruby bin/report.rb`; - the home directory, or `/`, for many cron jobs and service managers; - whatever a test runner or a daemon last changed it to with `Dir.chdir`. So a script that works from the project root fails with `Errno::ENOENT` when launched from anywhere else. The fix is to anchor paths to something stable. ## __dir__: the directory of the current file `__dir__` returns the **absolute, canonical path of the directory containing the file that calls it**. Its rdoc (`eval.c`) says symlinks in the path are resolved. It does not change when the working directory changes, so it is the right anchor for files that ship with your code: templates, fixtures, config defaults. Older code built the same value from `__FILE__` with `File.dirname` and `File.expand_path`; `__dir__` is the shorter, clearer form. ## Building paths: File.join and File.expand_path | Method | Result for `("config", "settings.yml")` relative to `/app/lib` | Touches disk? | |---|---|---| | `File.join(__dir__, "config", "settings.yml")` | `"/app/lib/config/settings.yml"` | no | | `File.expand_path("config/settings.yml", __dir__)` | `"/app/lib/config/settings.yml"` | no | | `File.expand_path("../config/settings.yml", __dir__)` | `"/app/config/settings.yml"` | no | | `File.absolute_path("~/x")` | `"<cwd>/~/x"` (no `~` expansion) | no | Key behaviours: 1. **`File.join`** joins its arguments with `"/"`, and where one segment ends with a separator and the next starts with one, it keeps only one. It does not resolve `..` and does not make the path absolute. 2. **`File.expand_path(name, dir)`** makes `name` absolute, using `dir` (or `Dir.pwd` when omitted) as the starting point, and resolves `.` and `..` lexically. It also expands a leading `~` to the home directory; `File.absolute_path` does the same work without the `~` expansion. 3. Both are **string operations**: neither checks that the file exists. `File.realpath` is the variant that asks the file system and resolves symlinks. 4. `File.join` always uses `/`; Ruby accepts forward slashes on Windows, so the result is portable. ## Taking paths apart The inverse helpers work on strings too: - `File.basename("/srv/img/photo.jpg")` → `"photo.jpg"`; `File.basename(path, ".*")` strips whatever extension there is → `"photo"`. - `File.extname("archive.tar.gz")` → `".gz"` — only the last extension; `File.extname(".profile")` → `""`, because a leading dot is not an extension. - `File.dirname("/srv/img/photo.jpg")` → `"/srv/img"`; `File.dirname(path, 2)` removes two components → `"/srv"`. ## Debugging "file not found" from a path When a job reports `Errno::ENOENT`, the fastest diagnosis is to print where Ruby actually looked: 1. Log `Dir.pwd` at startup, so every report shows the working directory the process really had. 2. Log `File.expand_path(path)` rather than the relative string; the absolute form shows at once whether the path was resolved against the wrong base. 3. Check for code that changes the working directory. `Dir.chdir(dir) { ... }` changes it for the **whole process**, restores it after the block, and warns or raises with "conflicting chdir during another chdir block" when threads collide — another reason code-owned files should hang off `__dir__` instead. ## What to say in an interview - Relative paths follow `Dir.pwd`, which the caller controls; anchor code-owned files to `__dir__`. - Prefer `File.join` or `File.expand_path` over string concatenation — they handle separators, and `expand_path` gives an absolute path you can log and compare. - Remember that neither method validates the path; building a path from user input still needs a containment check, which is a security topic of its own. - For object-style path code, `Pathname` wraps the same operations.

  • What is the difference between File.expand_path and File.realpath?
    `File.expand_path` is pure string work: it makes a path absolute against a base directory, resolves `.` and `..`, and expands `~`, without checking the file exists. `File.realpath` asks the file system: it resolves symlinks and raises `Errno::ENOENT` if a component is missing. Use `expand_path` to build paths and `realpath` when you need the canonical location of something that exists.
  • In Ruby, what does File.extname("backup.tar.gz") return, and how do you strip every extension?
    It returns `".gz"`: `File.extname` only takes the portion from the last period. `File.basename(path, ".*")` removes that one extension, giving `"backup.tar"`. To remove all of them, split on the first dot of the basename, as in `File.basename(path).split(".", 2).first`, keeping in mind that dotfiles start with a period.

saying these in an interview costs you the question

  • Relative paths are resolved against the directory of the Ruby file.
  • File.join checks that the resulting path exists on disk.
  • __dir__ returns the current working directory of the process.
  • File.extname("archive.tar.gz") returns ".tar.gz".
  • File.expand_path raises Errno::ENOENT when the file does not exist.
open as a page

In Ruby, how would you use Dir.glob to find old image thumbnails in nested directories, and what does it skip or sort by default?

level: middleimportance: must knowfreq 50%

basics

~10 s

Dir.glob("uploads/**/*_thumb.{jpg,png}") walks every subdirectory and returns matching paths, sorted by default since Ruby 3.0. It skips hidden entries unless File::FNM_DOTMATCH is passed, and returns an empty array when nothing matches.

open as a page

In Ruby's FileUtils, how do mkdir_p, cp, mv and rm_rf behave, and why is rm_rf riskier than rm_r?

level: middleimportance: should knowfreq 40%

basics

~20 s

FileUtils.mkdir_p creates missing parents and ignores an existing directory; cp copies files but not directories (cp_r does); mv renames, copying across file systems. rm_rf is rm_r with force: true, which swallows errors, so a wrong path fails silently.

open as a page

In Ruby 4.0, what does Pathname's / operator do, and why does Pathname("uploads") / "/etc/hosts" differ from File.join("uploads", "/etc/hosts")?

level: middleimportance: should knowfreq 35%

basics

~20 s

Pathname#/ is an alias of Pathname#+: it appends a relative fragment and returns a new Pathname, but an absolute right-hand side replaces the left entirely. So Pathname("uploads") / "/etc/hosts" is /etc/hosts, while File.join gives "uploads/etc/hosts".

open as a page

In Ruby, why can a long-running service using Tempfile.new fill the temp directory, and how do Tempfile.create and Dir.mktmpdir make cleanup predictable?

level: seniorimportance: should knowfreq 30%

basics

~20 s

Unless you unlink it, Tempfile.new deletes its file only when the Tempfile is garbage collected or the process exits normally, so files pile up. Tempfile.create with a block and Dir.mktmpdir with a block delete as soon as the block ends; anonymous: true removes the name immediately.

open as a page