Skip to content
VLSI Mentor

Tcl Part I · Module 12 · Files, Directories, and Paths

glob: Listing Files by Pattern

Finding every CSV in a directory is a one-liner, and the one-liner has a sharp edge that catches people on their first real run rather than in testing: when nothing matches, glob does not return an empty list — it raises an error. Anyone arriving from shell scripting expects the opposite, and the failure appears only when a directory happens to be empty, which is rarely the case while you are writing the code and frequently the case afterwards. This chapter covers the command, that behaviour, and the two options you should treat as part of its normal spelling.

Foundation11 min readTclPart IFilesglobPatterns

Part I · Module 12 · Chapter 12.12 · glob: Listing Files by Pattern

1. Matching files

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file mkdir data
foreach n {a.csv b.csv notes.txt} {
    set fp [open data/$n w] ; puts $fp "x" ; close $fp
}

puts [lsort [glob data/*.csv]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
data/a.csv data/b.csv

The pattern is the glob language from Chapter 5.5 — *, ?, [...] — applied to filenames. The result is an ordinary list, so everything from Module 6 works on it.

2. No matches is an error

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
glob data/*.zip
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no files matched glob pattern "data/*.zip"

Not an empty list — an error that stops the script. This is the opposite of a shell, where an unmatched pattern is usually passed through or quietly produces nothing.

-nocomplain is the fix:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set zips [glob -nocomplain data/*.zip]
puts "found [llength $zips] zip file(s)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
found 0 zip file(s)

It also covers a directory that does not exist at all:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts "missing dir : [llength [glob -nocomplain -directory nosuchdir *]] result(s)"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
missing dir : 0 result(s)

Without -nocomplain that is an error too — so one option handles both "nothing matched" and "nowhere to look".

3. Then decide what empty means

-nocomplain turns the error into data. It does not decide what the data means — that is still your job, and it is Chapter 12.8 §7's question again:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc inputs_in {dir} {
    set files [lsort [glob -nocomplain -directory $dir *.csv]]
    if {[llength $files] == 0} {
        error "no .csv inputs found in '$dir'"
    }
    return $files
}

puts "found : [llength [inputs_in data]] file(s)"

file mkdir empty
if {[catch {inputs_in empty} m]} { puts "empty : $m" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
found : 2 file(s)
empty : no .csv inputs found in 'empty'

The error now names the directory and says what was expected, which the built-in message does not.

4. -directory, and why it is not -tails

-directory says where to look without putting the directory in the pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [lsort [glob -directory data *.csv]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
data/a.csv data/b.csv

Note what came back: full paths, not bare names. -directory changes where the search happens, not what the results look like.

-tails is the option that strips the directory:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [lsort [glob -tails -directory data *.csv]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a.csv b.csv
You wantUse
Paths you can open directly-directory $dir
Bare filenames, for display or matching-tails -directory $dir

5. Several patterns, and -types

More than one pattern in a single call:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts [lsort [glob -nocomplain -tails -directory data *.csv *.txt]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a.csv b.csv notes.txt

And -types filters by what the entry is:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file mkdir data/subdir

puts "files only : [lsort [glob -nocomplain -tails -directory data -types f *]]"
puts "dirs only  : [lsort [glob -nocomplain -tails -directory data -types d *]]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
files only : a.csv b.csv notes.txt
dirs only  : subdir

-types f for ordinary files and -types d for directories are the two you will use. Without it, * matches both — which is why a loop that opens everything glob returned will eventually try to open a directory, the hazard from Chapter 12.8 §6.

6. Recursion is not built in

glob matches one directory level. There is no ** pattern:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file mkdir deep/a/b
foreach p {deep/top.csv deep/a/mid.csv deep/a/b/low.csv} {
    set fp [open $p w] ; puts $fp "x" ; close $fp
}

puts "one level  : [lsort [glob -nocomplain -tails -directory deep *.csv]]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
one level  : top.csv

To walk a tree, recurse yourself:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc find_files {dir pattern} {
    set found [glob -nocomplain -directory $dir $pattern]
    foreach sub [glob -nocomplain -directory $dir -types d *] {
        set found [concat $found [find_files $sub $pattern]]
    }
    return $found
}

foreach f [lsort [find_files deep *.csv]] {
    puts "  $f"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  deep/a/b/low.csv
  deep/a/mid.csv
  deep/top.csv

Twelve lines, and every glob in it has -nocomplain — which matters more in a recursive walk, because a leaf directory with no matches is the normal case rather than the exception.

7. A worked example

Selecting inputs, reporting clearly on what was found:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc survey {dir} {
    if {![file isdirectory $dir]} {
        return "not a directory: $dir"
    }

    set csvs  [lsort [glob -nocomplain -tails -directory $dir -types f *.csv]]
    set other [lsort [glob -nocomplain -tails -directory $dir -types f *]]
    set subs  [lsort [glob -nocomplain -tails -directory $dir -types d *]]

    set lines {}
    lappend lines "$dir:"
    lappend lines "  [llength $csvs] csv, [llength $other] file(s) total, [llength $subs] subdirectory(ies)"
    foreach c $csvs {
        set full [file join $dir $c]
        lappend lines [format "    %-12s %5d bytes" $c [file size $full]]
    }
    return [join $lines "\n"]
}

puts [survey data]
puts [survey empty]
puts [survey nosuchdir]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
data:
  2 csv, 3 file(s) total, 1 subdirectory(ies)
    a.csv            2 bytes
    b.csv            2 bytes
empty:
  0 csv, 0 file(s) total, 0 subdirectory(ies)
not a directory: nosuchdir

Every glob has -nocomplain, so the empty directory produces zeros rather than an error, and the missing directory is caught by an explicit check that gives a better message than the glob would.

8. Exercises

Exercise 1. What does glob data/*.zip do when nothing matches?

Exercise 2. What does -nocomplain change, and what does it not decide for you?

Exercise 3. Does glob -directory data *.csv return a.csv or data/a.csv?

Exercise 4. Which option gives bare filenames, and what does it require?

Exercise 5. Write a procedure returning every .log file in a directory, sorted, raising a clear error if there are none.

Exercise 6. Why does glob need -types f in a loop that opens each result?

Answers

1. It raises no files matched glob pattern "data/*.zip" and stops the script. It does not return an empty list.

2. It makes "nothing matched" produce an empty list instead of an error, and it covers a missing directory too. It does not decide what an empty result means — carrying on, using a default, or raising a better error is still your decision.

3. data/a.csv. -directory changes where the search happens, not the form of the results.

4. -tails, which requires -directory. Remember to join the names back onto the directory with file join before opening them.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc logs_in {dir} {
    set files [lsort [glob -nocomplain -directory $dir -types f *.log]]
    if {[llength $files] == 0} {
        error "no .log files in '$dir'"
    }
    return $files
}

6. Because without it * matches directories as well as files, and a loop that opens each result will eventually open a directory — which succeeds on some platforms and then fails at the first read, with a message naming a channel rather than a path.

9. Remember

  • glob pattern matches filenames with the string match language and returns an ordinary list.
  • No matches raises no files matched glob pattern "..." — not an empty list. This is the opposite of shell behaviour.
  • Write -nocomplain every time. It covers both "nothing matched" and "the directory does not exist".
  • Then decide what empty means yourself — carry on, default, or raise a message that names the directory.
  • -directory $dir returns full paths; -tails gives bare names and requires -directory.
  • Join -tails results back with file join before opening them.
  • -types f and -types d filter to files or directories; without them * matches both.
  • glob is one level deep — recurse yourself for a tree, with -nocomplain on every call.
  • The order is unspecified, so lsort around every glob is a good habit.

Where this fits

Part of the Tcl curriculum.