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?
answer
- relative to the working directory
- Dir.pwd, not the source file
- __dir__: absolute, symlinks resolved
- File.expand_path(name, __dir__)
- File.join inserts the separators
basics
~10 sRelative 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 sRuby 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# 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
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.
Explain what expand_path normalises, how File.join collapses separators, why neither touches the disk, and the extname and basename edge cases.
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.
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.