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
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]]data/a.csv data/b.csvThe 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
glob data/*.zipno 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:
set zips [glob -nocomplain data/*.zip]
puts "found [llength $zips] zip file(s)"found 0 zip file(s)It also covers a directory that does not exist at all:
puts "missing dir : [llength [glob -nocomplain -directory nosuchdir *]] result(s)"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:
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" }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:
puts [lsort [glob -directory data *.csv]]data/a.csv data/b.csvNote 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:
puts [lsort [glob -tails -directory data *.csv]]a.csv b.csv| You want | Use |
|---|---|
| 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:
puts [lsort [glob -nocomplain -tails -directory data *.csv *.txt]]a.csv b.csv notes.txtAnd -types filters by what the entry is:
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 *]]"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:
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]]"one level : top.csvTo walk a tree, recurse yourself:
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"
} deep/a/b/low.csv
deep/a/mid.csv
deep/top.csvTwelve 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:
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]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: nosuchdirEvery 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.
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 patternmatches filenames with thestring matchlanguage 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
-nocomplainevery 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 $dirreturns full paths;-tailsgives bare names and requires-directory.- Join
-tailsresults back withfile joinbefore opening them. -types fand-types dfilter to files or directories; without them*matches both.globis one level deep — recurse yourself for a tree, with-nocomplainon every call.- The order is unspecified, so
lsortaround everyglobis a good habit.
Where this fits
Part of the Tcl curriculum.
