Skip to content
VLSI Mentor

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

The file Command: exists, readable, size, mtime, type

file is an ensemble like string and dict, and its subcommands answer questions about a path without opening it. Most are one word and obvious. Two things make the set worth a chapter: the subcommands split cleanly into ones that are safe to call on a path that may not exist and ones that raise if it does not, and that split is not advertised by their names. And one of them, mtime, is the basis of every script that avoids redoing work it has already done — which is a more useful idea than its one-line description suggests.

Foundation11 min readTclPart IFilesfile commandmtime

Part I · Module 12 · Chapter 12.9 · The file Command: exists, readable, size, mtime, type

1. The questions

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open probe.txt w] ; puts $fp "some content" ; close $fp
file mkdir probedir

puts "exists      : [file exists probe.txt]"
puts "isfile      : [file isfile probe.txt]"
puts "isdirectory : [file isdirectory probe.txt]"
puts "readable    : [file readable probe.txt]"
puts "writable    : [file writable probe.txt]"
puts "size        : [file size probe.txt]"
puts "type        : [file type probe.txt]"
puts "type of dir : [file type probedir]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists      : 1
isfile      : 1
isdirectory : 0
readable    : 1
writable    : 1
size        : 13
type        : file
type of dir : directory

file is an ensemble, so the usual trick from Chapter 5.1 works — calling it with a bad subcommand lists them all.

2. Which are safe on a missing path

This is the split that matters, and the names do not tell you:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
puts "exists      : [file exists nothing.txt]"
puts "isfile      : [file isfile nothing.txt]"
puts "isdirectory : [file isdirectory nothing.txt]"
puts "readable    : [file readable nothing.txt]"
puts "writable    : [file writable nothing.txt]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists      : 0
isfile      : 0
isdirectory : 0
readable    : 0
writable    : 0

All five answer 0 and none of them raises. Now the accessors:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file size nothing.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
could not read "nothing.txt": no such file or directory
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file mtime nothing.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
could not read "nothing.txt": no such file or directory
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file type nothing.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
could not read "nothing.txt": no such file or directory
SubcommandOn a missing path
exists, isfile, isdirectory, readable, writable, executablereturns 0
size, mtime, atime, type, attributes, statraises an error

3. size

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open sized.txt w]
puts -nonewline $fp "exactly twenty chars"
close $fp

puts "bytes : [file size sized.txt]"
puts "chars : [string length "exactly twenty chars"]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
bytes : 20
chars : 20

file size is the size on disk in bytes, which for plain ASCII equals the character count and for anything else does not — the distinction Chapter 5.2 drew for string length versus string bytelength.

An empty file is 0 bytes, and a file holding one empty line is 1:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open e0.txt w] ;                close $fp
set fp [open e1.txt w] ; puts $fp "" ; close $fp
puts "empty file      : [file size e0.txt]"
puts "one blank line  : [file size e1.txt]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
empty file      : 0
one blank line  : 1

That single newline is the difference Chapter 12.3 §4 could not see after read -nonewline.

4. mtime: when it last changed

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open stamped.txt w] ; puts $fp "x" ; close $fp

set t [file mtime stamped.txt]
puts "raw          : $t"
puts "is an integer: [string is integer -strict $t]"
puts "formatted    : [clock format $t -format "%Y-%m-%d %H:%M:%S"]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
raw          : 1791467472
is an integer: 1
formatted    : 2026-10-08 11:51:12

The raw value is seconds since the epoch, so it is an ordinary integer you can compare, subtract and sort by. clock format renders it; clock seconds gives you the current time in the same units.

Those two numbers depend on when you run it. They are the only values on this page that will not match yours — as is the timestamp in section 7.

5. Telling fresh from stale

This is what mtime is for. A script that generates an output from an input can skip the work when the output is already newer:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc needs_rebuild {source target} {
    if {![file exists $target]} { return 1 }
    if {![file exists $source]} { error "source missing: $source" }
    return [expr {[file mtime $source] > [file mtime $target]}]
}

set fp [open input.txt w]  ; puts $fp "data" ; close $fp
puts "no output yet   : [needs_rebuild input.txt output.txt]"

set fp [open output.txt w] ; puts $fp "built" ; close $fp
puts "output is newer : [needs_rebuild input.txt output.txt]"

after 1100
set fp [open input.txt w]  ; puts $fp "changed" ; close $fp
puts "input touched   : [needs_rebuild input.txt output.txt]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
no output yet   : 1
output is newer : 0
input touched   : 1

Three states: nothing built yet, built and current, and the input has changed since. That is the whole of a make-style dependency check in five lines.

