Version 33 of Playing Bourne shell

Updated 2002-05-21 11:34:02

Richard Suchenwirth will have to exercise Tcl with colleagues who are familiar with the Bourne shell (/bin/sh - see Tcl heritage for the influences it had on Tcl!), and one task will be to replace /bin/sh scripts with equivalent Tcl scripts.

To make this migration easier, I am planning to introduce some syntactic sugar (Salt and sugar), so familiar built-ins can still be used. Most simply with interp alias, for which we first create a shorter version:

 proc alias {new args} {eval [list interp alias {} $new {}] $args}

 alias echo  puts stdout
 alias read  gets stdin ;# if we don't need the real [read]
 alias cp    file copy -force
 alias rm    file delete
 alias mkdir file mkdir
 # alias whoami subst $::tcl_platform(user)
 # raises cryptic error message when called with args, hence:
 proc whoami {} {set ::tcl_platform(user)} 

 alias -r    file readable
 alias -w    file writable
 alias -x    file executable

Note however that this echo 's output cannot be redirected to a file, or through a pipe. Things like

 foo=`echo $bar | grep "^grill"`

would have to be restructured:

 set foo [exec grep ^grill <<$bar]

which also has its charms, but needs habituation. (On the other foot, a Tcl'er would prefer regexp anyway...) Or see below, for a more powerful reimplementation which allows > and >> redirection.


The following one-liner emulates a frequently used part of /bin/sh's test command:

 proc -f name {expr {[file exists $name] && [file type $name]=="file"}}
 proc -z string {expr {[string length $string]==0}}

This way, /bin/sh code written like this:

 if [ -f $filename ] ...

needs no rewriting - but be aware that this is not a complete emulation of tests switches, so

 if [ $# -ne 1 ] ...

would have to rewritten in expr syntax (braced condition, C operators), but we even emulate the Perlish special variable ?# :

 set # [llength $argv]
 if {${#} != 1 } ...

This fills most of the shell special variables:

 # Create some shellish variables:
 proc sh'specials {} {
    global argv
    set ::0   $::argv0
    set n 0
    foreach i $argv {set ::[incr n] $i}
    set ::# [llength $argv]
    set ::\$ [pid]
    set ::? 0 ;# exit status of last command
 }

The following wrapper for exec emulates the behavior that errors are not propagated (do not cause evaluation to stop), but may be detected from the special $? variable:

 proc ! args {
    if [catch {eval exec $args} res] {
        if {[lindex $::errorCode 0]=="CHILDSTATUS"} {
            set ::? [lindex $::errorCode 2]
        }
    }
    set res
 } ;# and an echoing version...
 proc !! args {puts $args; puts [eval ! $args]}

This wraps the required clock calls into a lookalike of UNIX's date command:

 proc date {args} {
    set cmd {clock format [clock sec]}
    foreach arg $args {
        if {$arg=="-u"} {
            append cmd " -gmt 1"
        } elseif [regexp {\+(.+)} $arg -> fmt] {
            append cmd " -format [list $fmt]"
        } else {error "usage: date ?-u? ?+%H%M%S...?"}
    }
    eval $cmd
 }

To tell the truth, this is not a Bourne shell issue, but in sh scripts, system executables are often called which may not be present (or with different behavior) on other platforms.

 proc : args {set ::? 0} ;# sh's no-op - but resets exit code 

For the path-aware . in sh see source. And while we're at it, and after hinting at many more such example scripts everybody should have, here's some more Unix toolets partly reimplemented in Tcl (want more? write more!):

 proc echo {string {redirector -} {file -}} {
    set postcmd {close $fp}
    switch -- $redirector {
        >       {set fp [open $file w]}
        >>      {set fp [open $file a]}
        default {set fp stdout; set postcmd ""}
    }
    puts $fp $string
    eval $postcmd
 }
 proc cat files {
    set res ""
    foreach file [eval glob $files] {
        set fp [open $file]
        append res [read $fp [file size $file]]
        close $fp
    }
    set res
 }

Somehow my suspicion gets stronger that besides many other things, Tcl is a "lightweight operatting system" riding on top of the "real" one...


Here's an approximation to du which gives the disk usage in and below a given directory - but even after turning off the rounding up, it reports about 0.25% more than fileutils/du ;-(:

 proc du {{directory .}} {
    set res 0
    foreach item [glob -nocomplain $directory/*] {
        switch -- [file type $item] {
            directory {incr res [du $item]}
            file {
                set res [expr {$res+([file size $item]+0)/1024}]
            }
        }
    }
    set res
 } ;# RS

Tcl and other languages - Arts and crafts of Tcl-Tk programming