QF0
QF0

Reputation: 529

Tcl: how does this proc return a value?

I'm modifying the code below, but I have no idea how it works - enlightenment welcome. The issue is that there is a proc in it (cygwin_prefix) which is meant to create a command, by either

  1. leaving a filename unmodified, or
  2. prepending a string to the filename

The problem is that the proc returns nothing, but the script magically still works. How? Specifically, how does the line set command [cygwin_prefix filter_g] actually manage to correctly set command?

For background, the script simply execs filter_g < foo.txt > foo.txt.temp. However, historically (this no longer seems to be the case) this didn't work on Cygwin, so it instead ran /usr/bin/env tclsh filter_g < foo.txt > foo.txt.temp. The script as shown 'works' on both Linux (Tcl 8.5) and Cygwin (Tcl 8.6).

Thanks.


#!/usr/bin/env tclsh

proc cygwin_prefix { file } {
    global cygwin
    if {$cygwin} {
        set status [catch { set fpath [eval exec which $file] } result ]
        if { $status != 0 } {
            puts "which error: '$result'"
            exit 1
        }
        set file "/usr/bin/env tclsh $fpath"
    }
    set file
}

set cygwin 1
set filein foo.txt
set command [cygwin_prefix filter_g]
set command "$command < $filein > $filein.temp"
set status [catch { eval exec $command } result ]
if { $status != 0 } {
    puts "filter error: '$result'"
    exit 1
}
exit 0

Upvotes: 0

Views: 1852

Answers (2)

Donal Fellows
Donal Fellows

Reputation: 137567

The key to your question is two-fold.

  1. If a procedure doesn't finish with return (or error, of course) the result of the procedure is the result of the last command executed in that procedure's body.

    (Or the empty string, if no commands were executed. Doesn't apply in this case.)

    This is useful for things like procedures that just wrap commands:

    proc randomPick {list} {
        lindex $list [expr { int(rand() * [llength $list]) }]
    }
    

    Yes, you could add in return […] but it just adds clutter for something so short.

  2. The set command, with one argument, reads the named variable and produces the value inside the var as its result.

    A very long time ago (around 30 years now) this was how all variables were read. Fortunately for us, the $… syntax was added which is much more convenient in 99.99% of all cases. The only place left where it's sometimes sensible is with computed variable names, but most of the time there's a better option even there too.

The form you see with set file at the end of a procedure instead of return $file had currency for a while because it produced slightly shorter bytecode. By one unreachable opcode. The difference in bytecode is gone now. There's also no performance difference, and never was (especially by comparison with the weight of exec which launches subprocesses and generally does a lot of system calls!)

Upvotes: 2

glenn jackman
glenn jackman

Reputation: 246764

It's not required to use eval for exec. Building up a command as a list will protect you from, for example, path items that contain a space. Here's a quick rewrite to demonstrate:

proc cygwin_prefix { file } {
    if {$::cygwin} {
        set status [catch { set fpath [exec which $file] } result]
        if { $status != 0 } {
            error "which error: '$result'"
        }
        set file [list /usr/bin/env tclsh $fpath]
    }
    return $file
}

set cygwin 1
set filein foo.txt
set command [cygwin_prefix filter_g]
lappend command "<" $filein ">" $filein.temp

set status [catch { exec {*}$command } result]
if { $status != 0 } {
    error "filter error: '$result'"
}

This uses {*} to explode the list into individual words to pass to exec.

Upvotes: 2

Related Questions