TclX provides many built-in commands (not procs) that add the functionality of C or of most Unix shells to core Tcl. These functions include date and time processing, file operations, process handling, and the "system" call. Even the simplest commands provide a performance enhancement over core Tcl because no exec is necessary. The more advanced commands provide functionality not needed in the original vision of core Tcl, but very much needed to make Tcl a standalone language.
Note: this chapter is not an introduction to Unix programming. I'm assuming that you are already a Unix programmer, and therefore familiar with the Unix-style functions discussed here.
chown, chgrp, and chmod are syntactically much as you would expect. The only difference between the shell syntax and the TclX syntax is in chown, where owner and group (if both are specified) are a 2-element space-separated list rather than the owner.group (or POSIX owner:group) form familiar from the command line. (Some older Unices may not even let you specify owner and group with chown, but force you to use chgrp separately.)
cl>ls -l testfile -rw-r--r-- 1 root system 0 May 23 19:42 testfile tcl>chown wombat testfile tcl>ls -l testfile -rw-r--r-- 1 wombat system 0 May 23 19:42 testfile tcl>chown {de ourstaff} testfile tcl>ls -l testfile -rw-r--r-- 1 de ourstaff 0 May 23 19:42 testfile tcl>set fd [open testfile r] tcl>chown {de ourstaff} -fileid $fdAll three commands can accept symbolic as well as numeric user and group IDs, and chmod can accept both octal and symbolic protection modes. All three commands can accept a list of filenames in place of a single filename.
One essential command for controlling file access is umask, which works exactly like its csh equivalent:
umask ?octalMask?If the octalMask argument is omitted, the current umask is returned.
chroot dirNameinvokes the POSIX chroot system call, setting the process's effective filesystem root (/) to the specified directory; only the superuser can use this command.
dirs, pushd and popd also function much as a C or bash shell user would expect. dirs lists the directories currently on the directory stack; pushd dirName changes to the directory dirName and pushes it onto the stack, and popd pops the topmost (most recent) entry from the stack and cd's to it:
tcl>dirs /home/de tcl>pushd /usr/local/etc tcl>dirs /usr/local/etc /home/de tcl>pushd /tmp tcl>dirs /tmp /usr/local/etc /home/de tcl>popd /usr/local/etc tcl>popd /home/deTclX's confirming echo for pushd and popd differs slightly from what csh or bash would provide, but the functionality is the same.
An important Unix feature is the ability to deliver signals to processes, using either system calls or the kill command.
kill ?-pgroup? ?signal? idListsends a signal signal (the default is SIGTERM, or 15, if unspecified) to a process ID or list of process IDs provided as idList. If the -pgroup flag is present then the IDs in idList are accepted as process group IDs instead of individual process ids; when this flag is present and the process ID is 0, the current process group is assumed. (Not all Unices have process groups; they are a BSD concept, a way of grouping processes that share certain resources like stdin and stdout. POSIX-compliant systems should support process groups.) The signal can be specified as a Unix integer signal number, SIGXXX, or merely XXX, so the following are all equivalent:
tcl> kill HUP 151 tcl> kill SIGHUP 151 tcl> kill 1 151The Unix ln command is implemented as
link ?-sym? origPath linkPathwhich works exactly like the command-line equivalent, the only difference being that the symbolic link flag is -sym instead of simply -s. (Some Unix systems do not support symbolic links, so the -sym flag is not available when TclX is built on those systems.) The unlink command
unlink ?-nocomplain? fileListrequires no explanation -- and is more economical by far than exec /bin/rm -f $filename.
The mkdir command likewise,
mkdir ?-path? newDirdiffers from the Unix mkdir command only in that the -p flag has been expanded to the verbose -path. (Not every Unix supports the -p flag).
rmdir ?-nocomplain? dirListdoes exactly what you would expect.
readdir dirPathreturns a list of all the files in the target directory and represents a slight increase in efficiency (no additional process is launched) over exec ls dirPath:
tcl>readdir /usr/local/www/de/book outline.html over.html math.html string.html list.html unix.html debug.html file.html tcp.html keyl.html sample.sgmlThe nice command can only change the current process priority, with the same rules that apply to the shell nice command: negative niceness values only work if you're root! A major difference is that even the root user cannot change the priorities of other processes using the TclX nice comand. The priority increment is expressed as a positive integer value (with no dashes to confuse the user):
tcl> nice 10The sleep command does just what you would expect:
sleep sleepSecondscauses the TclX process to suspend itself for sleepSeconds seconds.
The TclX sync command implements both the shell sync command and the fsync system call (if the Unix system where TclX was built supports this call). sync with no argument schedules all cached disk writes for physical flush, and returns immediately. sync fileId, however, immediately flushes and syncs the file associated with fileId, and does not return until the flush is complete. If the current Unix system does not support fsync, then a TclX sync command with a fileId argument just ignores the argument and does a regular sync.
Not all TclX Unix functions are implementations of ordinary shell commands, as we saw with sync. Some implement C library functions and system calls.
alarm delaySecrequests a SIGALARM after delaySec seconds. delaySec can be expressed as a floating-point number, to denote fractional seconds; on systems without setitimer, delaySec is rounded up to the next integer number of seconds. Only one alarm can be active at any given time; if an alarm command cancels a previous alarm, the return value is the number of seconds that were remaining in the earlier alarm.
The function getclock returns the system time, as an integer number of seconds; this is useful for a quick and dirty unique filename:
tcl>set ofp [open /tmp/checkdump.[getclock] w]Because getclock returns an integer number of seconds, it can easily be used for relative time calculations. The additional date/time functions convertclock and fmtclock make it relatively trivial to parse and calculate datetime values.
fmtclock clockval ?formatString? ?GMT?converts a system clock value (as returned by getclock) into a formatted date/time string:
tcl>set now [getclock] tcl> echo $now 801285817 tcl>fmtclock $now Tue May 23 20:23:37 PDT 1995 tcl>fmtclock $now %H:%M:%S 20:23:37 tcl>fmtclock $now "%a %d %h %Y at %R" Tue 23 May 1995 at 20:23See the strftime (or equivalent function) man page on your system for some idea of fmtclock's formatting capabilities. convertclock provides the reverse transformation, converting a datetime string back into a system clock value.
convertclock dateString ?GMT? ?baseclock?accepts some useful keywords in dateString, such as yesterday, tomorrow, 3 weeks (from now), etc.:
tcl>set now [getclock] tcl> echo $now 801330497 tcl>fmtclock $now Wed May 24 08:48:17 PDT 1995 tcl>convertclock "Wed May 24 08:48:17" 801330497 tcl>fmtclock [convertclock tomorrow] Thu May 25 08:46:47 PDT 1995 tcl>fmtclock [convertclock yesterday] Tue May 23 08:46:56 PDT 1995 tcl>fmtclock [convertclock "3 weeks"] Wed Jun 14 08:47:03 PDT 1995 tcl>fmtclock [convertclock "next Tuesday"] Tue Jun 06 00:00:00 PDT 1995The baseclock argument, seldom used, permits you to supply a TOYclock value to be used as the current time/date.
You can have a lot of fun with convertclock and fmtclock, doing date magic for many kinds of user applications. fmtclock can be used on the system clock values returned by file mtime and file atime, as well:
tcl>set thresh [convertclock "-2 days"] tcl>echo $thresh 832124172 tcl>fmtclock $thresh Tue May 14 18:36:12 PDT 1996 tcl>fmtclock [getclock] Thu May 16 18:36:26 PDT 1996 tcl>foreach f [glob .*] { => if {[file mtime $f] > $thresh} { => echo "file $f was modified in the last 2 days" => } =>} file . was modified in the last 2 days file .cshrc was modified in the last 2 days file .history was modified in the last 2 days file .netscape-preferences was modified in the last 2 days file .netscape-cache was modified in the last 2 days file .netscape-history was modified in the last 2 days file .netscape-bookmarks.html was modified in the last 2 days tcl>(You could have done this with the Unix find command, of course; but using Tcl you can do more complicated things with files selected by mtime/atime/... than are easily done with the -exec flag on find.) Note: Core Tcl 7.5 incorporates this kind of date/time functionality, but users of earlier versions need TclX to play with system clock values.
Another Unix feature you can use easily from a sh script, but not from core Tcl is the times command:
tcl>times 0 51 0 0The return value is a list containing (in order) the user time and system time of the parent (tcl script) and child (executed command) processes (see the time function for your system, or the time (csh) and times (sh) commands).
The id command rolls up into one syntax the setuid, getuid family of system calls. It converts user/group ID numbers easily into user names and vice versa, as well as setting UID and GID.
tcl>id convert user de 777 tcl>id convert userid 777 de tcl> tcl>id convert groupid 666 ourstaff tcl>id convert group ourstaff 666 tcl>id user de tcl>id userid 777 tcl>id group ourstaffA user with sufficient privileges can change her group or uid:
tcl>id group tdevils tcl>id group tdevilsThe TclX id command implements setuid but not seteuid; both of these commands
id user userName id userid userIDwould set both real and effective UIDs to the user represented by userName or userID. id process returns the PID of the current process and its relatives:
tcl>id process 1452 tcl>id process parent 504 tcl>id process group 1452 tcl>The system command is provided as an alternative to exec, with the advantages of the system library function on which it is based. Unlike exec, system does not return the executed command's stdout as its result:
tcl>set res [exec ls .mailrc] tcl>echo $res .mailrc tcl>set res [system "ls .mailrc"] .mailrc tcl>echo $res 0 tcl>Instead it forwards stdout back to the interpreter, and returns the system call exit status as the result. Also, system uses the Unix shell so wildcard expansion, redirection, etc. work as from the shell command line:
tcl>set res [exec ls .ma*] Error: .ma* not found tcl>set res [system "ls .ma*"] .mailcap .mailrc tcl>The exec Tcl command expects a list of command elements, but the system call expects a single string containing the command, properly delimited with double-quotes or braces:
tcl>exec ls -l total 282 -rw-r--r-- 1 de wombats 0 May 22 18:18 debug.html -rw-r--r-- 1 de wombats 0 May 22 18:18 file.html tcl>system ls -l wrong # args: system command tcl>system "ls -l" total 282 -rw-r--r-- 1 de wombats 0 May 22 18:18 debug.html -rw-r--r-- 1 de wombats 0 May 22 18:18 file.html tcl>exec "ls -l" couldn't find "ls -l" to execute(This trivial inconsistency is sometimes difficult for Tcl users learning TclX; it springs from the faithfulness with which each of these commands imitates its associated C library routine.) Remember that characters enclosed in braces are sacrosanct to Tcl, but characters enclosed in double-quotes are not: if you wish to pass to the shell any characters that are "magic" for Tcl, you must "escape" them :
tcl>system "echo $HOME" Error: can't read "HOME": no such variable tcl>system "echo \$HOME" /no/place/like 0 tcl>
TclX also offers the Unix fork and execl system calls. fork returns a zero to the child process, and the child process ID to the parent; a Tcl error is generated if the fork fails. Using TclX you can launch and control child process from your Tcl scripts.
This example is from a CGI bin script (back-end to a WWW query page). The purpose of the script is to run a financial ledger; however, the ledger could take from minutes to hours to complete, and the Webserver and its client have a limited attention span. If the CGI script does not write back to stdout quickly enough, the user gets a misleading error about malformed HTML headers. This script accepts parameters from the user via the Web page, then forks off a copy of itself to run the ledger, eventually mailing the output to the user.
# feed the client something to keep its attention puts stdout "Content-type: text/html\n" puts stdout "Banner Report Results: " # # Log the user request, as we are about to start the job # set now [getclock] set afp [open /data/data7/hlogs/GL_log a] puts $afp "WANTED [fmtclock [getclock]] $type $args " close $afp # # fork off another process to exec the job # the correct command to exec will be found in the array cmd, # indexed by the type of ledger to be run # if {[set childPid [fork]] == 0} { # child process actions close stdout close stdin eval exec $cmd($type) & exit } # # Meanwhile... # parent process goes on and confirms to user that job was created, # then exits # puts stdout "<h1> Banner FIS Report: $type -- CONFIRM</h1> <p> <hr> <p> " puts stdout "Your report<br> " puts stdout "<b>Banner_$type $args</b><p>" puts stdout "will be sent to $addr when complete. If you do" puts stdout "not receive this report within the next hour," puts stdout "please inform Webmaster." puts stdout "<p> <hr> <p> </body> </html>" flush stdout exit
In a later section we'll show how we could launch our child process and continue to communicate with it over pipes; alternatively, we could just wait for it to complete and run down before taking some other action:
wait ?-nohang? ?-untraced? ?-pgroup? ?processID?waits for the termination of a process, then returns a 3-element list containing first the PID of the terminated process that we were waiting for, then (usually) the string EXIT, and lastly the exit code. If the process terminated because of a signal, the second list item would be SIG and the third would be the name of the signal that caused the termination. If the process is stopped (assuming your Unix system supports SIGSTP) then the second list item would be STOP and the third would be the signal name (SIGSTP).
If -nohang is specified, then TclX does not block waiting for a termination, but returns an empty list immediately. If -untraced is specified, then wait returns the status of child processes that have stopped, but whose status has not yet been reported. The -pgroup option causes TclX to wait on any process whose group process id is processID; processID (reasonably enough) defaults to the group id of the calling process.
We can use fork to create child processes, send them signals using kill, take action upon their demise with wait, and exchange information with them via pipes -- all from within a Tcl script. We can also handle signals sent to us by other processes:
tcl>signal trap 15 "echo I got a sig15, ouch!" tcl>id process 7896 tcl>I got a sig15, ouch!Here I started another login session and issued a kill -15 7896 from the csh command line; TclX trapped the signal and took the specified action. The signal command has many options: signals can be ignored, mapped into catchable Tcl error conditions, trapped as above, or blocked and unblocked (if you're POSIX-compliant). The signal get command allows you to check on the status of your signal handling:
tcl>signal get HUP {SIGHUP {default 0}} tcl> tcl>signal get 15 {SIGTERM {trap 0 {echo I got a sig15, ouch!}}}The return from signal get is a keyed list (we discussed keyed lists earlier in this chapter) in which the label is the name of the signal, and the value is itself a list indicating the action to be taken and whether the signal is blocked or not:
tcl>signal block HUP tcl>signal get HUP {SIGHUP {default 1}}By giving the Tcl programmer access to all these system calls and library functions, TclX makes Tcl a "real" Unix programming language in which you can write synchronized applications, daemons, and so forth. As we'll see later, TclX also supports TCP/IP for multi-process programming. With these features, some programmers can almost live without C -- except, of course, for those applications where performance is really critical or binary data have to be manipulated.