Skip to content
VLSI Mentor

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

When the File Is Not There

Every script that reads a file will eventually be pointed at one that does not exist, or that exists and cannot be read, or that is a directory. What should happen then is a decision, and the time to make it is while you are writing the code rather than while reading a stack trace at the wrong end of a long run. Tcl gives you two ways to handle it and they are not equivalent: one of them has a race condition that the other does not. This chapter shows both, is specific about which to reach for, and covers what the error messages actually say so you can recognise them.

Foundation11 min readTclPart IFilesErrorsRobustness

Part I · Module 12 · Chapter 12.8 · When the File Is Not There

1. What failure looks like

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
open missing.txt r
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
couldn't open "missing.txt": no such file or directory

The message names the path and the reason, which is more than many languages manage. Other reasons appear in the same shape — permission denied for a file you may not read, and so on.

Note that a different command reports the same situation differently:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file size missing.txt
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
could not read "missing.txt": no such file or directory

couldn't open from open, could not read from file size. Worth knowing when you are grepping a log for one of them.

2. Strategy one: ask first

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc read_checked {path} {
    if {![file exists $path]} {
        error "no such file: $path"
    }
    if {![file readable $path]} {
        error "cannot read: $path"
    }
    set fp [open $path r]
    set content [read -nonewline $fp]
    close $fp
    return $content
}

set fp [open present.txt w] ; puts $fp "here" ; close $fp
puts "ok      : [read_checked present.txt]"

