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?
answer
- ** followed by / recurses
- {jpg,png} alternation
- dotfiles skipped without FNM_DOTMATCH
- sorted by default since 3.0
- Dir.children drops . and ..
basics
~10 sDir.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.
solid answer
~40 s`Dir.glob` takes shell-style patterns, not regexps: `*` matches within one name, `**/` recurses through subdirectories, `{jpg,png}` is alternation and `?` one character. For the cleanup, `Dir.glob("uploads/**/*_thumb.{jpg,png}")` returns every thumbnail path; filter with `File.mtime(path) < cutoff` and delete. Defaults to know: entries starting with `.` are **not matched** unless you pass `flags: File::FNM_DOTMATCH`; results are **sorted** by default since Ruby 3.0 (`sort: false` skips that for large trees); `base:` makes results relative to a directory; with a block it yields each path and returns `nil`. No match is an empty array, never an error. For one directory level, `Dir.children(dir)` lists names without `.` and `..`, while `Dir.entries` includes them.
code
ruby · 14 linescutoff = Time.now - 30 * 24 * 60 * 60
thumbs = Dir.glob("uploads/**/*_thumb.{jpg,png}")
stale = thumbs.select { File.file?(it) && File.mtime(it) < cutoff }
stale.each { File.delete(it) }
# Relative results under a base directory
Dir.glob("**/*.jpg", base: "uploads") # => ["2026/09/cat_thumb.jpg", ...]
# Include dotfiles such as uploads/.cache/x_thumb.jpg
Dir.glob("uploads/**/*_thumb.jpg", flags: File::FNM_DOTMATCH)
# One level, no . or ..
Dir.children("uploads") # => ["2025", "2026"] in file-system ordergo deeper
Recall the pattern syntax: * within a name, **/ for recursion, braces for alternatives; and that Dir.children lists one directory.
Explain the defaults: hidden entries skipped without FNM_DOTMATCH, sorted results since 3.0, base: stripping the prefix, and an empty array on no match.
Build cleanup jobs that filter with File.file?, dry-run before deleting, stream with the block form and sort: false on huge trees, and log how much they touched.
Weigh recurring glob-and-delete jobs against lifecycle rules in the storage layer, and decide who owns retention policy for generated files.
## The task A service stores image uploads under `uploads/`, with generated thumbnails such as `uploads/2026/09/cat_thumb.jpg` spread through nested year and month directories. A nightly job must delete thumbnails older than 30 days. `Dir.glob` is the tool for finding them. ## Glob patterns, not regexps `Dir.glob(pattern)` expands **shell-style patterns** documented in `dir.rb`: | Pattern | Meaning | |---|---| | `*` | any characters within one entry name (not across `/`) | | `**/` | zero or more directories, recursively — only when followed by `/` | | `?` | exactly one character | | `[abc]`, `[^a-z]` | one character from a set | | `{jpg,png}` | alternation: either string | So `Dir.glob("uploads/**/*_thumb.{jpg,png}")` finds every `_thumb.jpg` or `_thumb.png` at any depth, including directly in `uploads/`. Written without the slash, `**` behaves like `*`. ## The defaults interviewers ask about 1. **Hidden entries are skipped.** `*` does not match names starting with `.`, and `**/` does not descend into dot-directories. Pass `flags: File::FNM_DOTMATCH` (or a pattern such as `{*,.*}`) to include them. 2. **Results are sorted.** Since Ruby 3.0, `Dir.glob` and `Dir[]` sort by default; before that the order was whatever the file system returned, which differed between machines. `sort: false` skips the sort on very large trees. With brace alternatives, results follow the order of the alternatives — the rdoc's own example `Dir.glob('*.{rb,h}')` returns `["main.rb", "config.h"]`. 3. **No match is not an error.** The result is `[]`, so a typo in the pattern silently finds nothing; log the count. 4. **`base:`** sets the directory the pattern is relative to, and the results do **not** include that prefix: `Dir.glob("**/*.jpg", base: "uploads")` returns `"2026/09/cat_thumb.jpg"`. 5. **Block form.** `Dir.glob(pattern) { |path| ... }` yields each path and returns `nil`. ## The cleanup job ```ruby cutoff = Time.now - 30 * 24 * 60 * 60 old = Dir.glob("uploads/**/*_thumb.{jpg,png}").select do |path| File.file?(path) && File.mtime(path) < cutoff end old.each { |path| File.delete(path) } puts "deleted #{old.size} thumbnails" ``` Two production points: filter with `File.file?` so a directory named like a thumbnail is never passed to `File.delete`, and run the job once with the delete line replaced by a `puts` to review what it would remove. ## Listing one directory When you do not need recursion or a pattern: - `Dir.children("uploads")` returns entry names **without** `.` and `..`. - `Dir.entries("uploads")` returns them **with** `.` and `..`, a classic source of bugs when code recurses into `..`. - `Dir.each_child("uploads") { |name| ... }` is the streaming form. None of these sort, and none include the directory prefix — join it back with `File.join(dir, name)`. `Dir.exist?(path)` checks for a directory. ## Several patterns, shorthand and escaping - **An array of patterns** is accepted: `Dir.glob(["uploads/**/*.jpg", "uploads/**/*.png"])` returns the results of each pattern in turn. - **`Dir[]`** is shorthand for `Dir.glob` without flags: `Dir["uploads/**/*.jpg"]`. It takes the same `base:` and `sort:` keywords. - **Literal metacharacters.** A directory literally named `drafts[old]` would be read as a character class. Escape the metacharacter with a backslash in the pattern, or pass `base:` so the directory name is not part of the pattern at all. - **Windows.** Use forward slashes in patterns; the rdoc notes that a backslash in a pattern does not work as a separator there. - **`Pathname.glob`** returns `Pathname` objects instead of strings, if the rest of the code works with `Pathname`. ## Common mistakes - Using `Dir.glob("uploads/*.jpg")` and missing everything in subdirectories. - Expecting `**` alone (`uploads/**.jpg`) to recurse. - Assuming unsorted results on Ruby 3.0+, or sorted results from `Dir.children`. - Forgetting that dotfiles are invisible to the default pattern.
- The glob finds 3 million thumbnails and the job is slow before deleting anything; what would you change?Pass `sort: false`, since order does not matter for deletion, and use the block form `Dir.glob(pattern) { |path| ... }` so each path is checked and deleted as it is found instead of first returning a 3-million-element Array. Batch logging, and consider narrowing the pattern to old year or month directories.
- Why can code that recurses with Dir.entries loop forever or escape the directory?`Dir.entries` includes `.` and `..`. Recursing into `.` revisits the same directory, and `..` climbs to the parent. `Dir.children` and `Dir.each_child` exclude both, which is why they are the safer default for walking a tree by hand.
saying these in an interview costs you the question
- Dir.glob returns files in whatever order the file system gives on Ruby 4.0.
- Dir.glob("uploads/**.jpg") searches all subdirectories.
- Dir.glob matches hidden files by default, just like ls -a.
- Dir.glob raises Errno::ENOENT when nothing matches the pattern.
- Dir.children includes the . and .. entries.