6. type

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
foreach p {probe.txt probedir} {
    puts [format "%-10s %s" $p [file type $p]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
probe.txt  file
probedir   directory

The values you will meet are file and directory; link appears for a symbolic link. file type raises on a missing path, so pair it with file exists, or use the predicates instead when a yes-or-no answer will do.

7. Describing a path safely

Putting the split from section 2 to work:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc describe {path} {
    if {![file exists $path]} {
        return "$path: does not exist"
    }

    set kind [file type $path]
    if {$kind ne "file"} {
        return "$path: a $kind"
    }

    set size [file size $path]
    set when [clock format [file mtime $path] -format "%Y-%m-%d %H:%M"]
    set perms {}
    if {[file readable $path]} { lappend perms readable }
    if {[file writable $path]} { lappend perms writable }

    return "$path: $size bytes, modified $when, [join $perms {, }]"
}

foreach p {probe.txt probedir nothing.txt} {
    puts [describe $p]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
probe.txt: 13 bytes, modified 2026-10-08 11:51, readable, writable
probedir: a directory
nothing.txt: does not exist

Every accessor runs only after file exists has said yes, so none of them can raise. The timestamp is the one line that will differ for you.

8. A worked example

A tiny build check that reports what it would do and why:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc build_status {sources target} {
    if {![file exists $target]} {
        return "BUILD  $target does not exist yet"
    }

    set target_time [file mtime $target]
    set newer {}

    foreach s $sources {
        if {![file exists $s]} { return "ERROR  missing source: $s" }
        if {[file mtime $s] > $target_time} { lappend newer $s }
    }

    if {[llength $newer] == 0} {
        return "SKIP   $target is up to date"
    }
    return "BUILD  $target is older than: [join $newer {, }]"
}

foreach f {a.src b.src} { set fp [open $f w] ; puts $fp "x" ; close $fp }

puts [build_status {a.src b.src} out.bin]

set fp [open out.bin w] ; puts $fp "built" ; close $fp
puts [build_status {a.src b.src} out.bin]

after 1100
set fp [open b.src w] ; puts $fp "edited" ; close $fp
puts [build_status {a.src b.src} out.bin]

puts [build_status {a.src missing.src} out.bin]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
BUILD  out.bin does not exist yet
SKIP   out.bin is up to date
BUILD  out.bin is older than: b.src
ERROR  missing source: missing.src

Four outcomes from one procedure: nothing built, built and current, built but out of date with the stale input named, and a source that is not there. The error reports the first source that failed file exists, which is why it names missing.src rather than stopping at a.src.

9. Exercises

Exercise 1. Which file subcommands are safe on a path that does not exist, and which raise?

Exercise 2. What is the rule that distinguishes the two groups?

Exercise 3. What are the units of file mtime, and how do you render it?

Exercise 4. Write needs_rebuild returning 1 when a target is missing or older than its source.

Exercise 5. Why does the rebuild example need a delay between writing the two files?

Exercise 6. What does file size report for an empty file and for one containing a single blank line?

Answers

1. exists, isfile, isdirectory, readable, writable and executable return 0 and never raise. size, mtime, atime, type, attributes and stat raise could not read "path": no such file or directory.

2. Predicates versus accessors. A yes-or-no question has a sensible answer for a missing path — "no". A question asking for a value has nothing to return, so it fails.

3. Seconds since the epoch, as an ordinary integer. clock format $t -format "..." renders it, and clock seconds gives the current time in the same units.

4.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc needs_rebuild {source target} {
    if {![file exists $target]} { return 1 }
    if {![file exists $source]} { error "source missing: $source" }
    return [expr {[file mtime $source] > [file mtime $target]}]
}

5. Because mtime has one-second resolution. Two files written in the same second get equal timestamps, so > is false and a genuine change looks like an up-to-date target.

6. 0 bytes for an empty file, 1 byte for a file containing a single blank line — the newline itself.

10. Remember

  • file is an ensemble; calling it with a bad subcommand lists every subcommand.
  • Predicates are safe on a missing path and return 0: exists, isfile, isdirectory, readable, writable, executable.
  • Accessors raise: size, mtime, type, and friends say could not read "path": no such file or directory.
  • Guard an accessor with a predicate, or wrap it in catch.
  • file size is bytes, which equals characters only for plain ASCII.
  • file mtime is seconds since the epoch — an integer you can compare and sort; clock format renders it.
  • Comparing mtimes is how a script skips work it has already done.
  • mtime has one-second resolution, so files written in the same second tie — a real source of flaky build logic.
  • file type returns file, directory or link and raises on a missing path.

Where this fits

Part of the Tcl curriculum.