if {[catch {read_checked absent.txt} msg]} { puts "missing : $msg" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok      : here
missing : no such file: absent.txt

The questions are cheap and never raise:

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

3. Strategy two: try it and catch

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc read_caught {path} {
    if {[catch {open $path r} fp]} {
        error "could not open $path: $fp"
    }
    set code [catch {read -nonewline $fp} content]
    close $fp
    if {$code} { return -code error $content }
    return $content
}

puts "ok      : [read_caught present.txt]"

if {[catch {read_caught absent.txt} msg]} { puts "missing : $msg" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok      : here
missing : could not open absent.txt: couldn't open "absent.txt": no such file or directory

When catch traps an error, the variable holds the message rather than a channel — which is why $fp appears inside the text there. On success the same variable holds the handle. That dual use is idiomatic and worth reading twice.

4. Why checking first is a race

Here is the argument that decides between them. Between file exists returning 1 and open running, the file can disappear — another process, a cleanup job, a user. The window is small and it is not zero:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open fragile.txt w] ; puts $fp "here for now" ; close $fp

puts "exists? [file exists fragile.txt]"

# something else removes it at exactly the wrong moment
file delete fragile.txt

if {[catch {open fragile.txt r} result]} {
    puts "but open failed: $result"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists? 1
but open failed: couldn't open "fragile.txt": no such file or directory

The check passed and the open still failed. That is contrived here because the delete is on the next line, and in a real script it is a scheduled cleanup, a parallel job, or a network filesystem.

5. So use both, for different jobs

The checks are still worth having — not as the guard, but for the message:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc read_best {path} {
    if {[catch {open $path r} fp]} {
        # the open already failed; now work out why, for a useful message
        if {![file exists $path]} {
            error "input file does not exist: $path"
        } elseif {[file isdirectory $path]} {
            error "input path is a directory, not a file: $path"
        } elseif {![file readable $path]} {
            error "input file is not readable (check permissions): $path"
        }
        error "could not open $path: $fp"
    }
    set code [catch {read -nonewline $fp} content]
    close $fp
    if {$code} { return -code error $content }
    return $content
}

puts "ok  : [read_best present.txt]"
foreach bad {absent.txt} {
    if {[catch {read_best $bad} m]} { puts "bad : $m" }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok  : here
bad : input file does not exist: absent.txt

The open is still the thing that decides. The checks run only after it has failed, so there is no race — and the caller gets a message that says what to do about it rather than just what went wrong.

6. file exists is not enough on its own

A directory exists, is readable, and is not a file:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
file mkdir somedir

puts "exists   : [file exists somedir]"
puts "readable : [file readable somedir]"
puts "isfile   : [file isfile somedir]"
puts "isdirectory : [file isdirectory somedir]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
exists   : 1
readable : 1
isfile   : 0
isdirectory : 1

And on this platform, opening it for reading succeeds:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set fp [open somedir r]
puts "opening a directory returned a handle: [string match "file*" $fp]"
close $fp
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
opening a directory returned a handle: 1

So a guard built on file exists and file readable passes a directory straight through to code expecting a file.

7. Deciding what missing should mean

Three reasonable answers, and the question that picks one.

Stop. The file is required; without it there is nothing to do:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc load_required {path} {
    if {[catch {open $path r} fp]} {
        error "required input missing: $path"
    }
    set content [read -nonewline $fp]
    close $fp
    return $content
}

Carry on with a default. The file is optional — a user's settings, an override:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc load_optional {path default} {
    if {[catch {open $path r} fp]} { return $default }
    set content [read -nonewline $fp]
    close $fp
    return $content
}

puts "present : [load_optional present.txt "(default)"]"
puts "absent  : [load_optional absent.txt  "(default)"]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
present : here
absent  : (default)

Create it. The file is something you own and an absent one simply means the first run:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc load_or_create {path initial} {
    if {![file exists $path]} {
        set fp [open $path w]
        puts -nonewline $fp $initial
        close $fp
    }
    set fp [open $path r]
    set content [read -nonewline $fp]
    close $fp
    return $content
}

puts "first call  : [load_or_create state.txt "count 0"]"
puts "second call : [load_or_create state.txt "count 0"]"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
first call  : count 0
second call : count 0

The deciding question is the same one Chapter 7.3 §7 asked about missing dictionary keys: can the caller sensibly carry on without it? Required input, no. Optional override, yes. Your own state file, create it.

8. A worked example

Putting both halves together — and noting one place where the order has to be the other way round:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc load_config {path} {
    # A directory must be rejected BEFORE the open, because section 6 showed
    # that opening one succeeds. Everything else is handled after the open.
    if {[file isdirectory $path]} {
        error "config path is a directory: $path"
    }

    if {[catch {open $path r} fp]} {
        if {![file exists $path]}   { return [dict create] }
        if {![file readable $path]} { error "config not readable: $path" }
        error "could not open config $path: $fp"
    }

    set code [catch {read -nonewline $fp} content]
    close $fp
    if {$code} { return -code error $content }

    set cfg [dict create]
    foreach line [split $content "\n"] {
        set t [string trim $line]
        if {$t eq "" || [string match "#*" $t]} continue
        set i [string first "=" $t]
        if {$i < 0} continue
        dict set cfg [string trim [string range $t 0 [expr {$i-1}]]] \
                     [string trim [string range $t [expr {$i+1}] end]]
    }
    return $cfg
}

set fp [open real.cfg w] ; puts $fp "mode = fast" ; puts $fp "runs = 3" ; close $fp
file mkdir dir.cfg

puts "present : [load_config real.cfg]"
puts "absent  : '[load_config nothing.cfg]' (empty dict, treated as optional)"
if {[catch {load_config dir.cfg} m]} { puts "a dir   : $m" }
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
present : mode fast runs 3
absent  : '' (empty dict, treated as optional)
a dir   : config path is a directory: dir.cfg

Three outcomes: a parsed configuration, an empty one because the file is optional, and a clear error for the case that is a mistake rather than an absence.

9. Exercises

Exercise 1. What is the exact message from open missing.txt r, and how does file size missing.txt differ?

Exercise 2. Why is if {[file exists $p]} { open $p r } not a guarantee?

Exercise 3. What does catch {open $p r} fp leave in fp on success, and on failure?

Exercise 4. Why is file exists insufficient for "a readable ordinary file"?

Exercise 5. Give the three reasonable meanings for a missing file and the question that chooses between them.

Exercise 6. Write a procedure returning a file's contents, or an empty string if it cannot be read, without raising.

Answers

1. couldn't open "missing.txt": no such file or directory. file size says could not read "missing.txt": no such file or directory — the same situation with different wording.

2. Because the file can be removed, renamed or have its permissions changed between the check and the open. The check predicts; only the open decides. This is a time-of-check-to-time-of-use race.

3. On success, the channel handle. On failure, the error message. The same variable carries both, which is why catch {open ...} fp reads oddly the first time.

4. Because a directory also exists and is readable, and on this platform open on a directory for reading even succeeds. file isfile is the question you actually meant.

5. Stop, because the input is required; carry on with a default, because it is optional; or create it, because it is your own state file and this is the first run. The question is whether the caller can sensibly carry on without it.

6.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc read_or_empty {path} {
    if {[catch {open $path r} fp]} { return "" }
    set code [catch {read -nonewline $fp} content]
    close $fp
    if {$code} { return "" }
    return $content
}
puts "present : '[read_or_empty present.txt]'"
puts "absent  : '[read_or_empty absent.txt]'"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
present : 'here'
absent  : ''

10. Remember

  • open on a missing file says couldn't open "path": no such file or directory; file size says could not read.
  • file exists, readable and writable never raise — they return 0 for a path that is not there.
  • Checking before opening is a race. The file can vanish in between; only the open decides.
  • So catch around open is the part that cannot be wrong, and the checks belong after a failure, to produce a better message.
  • file exists is true for directories too — use file isfile when you mean an ordinary file. Opening a directory for reading succeeds, so this is the one check that must come before the open; the failure otherwise surfaces at read as illegal operation on a directory, naming a channel rather than your path.
  • Decide what missing means: stop, default, or create. The question is whether the caller can carry on without it.
  • Say which you chose, so the next person knows it was deliberate.

Where this fits

Part of the Tcl curriculum.