fileutil.tcl

Go to the documentation of this file.
00001 /*  fileutil.tcl --*/
00002 /* */
00003 /*  Tcl implementations of standard UNIX utilities.*/
00004 /* */
00005 /*  Copyright (c) 1998-2000 by Ajuba Solutions.*/
00006 /*  Copyright (c) 2002      by Phil Ehrens <phil@slug.org> (fileType)*/
00007 /*  Copyright (c) 2005-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net>*/
00008 /* */
00009 /*  See the file "license.terms" for information on usage and redistribution*/
00010 /*  of this file, and for a DISCLAIMER OF ALL WARRANTIES.*/
00011 /*  */
00012 /*  RCS: @(#) $Id: fileutil.tcl,v 1.70 2007/08/10 19:40:49 andreas_kupries Exp $*/
00013 
00014 package require Tcl 8.2
00015 package require cmdline
00016 package provide fileutil 1.13.3
00017 
00018 namespace ::fileutil {
00019     namespace export \
00020         grep find findByPattern cat touch foreachLine \
00021         jail stripPwd stripN stripPath tempdir tempfile \
00022         install fileType writeFile appendToFile \
00023         insertIntoFile removeFromFile replaceInFile \
00024         updateInPlace test tempdirRe
00025 }
00026 
00027 # ::fileutil = ::grep --
00028 /* */
00029 /*  Implementation of grep.  Adapted from the Tcler's Wiki.*/
00030 /* */
00031 /*  Arguments:*/
00032 /*  pattern     pattern to search for.*/
00033 /*  files       list of files to search; if NULL, uses stdin.*/
00034 /* */
00035 /*  Results:*/
00036 /*  results     list of matches*/
00037 
00038 ret  ::fileutil::grep (type pattern , optional files ={)} {
00039     set result [list]
00040     if {[llength $files] == 0} {
00041     /*  read from stdin*/
00042      lnum =  0
00043     while {[gets stdin line] >= 0} {
00044         incr lnum
00045         if {[regexp -- $pattern $line]} {
00046         lappend result "${lnum}:${line}"
00047         }
00048     }
00049     } else {
00050     foreach filename $files {
00051          file =  [open $filename r]
00052          lnum =  0
00053         while {[gets $file line] >= 0} {
00054         incr lnum
00055         if {[regexp -- $pattern $line]} {
00056             lappend result "${filename}:${lnum}:${line}"
00057         }
00058         }
00059         close $file
00060     }
00061     }
00062     return $result
00063 }
00064 
00065 /*  ::fileutil::find ==*/
00066 
00067 /*  Below is the core command, which is portable across Tcl versions and*/
00068 /*  platforms. Functionality which is common or platform and/or Tcl*/
00069 /*  version dependent, has been factored out/ encapsulated into separate*/
00070 /*  (small) commands. Only these commands may have multiple variant*/
00071 /*  implementations per the available features of the Tcl core /*/
00072 /*  platform.*/
00073 /* */
00074 /*  These commands are*/
00075 /* */
00076 /*  FADD   - Add path result, performs filtering. Portable!*/
00077 /*  GLOBF  - Return files in a directory.         Tcl version/platform dependent.*/
00078 /*  GLOBD  - Return dirs  in a directory.         Tcl version/platform dependent.*/
00079 /*  ACCESS - Check directory for accessibility.   Tcl version/platform dependent.*/
00080 
00081 ret  ::fileutil::find (optional basedir =. , optional filtercmd ={)} {
00082     set result {}
00083      filt =    [string length $filtercmd]
00084 
00085     if {[file isfile $basedir]} {
00086     /*  The base is a file, and therefore only possible result,*/
00087     /*  modulo filtering.*/
00088 
00089     FADD $basedir
00090 
00091     } elseif {[file isdirectory $basedir]} {
00092 
00093     /*  For a directory as base we do an iterative recursion through*/
00094     /*  the directory hierarchy starting at the base. We use a queue*/
00095     /*  (Tcl list) of directories we have to check. We access it by*/
00096     /*  index, and stop when we have reached beyond the end of the*/
00097     /*  list. This is faster than removing elements from the be-*/
00098     /*  ginning of the list, as that entails copying down a possibly*/
00099     /*  large list of directories, making it O(n*n). The index is*/
00100     /*  faster, O(n), at the expense of memory. Nothing is deleted*/
00101     /*  from the list until we have processed all directories in the*/
00102     /*  hierarchy.*/
00103     /* */
00104     /*  We scan each directory at least twice. First for files, then*/
00105     /*  for directories. The scans may internally make several*/
00106     /*  passes (normal vs hidden files).*/
00107     /* */
00108     /*  Looped directory structures due to symbolic links are*/
00109     /*  handled by _fully_ normalizing directory paths and checking*/
00110     /*  if we encountered the normalized form before. The array*/
00111     /*  'known' is our cache where we record the known normalized*/
00112     /*  paths.*/
00113 
00114      pending =  [list $basedir]
00115      at =       0
00116     array    known =  {}
00117 
00118     while {$at < [llength $pending]} {
00119         /*  Get next directory not yet processed.*/
00120          current =  [lindex $pending $at]
00121         incr at
00122 
00123         /*  Is the directory accessible? Continue if not.*/
00124         ACCESS $current
00125 
00126         /*  Files first, then the sub-directories ...*/
00127 
00128         foreach f [GLOBF $current] { FADD $f }
00129 
00130         foreach f [GLOBD $current] {
00131         /*  Ignore current and parent directory, this needs*/
00132         /*  explicit filtering outside of the filter command.*/
00133         if {
00134             [string equal [file tail $f]  "."] ||
00135             [string equal [file tail $f] ".."]
00136         } continue
00137 
00138         /*  Extend result, modulo filtering.*/
00139         FADD $f
00140 
00141         /*  Detection of symlink loops via a portable path*/
00142         /*  normalization computing a canonical form of the path*/
00143         /*  followed by a check if that canonical form was*/
00144         /*  encountered before. If ok, record directory for*/
00145         /*  expansion in future iterations.*/
00146 
00147          norm =  [fileutil::fullnormalize $f]
00148         if {[info exists known($norm)]} continue
00149          known = ($norm) .
00150 
00151         lappend pending $f
00152         }
00153     }
00154     } else {
00155     return -code error "$basedir does not exist"
00156     }
00157 
00158     return $result
00159 }
00160 
00161 /*  Helper command for fileutil::find. Performs the filtering of the*/
00162 /*  result per a filter command for the candidates found by the*/
00163 /*  traversal core, see above. This is portable.*/
00164 
00165 ret  ::fileutil::FADD (type filename) {
00166     upvar 1 result result filt filt filtercmd filtercmd
00167     if {!$filt} {
00168     lappend result $filename
00169     return
00170     }
00171 
00172     set here [pwd]
00173     cd [file dirname $filename]
00174 
00175     if {[uplevel 2 [linsert $filtercmd end [file tail $filename]]]} {
00176     lappend result $filename
00177     }
00178 
00179     cd $here
00180     return
00181 }
00182 
00183 /*  The next three helper commands for fileutil::find depend strongly on*/
00184 /*  the version of Tcl, and partially on the platform.*/
00185 
00186 /*  1. The -directory and -types swithes were added to glob in Tcl*/
00187 /*     8.3. This means that we have to emulate them for Tcl 8.2.*/
00188 /* */
00189 /*  2. In Tcl 8.3 using -types f will return only true files, but not*/
00190 /*     links to files. This changed in 8.4+ where links to files are*/
00191 /*     returned as well. So for 8.3 we have to handle the links*/
00192 /*     separately (-types l) and also filter on our own.*/
00193 /*     Note that Windows file links are hard links which are reported by*/
00194 /*     -types f, but not -types l, so we can optimize that for the two*/
00195 /*     platforms.*/
00196 /* */
00197 /*  3. In Tcl 8.3 we also have a crashing bug in glob (SIGABRT, "stat on*/
00198 /*     a known file") when trying to perform 'glob -types {hidden f}' on*/
00199 /*     a directory without e'x'ecute permissions. We code around by*/
00200 /*     testing if we can cd into the directory (stat might return enough*/
00201 /*     information too (mode), but possibly also not portable).*/
00202 /* */
00203 /*     For Tcl 8.2 and 8.4+ glob simply delivers an empty result*/
00204 /*     (-nocomplain), without crashing. For them this command is defined*/
00205 /*     so that the bytecode compiler removes it from the bytecode.*/
00206 /* */
00207 /*     This bug made the ACCESS helper necessary.*/
00208 /*     We code around the problem by testing if we can cd into the*/
00209 /*     directory (stat might return enough information too (mode), but*/
00210 /*     possibly also not portable).*/
00211 
00212 if {[package vsatisfies [package present Tcl] 8.4]} {
00213     /*  Tcl 8.4+.*/
00214     /*  (Ad 1) We have -directory, and -types,*/
00215     /*  (Ad 2) Links are returned for -types f/d if they refer to files/dirs.*/
00216     /*  (Ad 3) No bug to code around*/
00217 
00218     ret  ::fileutil::ACCESS (type args) {}
00219 
00220     ret  ::fileutil::GLOBF (type current) {
00221     concat \
00222         [glob -nocomplain -directory $current -types f          -- *] \
00223         [glob -nocomplain -directory $current -types {hidden f} -- *]
00224     }
00225 
00226     ret  ::fileutil::GLOBD (type current) {
00227     concat \
00228         [glob -nocomplain -directory $current -types d          -- *] \
00229         [glob -nocomplain -directory $current -types {hidden d} -- *]
00230     }
00231 
00232 } elseif {[package vsatisfies [package present Tcl] 8.3]} {
00233     /*  8.3.*/
00234     /*  (Ad 1) We have -directory, and -types,*/
00235     /*  (Ad 2) Links are NOT returned for -types f/d, collect separately.*/
00236     /*         No symbolic file links on Windows.*/
00237     /*  (Ad 3) Bug to code around.*/
00238 
00239     ret  ::fileutil::ACCESS (type current) {
00240     if {[catch {
00241         set h [pwd] ; cd $current ; cd $h
00242     }]} {return -code continue}
00243     return
00244     }
00245 
00246     if {[string equal $::tcl_platform(platform) windows]} {
00247     ret  ::fileutil::GLOBF (type current) {
00248         concat \
00249         [glob -nocomplain -directory $current -types f          -- *] \
00250         [glob -nocomplain -directory $current -types {hidden f} -- *]]
00251     }
00252     } else {
00253     ret  ::fileutil::GLOBF (type current) {
00254         set l [concat \
00255                [glob -nocomplain -directory $current -types f          -- *] \
00256                [glob -nocomplain -directory $current -types {hidden f} -- *]]
00257 
00258         foreach x [concat \
00259                [glob -nocomplain -directory $current -types l          -- *] \
00260                [glob -nocomplain -directory $current -types {hidden l} -- *]] {
00261         if {![file isfile $x]} continue
00262         lappend l $x
00263         }
00264 
00265         return $l
00266     }
00267     }
00268 
00269     ret  ::fileutil::GLOBD (type current) {
00270     set l [concat \
00271            [glob -nocomplain -directory $current -types d          -- *] \
00272            [glob -nocomplain -directory $current -types {hidden d} -- *]]
00273 
00274     foreach x [concat \
00275                [glob -nocomplain -directory $current -types l          -- *] \
00276                [glob -nocomplain -directory $current -types {hidden l} -- *]] {
00277         if {![file isdirectory $x]} continue
00278         lappend l $x
00279     }
00280 
00281     return $l
00282     }
00283 } else {
00284     /*  8.2.*/
00285     /*  (Ad 1,2,3) We do not have -directory, nor -types. Full emulation required.*/
00286 
00287     ret  ::fileutil::ACCESS (type args) {}
00288 
00289     if {[string equal $::tcl_platform(platform) windows]} {
00290     /*  Hidden files cannot be handled by Tcl 8.2 in glob. We have*/
00291     /*  to punt.*/
00292 
00293     ret  ::fileutil::GLOBF (type current) {
00294         set current \\[join [split $current {}] \\]
00295         set res {}
00296         foreach x [glob -nocomplain -- [file join $current *]] {
00297         if {![file isfile $x]} continue
00298         lappend res $x
00299         }
00300         return $res
00301     }
00302 
00303     ret  ::fileutil::GLOBD (type current) {
00304         set current \\[join [split $current {}] \\]
00305         set res {}
00306         foreach x [glob -nocomplain -- [file join $current *]] {
00307         if {![file isdirectory $x]} continue
00308         lappend res $x
00309         }
00310         return $res
00311     }
00312     } else {
00313     /*  Hidden files on Unix are dot-files. We emulate the switch*/
00314     /*  '-types hidden' by using an explicit pattern.*/
00315 
00316     ret  ::fileutil::GLOBF (type current) {
00317         set current \\[join [split $current {}] \\]
00318         set res {}
00319         foreach x [glob -nocomplain -- [file join $current *] [file join $current .*]] {
00320         if {![file isfile $x]} continue
00321         lappend res $x
00322         }
00323         return $res
00324     }
00325 
00326     ret  ::fileutil::GLOBD (type current) {
00327         set current \\[join [split $current {}] \\]
00328         set res {}
00329         foreach x [glob -nocomplain -- [file join $current *] [file join $current .*]] {
00330         if {![file isdirectory $x]} continue
00331         lappend res $x
00332         }
00333         return $res
00334     }
00335     }
00336 }
00337 
00338 /*  ::fileutil::findByPattern --*/
00339 /* */
00340 /*  Specialization of find. Finds files based on their names,*/
00341 /*  which have to match the specified patterns. Options are used*/
00342 /*  to specify which type of patterns (regexp-, glob-style) is*/
00343 /*  used.*/
00344 /* */
00345 /*  Arguments:*/
00346 /*  basedir     Directory to start searching from.*/
00347 /*  args        Options (-glob, -regexp, --) followed by a*/
00348 /*          list of patterns to search for.*/
00349 /* */
00350 /*  Results:*/
00351 /*  files       a list of interesting files.*/
00352 
00353 ret  ::fileutil::findByPattern (type basedir , type args) {
00354     set pos 0
00355     set cmd ::fileutil::FindGlob
00356     foreach a $args {
00357     incr pos
00358     switch -glob -- $a {
00359         --      {break}
00360         -regexp {set cmd ::fileutil::FindRegexp}
00361         -glob   {set cmd ::fileutil::FindGlob}
00362         -*      {return -code error "Unknown option $a"}
00363         default {incr pos -1 ; break}
00364     }
00365     }
00366 
00367     set args [lrange $args $pos end]
00368 
00369     if {[llength $args] != 1} {
00370     set pname [lindex [info level 0] 0]
00371     return -code error \
00372         "wrong#args for \"$pname\", should be\
00373         \"$pname basedir ?-regexp|-glob? ?--? patterns\""
00374     }
00375 
00376     set patterns [lindex $args 0]
00377     return [find $basedir [list $cmd $patterns]]
00378 }
00379 
00380 
00381 /*  ::fileutil::FindRegexp --*/
00382 /* */
00383 /*  Internal helper. Filter command used by 'findByPattern'*/
00384 /*  to match files based on regular expressions.*/
00385 /* */
00386 /*  Arguments:*/
00387 /*  patterns    List of regular expressions to match against.*/
00388 /*  filename    Name of the file to match against the patterns.*/
00389 /*  Results:*/
00390 /*  interesting A boolean flag. Set to true if the file*/
00391 /*          matches at least one of the patterns.*/
00392 
00393 ret  ::fileutil::FindRegexp (type patterns , type filename) {
00394     foreach p $patterns {
00395     if {[regexp -- $p $filename]} {
00396         return 1
00397     }
00398     }
00399     return 0
00400 }
00401 
00402 /*  ::fileutil::FindGlob --*/
00403 /* */
00404 /*  Internal helper. Filter command used by 'findByPattern'*/
00405 /*  to match files based on glob expressions.*/
00406 /* */
00407 /*  Arguments:*/
00408 /*  patterns    List of glob expressions to match against.*/
00409 /*  filename    Name of the file to match against the patterns.*/
00410 /*  Results:*/
00411 /*  interesting A boolean flag. Set to true if the file*/
00412 /*          matches at least one of the patterns.*/
00413 
00414 ret  ::fileutil::FindGlob (type patterns , type filename) {
00415     foreach p $patterns {
00416     if {[string match $p $filename]} {
00417         return 1
00418     }
00419     }
00420     return 0
00421 }
00422 
00423 /*  ::fileutil::stripPwd --*/
00424 /* */
00425 /*  If the specified path references is a path in [pwd] (or [pwd] itself) it*/
00426 /*  is made relative to [pwd]. Otherwise it is left unchanged.*/
00427 /*  In the case of [pwd] itself the result is the string '.'.*/
00428 /* */
00429 /*  Arguments:*/
00430 /*  path        path to modify*/
00431 /* */
00432 /*  Results:*/
00433 /*  path        The (possibly) modified path.*/
00434 
00435 ret  ::fileutil::stripPwd (type path) {
00436 
00437     # [file split] is used to generate a canonical form for both
00438     # paths, for easy comparison, and also one which is easy to modify
00439     # using list commands.
00440 
00441     set pwd [pwd]
00442     if {[string equal $pwd $path]} {
00443     return "."
00444     }
00445 
00446     set pwd   [file split $pwd]
00447     set npath [file split $path]
00448 
00449     if {[string match ${pwd}* $npath]} {
00450     set path [eval [linsert [lrange $npath [llength $pwd] end] 0 file join ]]
00451     }
00452     return $path
00453 }
00454 
00455 /*  ::fileutil::stripN --*/
00456 /* */
00457 /*  Removes N elements from the beginning of the path.*/
00458 /* */
00459 /*  Arguments:*/
00460 /*  path        path to modify*/
00461 /*  n       number of elements to strip*/
00462 /* */
00463 /*  Results:*/
00464 /*  path        The modified path*/
00465 
00466 ret  ::fileutil::stripN (type path , type n) {
00467     set path [file split $path]
00468     if {$n >= [llength $path]} {
00469     return {}
00470     } else {
00471     return [eval [linsert [lrange $path $n end] 0 file join]]
00472     }
00473 }
00474 
00475 /*  ::fileutil::stripPath --*/
00476 /* */
00477 /*  If the specified path references/is a path in prefix (or prefix itself) it*/
00478 /*  is made relative to prefix. Otherwise it is left unchanged.*/
00479 /*  In the case of it being prefix itself the result is the string '.'.*/
00480 /* */
00481 /*  Arguments:*/
00482 /*  prefix      prefix to strip from the path.*/
00483 /*  path        path to modify*/
00484 /* */
00485 /*  Results:*/
00486 /*  path        The (possibly) modified path.*/
00487 
00488 ret  ::fileutil::stripPath (type prefix , type path) {
00489     # [file split] is used to generate a canonical form for both
00490     # paths, for easy comparison, and also one which is easy to modify
00491     # using list commands.
00492 
00493     if {[string equal $prefix $path]} {
00494     return "."
00495     }
00496 
00497     set prefix [file split $prefix]
00498     set npath  [file split $path]
00499 
00500     if {[string match ${prefix}* $npath]} {
00501     set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]]
00502     }
00503     return $path
00504 }
00505 
00506 /*  ::fileutil::jail --*/
00507 /* */
00508 /*  Ensures that the input path 'filename' stays within the the*/
00509 /*  directory 'jail'. In this way it preventsuser-supplied paths*/
00510 /*  from escaping the jail.*/
00511 /* */
00512 /*  Arguments:*/
00513 /*  jail        The path to the directory the other must*/
00514 /*          not escape from.*/
00515 /*  filename    The path to prevent from escaping.*/
00516 /* */
00517 /*  Results:*/
00518 /*  path        The (possibly) modified path surely within*/
00519 /*          the confines of the jail.*/
00520 
00521 ret  fileutil::jail (type jail , type filename) {
00522     if {![string equal [file pathtype $filename]  "relative"]} {
00523     # Although the path to check is absolute (or volumerelative on
00524     # windows) we cannot perform a simple prefix check to see if
00525     # the path is inside the jail or not. We have to normalize
00526     # both path and jail and then we can check. If the path is
00527     # outside we make the original path relative and prefix it
00528     # with the original jail. We do make the jail pseudo-absolute
00529     # by prefixing it with the current working directory for that.
00530 
00531     # Normalized jail. Fully resolved sym links, if any. Our main
00532     # complication is that normalize does not resolve symlinks in the
00533     # last component of the path given to it, so we add a bogus
00534     # component, resolve, and then strip it off again. That is why the
00535     # code is so large and long.
00536 
00537     set njail [eval [list file join] [lrange [file split \
00538         [Normalize [file join $jail __dummy__]]] 0 end-1]]
00539 
00540     # Normalize filename. Fully resolved sym links, if
00541     # any. S.a. for an explanation of the complication.
00542 
00543     set nfile [eval [list file join] [lrange [file split \
00544         [Normalize [file join $filename __dummy__]]] 0 end-1]]
00545 
00546     if {[string match ${njail}* $nfile]} {
00547         return $filename
00548     }
00549 
00550     # Outside the jail, put it inside. ... We normalize the input
00551     # path lexically for this, to prevent escapes still lurking in
00552     # the original path. (We cannot use the normalized path,
00553     # symlinks may have bent it out of shape in unrecognizable ways.
00554 
00555     return [eval [linsert [lrange [file split \
00556         [LexNormalize $filename]] 1 end] 0 file join [pwd] $jail]]
00557     } else {
00558     # The path is relative, consider it as outside
00559     # implicitly. Normalize it lexically! to prevent escapes, then
00560     # put the jail in front, use PWD to ensure absoluteness.
00561 
00562     return [eval [linsert [file split [LexNormalize $filename]] 0 \
00563         file join [pwd] $jail]]
00564     }
00565 }
00566 
00567 
00568 /*  ::fileutil::test --*/
00569 /* */
00570 /*  Simple API to testing various properties of*/
00571 /*  a path (read, write, file/dir, existence)*/
00572 /* */
00573 /*  Arguments:*/
00574 /*  path    path to test*/
00575 /*  codes   names of the properties to test*/
00576 /*  msgvar  Name of variable to leave an error*/
00577 /*      message in. Optional.*/
00578 /*  label   Label for error message, optional*/
00579 /* */
00580 /*  Results:*/
00581 /*  ok  boolean flag, set if the path passes*/
00582 /*      all tests.*/
00583 
00584 namespace ::fileutil {
00585     variable  test
00586     array  test =  {
00587     read   {readable    {Read access is denied}}
00588     write  {writable    {Write access is denied}}
00589     exec   {executable  {Is not executable}}
00590     exists {exists      {Does not exist}}
00591     file   {isfile      {Is not a file}}
00592     dir    {isdirectory {Is not a directory}}
00593     }
00594 }
00595 
00596 ret  ::fileutil::test (type path , type codes , optional msgvar ={) {label {}}} {
00597     variable test
00598 
00599     if {[string equal $msgvar ""]} {
00600      msg =  ""
00601     } else {
00602     upvar 1 $msgvar msg
00603     }
00604 
00605     if {![string equal $label ""]} {append label { }}
00606 
00607     if {![regexp {^(read|write|exec|exists|file|dir)} $codes]} {
00608     /*  Translate single characters into proper codes*/
00609      codes =  [string map {
00610         r read w write e exists x exec f file d dir
00611     } [split $codes {}]]
00612     }
00613 
00614     foreach c $codes {
00615     foreach {cmd text} $test($c) break
00616     if {![file $cmd $path]} {
00617          msg =  "$label\"$path\": $text"
00618         return 0
00619     }
00620     }
00621 
00622     return 1
00623 }
00624 
00625 /*  ::fileutil::cat --*/
00626 /* */
00627 /*  Tcl implementation of the UNIX "cat" command.  Returns the contents*/
00628 /*  of the specified files.*/
00629 /* */
00630 /*  Arguments:*/
00631 /*  args    names of the files to read, interspersed with options*/
00632 /*      to set encodings, translations, or eofchar.*/
00633 /* */
00634 /*  Results:*/
00635 /*  data    data read from the file.*/
00636 
00637 ret  ::fileutil::cat (type args) {
00638     # Syntax: (?options? file)+
00639     # options = -encoding    ENC
00640     #         | -translation TRA
00641     #         | -eofchar     ECH
00642     #         | --
00643 
00644     if {![llength $args]} {
00645     # Argument processing stopped with arguments missing.
00646     return -code error \
00647         "wrong#args: should be\
00648         [lindex [info level 0] 0] ?-eofchar|-translation|-encoding arg?+ file ..."
00649     }
00650 
00651     # We go through the arguments using foreach and keeping track of
00652     # the index we are at. We do not shift the arguments out to the
00653     # left. That is inherently quadratic, copying everything down.
00654 
00655     set opts {}
00656     set mode maybeopt
00657     set channels {}
00658 
00659     foreach a $args {
00660     if {[string equal $mode optarg]} {
00661         lappend opts $a
00662         set mode maybeopt
00663         continue
00664     } elseif {[string equal $mode maybeopt]} {
00665         if {[string match -* $a]} {
00666         switch -exact -- $a {
00667             -encoding -
00668             -translation -
00669             -eofchar {
00670             lappend opts $a
00671             set mode optarg
00672             continue
00673             }
00674             -- {
00675             set mode file
00676             continue
00677             }
00678             default {
00679             return -code error \
00680                 "Bad option \"$a\",\
00681                 expected one of\
00682                 -encoding, -eofchar,\
00683                 or -translation"
00684             }
00685         }
00686         }
00687         # Not an option, but a file. Change mode and fall through.
00688         set mode file
00689     }
00690     # Process file arguments
00691 
00692     if {[string equal $a -]} {
00693         # Stdin reference is special.
00694 
00695         # Test that the current options are all ok.
00696         # For stdin we have to avoid closing it.
00697 
00698         set old [fconfigure stdin]
00699         set fail [catch {
00700         SetOptions stdin $opts
00701         } msg] ; # {}
00702         SetOptions stdin $old
00703 
00704         if {$fail} {
00705         return -code error $msg
00706         }
00707 
00708         lappend channels [list $a $opts 0]
00709     } else {
00710         if {![file exists $a]} {
00711         return -code error "Cannot read file \"$a\", does not exist"
00712         } elseif {![file isfile $a]} {
00713         return -code error "Cannot read file \"$a\", is not a file"
00714         } elseif {![file readable $a]} {
00715         return -code error "Cannot read file \"$a\", read access is denied"
00716         }
00717 
00718         # Test that the current options are all ok.
00719         set c [open $a r]
00720         set fail [catch {
00721         SetOptions $c $opts
00722         } msg] ; # {}
00723         close $c
00724         if {$fail} {
00725         return -code error $msg
00726         }
00727 
00728         lappend channels [list $a $opts [file size $a]]
00729     }
00730 
00731     # We may have more options and files coming after.
00732     set mode maybeopt
00733     }
00734 
00735     if {![string equal $mode maybeopt]} {
00736     # Argument processing stopped with arguments missing.
00737     return -code error \
00738         "wrong#args: should be\
00739         [lindex [info level 0] 0] ?-eofchar|-translation|-encoding arg?+ file ..."
00740     }
00741 
00742     set data ""
00743     foreach c $channels {
00744     foreach {fname opts size} $c break
00745 
00746     if {[string equal $fname -]} {
00747         set old [fconfigure stdin]
00748         SetOptions stdin $opts
00749         append data [read stdin]
00750         SetOptions stdin $old
00751         continue
00752     }
00753 
00754     set c [open $fname r]
00755     SetOptions $c $opts
00756 
00757     if {$size > 0} {
00758         # Used the [file size] command to get the size, which
00759         # preallocates memory, rather than trying to grow it as
00760         # the read progresses.
00761         append data [read $c $size]
00762     } else {
00763         # if the file has zero bytes it is either empty, or
00764         # something where [file size] reports 0 but the file
00765         # actually has data (like the files in the /proc
00766         # filesystem on Linux).
00767         append data [read $c]
00768     }
00769     close $c
00770     }
00771 
00772     return $data
00773 }
00774 
00775 /*  ::fileutil::writeFile --*/
00776 /* */
00777 /*  Write the specified data into the named file,*/
00778 /*  creating it if necessary.*/
00779 /* */
00780 /*  Arguments:*/
00781 /*  options...  Options and arguments.*/
00782 /*  filename    Path to the file to write.*/
00783 /*  data        The data to write into the file*/
00784 /* */
00785 /*  Results:*/
00786 /*  None.*/
00787 
00788 ret  ::fileutil::writeFile (type args) {
00789     # Syntax: ?options? file data
00790     # options = -encoding    ENC
00791     #         | -translation TRA
00792     #         | -eofchar     ECH
00793     #         | --
00794 
00795     Spec Writable $args opts fname data
00796 
00797     # Now perform the requested operation.
00798 
00799     file mkdir [file dirname $fname]
00800     set              c [open $fname w]
00801     SetOptions      $c $opts
00802     puts -nonewline $c $data
00803     close           $c
00804     return
00805 }
00806 
00807 /*  ::fileutil::appendToFile --*/
00808 /* */
00809 /*  Append the specified data at the end of the named file,*/
00810 /*  creating it if necessary.*/
00811 /* */
00812 /*  Arguments:*/
00813 /*  options...  Options and arguments.*/
00814 /*  filename    Path to the file to extend.*/
00815 /*  data        The data to extend the file with.*/
00816 /* */
00817 /*  Results:*/
00818 /*  None.*/
00819 
00820 ret  ::fileutil::appendToFile (type args) {
00821     # Syntax: ?options? file data
00822     # options = -encoding    ENC
00823     #         | -translation TRA
00824     #         | -eofchar     ECH
00825     #         | --
00826 
00827     Spec Writable $args opts fname data
00828 
00829     # Now perform the requested operation.
00830 
00831     file mkdir [file dirname $fname]
00832     set              c [open $fname a]
00833     SetOptions      $c $opts
00834     set at    [tell $c]
00835     puts -nonewline $c $data
00836     close           $c
00837     return $at
00838 }
00839 
00840 /*  ::fileutil::insertIntoFile --*/
00841 /* */
00842 /*  Insert the specified data into the named file,*/
00843 /*  creating it if necessary, at the given locaton.*/
00844 /* */
00845 /*  Arguments:*/
00846 /*  options...  Options and arguments.*/
00847 /*  filename    Path to the file to extend.*/
00848 /*  data        The data to extend the file with.*/
00849 /* */
00850 /*  Results:*/
00851 /*  None.*/
00852 
00853 ret  ::fileutil::insertIntoFile (type args) {
00854 
00855     # Syntax: ?options? file at data
00856     # options = -encoding    ENC
00857     #         | -translation TRA
00858     #         | -eofchar     ECH
00859     #         | --
00860 
00861     Spec ReadWritable $args opts fname at data
00862 
00863     set max [file size $fname]
00864     CheckLocation $at $max insertion
00865 
00866     if {[string length $data] == 0} {
00867     # Another degenerate case, inserting nothing.
00868     # Leave the file well enough alone.
00869     return
00870     }
00871 
00872     foreach {c o t} [Open2 $fname $opts] break
00873 
00874     # The degenerate cases of both appending and insertion at the
00875     # beginning of the file allow more optimized implementations of
00876     # the operation.
00877 
00878     if {$at == 0} {
00879     puts -nonewline    $o $data
00880     fcopy           $c $o
00881     } elseif {$at == $max} {
00882     fcopy           $c $o
00883     puts -nonewline    $o $data
00884     } else {
00885     fcopy           $c $o -size $at
00886     puts -nonewline    $o $data
00887     fcopy           $c $o
00888     }
00889 
00890     Close2 $fname $t $c $o
00891     return
00892 }
00893 
00894 /*  ::fileutil::removeFromFile --*/
00895 /* */
00896 /*  Remove n characters from the named file,*/
00897 /*  starting at the given locaton.*/
00898 /* */
00899 /*  Arguments:*/
00900 /*  options...  Options and arguments.*/
00901 /*  filename    Path to the file to extend.*/
00902 /*  at      Location to start the removal from.*/
00903 /*  n       Number of characters to remove.*/
00904 /* */
00905 /*  Results:*/
00906 /*  None.*/
00907 
00908 ret  ::fileutil::removeFromFile (type args) {
00909 
00910     # Syntax: ?options? file at n
00911     # options = -encoding    ENC
00912     #         | -translation TRA
00913     #         | -eofchar     ECH
00914     #         | --
00915 
00916     Spec ReadWritable $args opts fname at n
00917 
00918     set max [file size $fname]
00919     CheckLocation    $at $max removal
00920     CheckLength   $n $at $max removal
00921 
00922     if {$n == 0} {
00923     # Another degenerate case, removing nothing.
00924     # Leave the file well enough alone.
00925     return
00926     }
00927 
00928     foreach {c o t} [Open2 $fname $opts] break
00929 
00930     # The degenerate cases of both removal from the beginning or end
00931     # of the file allow more optimized implementations of the
00932     # operation.
00933 
00934     if {$at == 0} {
00935     seek  $c    $n current
00936     fcopy $c $o
00937     } elseif {($at + $n) == $max} {
00938     fcopy $c $o -size $at
00939     # Nothing further to copy.
00940     } else {
00941     fcopy $c $o -size $at
00942     seek  $c    $n current
00943     fcopy $c $o
00944     }
00945 
00946     Close2 $fname $t $c $o
00947     return
00948 }
00949 
00950 /*  ::fileutil::replaceInFile --*/
00951 /* */
00952 /*  Remove n characters from the named file,*/
00953 /*  starting at the given locaton, and replace*/
00954 /*  it with the given data.*/
00955 /* */
00956 /*  Arguments:*/
00957 /*  options...  Options and arguments.*/
00958 /*  filename    Path to the file to extend.*/
00959 /*  at      Location to start the removal from.*/
00960 /*  n       Number of characters to remove.*/
00961 /*  data        The replacement data.*/
00962 /* */
00963 /*  Results:*/
00964 /*  None.*/
00965 
00966 ret  ::fileutil::replaceInFile (type args) {
00967 
00968     # Syntax: ?options? file at n data
00969     # options = -encoding    ENC
00970     #         | -translation TRA
00971     #         | -eofchar     ECH
00972     #         | --
00973 
00974     Spec ReadWritable $args opts fname at n data
00975 
00976     set max [file size $fname]
00977     CheckLocation    $at $max replacement
00978     CheckLength   $n $at $max replacement
00979 
00980     if {
00981     ($n == 0) &&
00982     ([string length $data] == 0)
00983     } {
00984     # Another degenerate case, replacing nothing with
00985     # nothing. Leave the file well enough alone.
00986     return
00987     }
00988 
00989     foreach {c o t} [Open2 $fname $opts] break
00990 
00991     # Check for degenerate cases and handle them separately,
00992     # i.e. strip the no-op parts out of the general implementation.
00993 
00994     if {$at == 0} {
00995     if {$n == 0} {
00996         # Insertion instead of replacement.
00997 
00998         puts -nonewline    $o $data
00999         fcopy           $c $o
01000 
01001     } elseif {[string length $data] == 0} {
01002         # Removal instead of replacement.
01003 
01004         seek  $c    $n current
01005         fcopy $c $o
01006 
01007     } else {
01008         # General replacement at front.
01009 
01010         seek         $c    $n current
01011         puts -nonewline $o $data
01012         fcopy        $c $o
01013     }
01014     } elseif {($at + $n) == $max} {
01015     if {$n == 0} {
01016         # Appending instead of replacement
01017 
01018         fcopy           $c $o
01019         puts -nonewline    $o $data
01020 
01021     } elseif {[string length $data] == 0} {
01022         # Truncating instead of replacement
01023 
01024         fcopy $c $o -size $at
01025         # Nothing further to copy.
01026 
01027     } else {
01028         # General replacement at end
01029 
01030         fcopy        $c $o -size $at
01031         puts -nonewline $o $data
01032     }
01033     } else {
01034     if {$n == 0} {
01035         # General insertion.
01036 
01037         fcopy           $c $o -size $at
01038         puts -nonewline    $o $data
01039         fcopy           $c $o
01040 
01041     } elseif {[string length $data] == 0} {
01042         # General removal.
01043 
01044         fcopy $c $o -size $at
01045         seek  $c    $n current
01046         fcopy $c $o
01047 
01048     } else {
01049         # General replacement.
01050 
01051         fcopy        $c $o -size $at
01052         seek         $c    $n current
01053         puts -nonewline $o $data
01054         fcopy        $c $o
01055     }
01056     }
01057 
01058     Close2 $fname $t $c $o
01059     return
01060 }
01061 
01062 /*  ::fileutil::updateInPlace --*/
01063 /* */
01064 /*  Run command prefix on the contents of the*/
01065 /*  file and replace them with the result of*/
01066 /*  the command.*/
01067 /* */
01068 /*  Arguments:*/
01069 /*  options...  Options and arguments.*/
01070 /*  filename    Path to the file to extend.*/
01071 /*  cmd     Command prefix to run.*/
01072 /* */
01073 /*  Results:*/
01074 /*  None.*/
01075 
01076 ret  ::fileutil::updateInPlace (type args) {
01077     # Syntax: ?options? file cmd
01078     # options = -encoding    ENC
01079     #         | -translation TRA
01080     #         | -eofchar     ECH
01081     #         | --
01082 
01083     Spec ReadWritable $args opts fname cmd
01084 
01085     # readFile/cat inlined ...
01086 
01087     set             c [open $fname r]
01088     SetOptions     $c $opts
01089     set data [read $c]
01090     close          $c
01091 
01092     # Transformation. Abort and do not modify the target file if an
01093     # error was raised during this step.
01094 
01095     lappend cmd $data
01096     set code [catch {uplevel 1 $cmd} res]
01097     if {$code} {
01098     return -code $code $res
01099     }
01100 
01101     # writeFile inlined, with careful preservation of old contents
01102     # until we are sure that the write was ok.
01103 
01104     if {[catch {
01105     file rename -force $fname ${fname}.bak
01106 
01107     set              o [open $fname w]
01108     SetOptions      $o $opts
01109     puts -nonewline $o $res
01110     close           $o
01111 
01112     file delete -force ${fname}.bak
01113     } msg]} {
01114     if {[file exists ${fname}.bak]} {
01115         catch {
01116         file rename -force ${fname}.bak $fname
01117         }
01118         return -code error $msg
01119     }
01120     }
01121     return
01122 }
01123 
01124 ret  ::fileutil::Writable (type fname , type mv) {
01125     upvar 1 $mv msg
01126     if {[file exists $fname]} {
01127     if {![file isfile $fname]} {
01128         set msg "Cannot use file \"$fname\", is not a file"
01129         return 0
01130     } elseif {![file writable $fname]} {
01131         set msg "Cannot use file \"$fname\", write access is denied"
01132         return 0
01133     }
01134     }
01135     return 1
01136 }
01137 
01138 ret  ::fileutil::ReadWritable (type fname , type mv) {
01139     upvar 1 $mv msg
01140     if {![file exists $fname]} {
01141     set msg "Cannot use file \"$fname\", does not exist"
01142     return 0
01143     } elseif {![file isfile $fname]} {
01144     set msg "Cannot use file \"$fname\", is not a file"
01145     return 0
01146     } elseif {![file writable $fname]} {
01147     set msg "Cannot use file \"$fname\", write access is denied"
01148     return 0
01149     } elseif {![file readable $fname]} {
01150     set msg "Cannot use file \"$fname\", read access is denied"
01151     return 0
01152     }
01153     return 1
01154 }
01155 
01156 ret  ::fileutil::Spec (type check , type alist , type ov , type fv , type args) {
01157     upvar 1 $ov opts $fv fname
01158 
01159     set  n [llength $args] ; # Num more args
01160     incr n                 ; # Count path as well
01161 
01162     set opts {}
01163     set mode maybeopt
01164 
01165     set at 0
01166     foreach a $alist {
01167     if {[string equal $mode optarg]} {
01168         lappend opts $a
01169         set mode maybeopt
01170         incr at
01171         continue
01172     } elseif {[string equal $mode maybeopt]} {
01173         if {[string match -* $a]} {
01174         switch -exact -- $a {
01175             -encoding -
01176             -translation -
01177             -eofchar {
01178             lappend opts $a
01179             set mode optarg
01180             incr at
01181             continue
01182             }
01183             -- {
01184             # Stop processing.
01185             incr at
01186             break
01187             }
01188             default {
01189             return -code error \
01190                 "Bad option \"$a\",\
01191                 expected one of\
01192                 -encoding, -eofchar,\
01193                 or -translation"
01194             }
01195         }
01196         }
01197         # Not an option, but a file.
01198         # Stop processing.
01199         break
01200     }
01201     }
01202 
01203     if {([llength $alist] - $at) != $n} {
01204     # Argument processing stopped with arguments missing, or too
01205     # many
01206     return -code error \
01207         "wrong#args: should be\
01208         [lindex [info level 1] 0] ?-eofchar|-translation|-encoding arg? file $args"
01209     }
01210 
01211     set fname [lindex $alist $at]
01212     incr at
01213     foreach \
01214         var $args \
01215         val [lrange $alist $at end] {
01216     upvar 1 $var A
01217     set A $val
01218     }
01219 
01220     # Check given path ...
01221 
01222     if {![eval [linsert $check end $a msg]]} {
01223     return -code error $msg
01224     }
01225 
01226     return
01227 }
01228 
01229 ret  ::fileutil::Open2 (type fname , type opts) {
01230     set c [open $fname r]
01231     set t [tempfile]
01232     set o [open $t     w]
01233 
01234     SetOptions $c $opts
01235     SetOptions $o $opts
01236 
01237     return [list $c $o $t]
01238 }
01239 
01240 ret  ::fileutil::Close2 (type f , type temp , type in , type out) {
01241     close $in
01242     close $out
01243 
01244     file copy   -force $f ${f}.bak
01245     file rename -force $temp $f
01246     file delete -force ${f}.bak
01247     return
01248 }
01249 
01250 ret  ::fileutil::SetOptions (type c , type opts) {
01251     if {![llength $opts]} return
01252     eval [linsert $opts 0 fconfigure $c]
01253     return
01254 }
01255 
01256 ret  ::fileutil::CheckLocation (type at , type max , type label) {
01257     if {![string is integer -strict $at]} {
01258     return -code error \
01259         "Expected integer but got \"$at\""
01260     } elseif {$at < 0} {
01261     return -code error \
01262         "Bad $label point $at, before start of data"
01263     } elseif {$at > $max} {
01264     return -code error \
01265         "Bad $label point $at, behind end of data"
01266     }
01267 }
01268 
01269 ret  ::fileutil::CheckLength (type n , type at , type max , type label) {
01270     if {![string is integer -strict $n]} {
01271     return -code error \
01272         "Expected integer but got \"$n\""
01273     } elseif {$n < 0} {
01274     return -code error \
01275         "Bad $label size $n"
01276     } elseif {($at + $n) > $max} {
01277     return -code error \
01278         "Bad $label size $n, going behind end of data"
01279     }
01280 }
01281 
01282 /*  ::fileutil::foreachLine --*/
01283 /* */
01284 /*  Executes a script for every line in a file.*/
01285 /* */
01286 /*  Arguments:*/
01287 /*  var     name of the variable to contain the lines*/
01288 /*  filename    name of the file to read.*/
01289 /*  cmd     The script to execute.*/
01290 /* */
01291 /*  Results:*/
01292 /*  None.*/
01293 
01294 ret  ::fileutil::foreachLine (type var , type filename , type cmd) {
01295     upvar 1 $var line
01296     set fp [open $filename r]
01297 
01298     # -future- Use try/eval from tcllib/control
01299     catch {
01300     set code 0
01301     set result {}
01302     while {[gets $fp line] >= 0} {
01303         set code [catch {uplevel 1 $cmd} result]
01304         if {($code != 0) && ($code != 4)} {break}
01305     }
01306     }
01307     close $fp
01308 
01309     if {($code == 0) || ($code == 3) || ($code == 4)} {
01310         return $result
01311     }
01312     if {$code == 1} {
01313         global errorCode errorInfo
01314         return \
01315         -code      $code      \
01316         -errorcode $errorCode \
01317         -errorinfo $errorInfo \
01318         $result
01319     }
01320     return -code $code $result
01321 }
01322 
01323 /*  ::fileutil::touch --*/
01324 /* */
01325 /*  Tcl implementation of the UNIX "touch" command.*/
01326 /* */
01327 /*  touch [-a] [-m] [-c] [-r ref_file] [-t time] filename ...*/
01328 /* */
01329 /*  Arguments:*/
01330 /*  -a      change the access time only, unless -m also specified*/
01331 /*  -m      change the modification time only, unless -a also specified*/
01332 /*  -c      silently prevent creating a file if it did not previously exist*/
01333 /*  -r ref_file use the ref_file's time instead of the current time*/
01334 /*  -t time     use the specified time instead of the current time*/
01335 /*          ("time" is an integer clock value, like [clock seconds])*/
01336 /*  filename ...    the files to modify*/
01337 /* */
01338 /*  Results*/
01339 /*  None.*/
01340 /* */
01341 /*  Errors:*/
01342 /*  Both of "-r" and "-t" cannot be specified.*/
01343 
01344 if {[package vsatisfies [package provide Tcl] 8.3]} {
01345     namespace ::fileutil {
01346     namespace export touch
01347     }
01348 
01349     ret  ::fileutil::touch (type args) {
01350         # Don't bother catching errors, just let them propagate up
01351         
01352         set options {
01353             {a          "set the atime only"}
01354             {m          "set the mtime only"}
01355             {c          "do not create non-existant files"}
01356             {r.arg  ""  "use time from ref_file"}
01357             {t.arg  -1  "use specified time"}
01358         }
01359         set usage ": [lindex [info level 0] 0]\
01360                       \[options] filename ...\noptions:"
01361         array set params [::cmdline::getoptions args $options $usage]
01362         
01363         # process -a and -m options
01364         set set_atime [set set_mtime "true"]
01365         if {  $params(a) && ! $params(m)} {set set_mtime "false"}
01366         if {! $params(a) &&   $params(m)} {set set_atime "false"}
01367         
01368         # process -r and -t
01369         set has_t [expr {$params(t) != -1}]
01370         set has_r [expr {[string length $params(r)] > 0}]
01371         if {$has_t && $has_r} {
01372             return -code error "Cannot specify both -r and -t"
01373         } elseif {$has_t} {
01374             set atime [set mtime $params(t)]
01375         } elseif {$has_r} {
01376             file stat $params(r) stat
01377             set atime $stat(atime)
01378             set mtime $stat(mtime)
01379         } else {
01380             set atime [set mtime [clock seconds]]
01381         }
01382 
01383         # do it
01384         foreach filename $args {
01385             if {! [file exists $filename]} {
01386                 if {$params(c)} {continue}
01387                 close [open $filename w]
01388             }
01389             if {$set_atime} {file atime $filename $atime}
01390             if {$set_mtime} {file mtime $filename $mtime}
01391         }
01392         return
01393     }
01394 }
01395 
01396 /*  ::fileutil::fileType --*/
01397 /* */
01398 /*  Do some simple heuristics to determine file type.*/
01399 /* */
01400 /* */
01401 /*  Arguments:*/
01402 /*  filename        Name of the file to test.*/
01403 /* */
01404 /*  Results*/
01405 /*  type            Type of the file.  May be a list if multiple tests*/
01406 /*                        are positive (eg, a file could be both a directory */
01407 /*                        and a link).  In general, the list proceeds from most*/
01408 /*                        general (eg, binary) to most specific (eg, gif), so*/
01409 /*                        the full type for a GIF file would be */
01410 /*                        "binary graphic gif"*/
01411 /* */
01412 /*                        At present, the following types can be detected:*/
01413 /* */
01414 /*                        directory*/
01415 /*                        empty*/
01416 /*                        binary*/
01417 /*                        text*/
01418 /*                        script <interpreter>*/
01419 /*                        executable [elf, dos, ne, pe]*/
01420 /*                        binary graphic [gif, jpeg, png, tiff, bitmap, icns]*/
01421 /*                        ps, eps, pdf*/
01422 /*                        html*/
01423 /*                        xml <doctype>*/
01424 /*                        message pgp*/
01425 /*                        compressed [bzip, gzip, zip, tar]*/
01426 /*                        audio [mpeg, wave]*/
01427 /*                        gravity_wave_data_frame*/
01428 /*                        link*/
01429 /*          doctools, doctoc, and docidx documentation files.*/
01430 /*                   */
01431 
01432 ret  ::fileutil::fileType (type filename) {
01433     ;## existence test
01434     if { ! [ file exists $filename ] } {
01435         set err "file not found: '$filename'"
01436         return -code error $err
01437     }
01438     ;## directory test
01439     if { [ file isdirectory $filename ] } {
01440         set type directory
01441         if { ! [ catch {file readlink $filename} ] } {
01442             lappend type link
01443         }
01444         return $type
01445     }
01446     ;## empty file test
01447     if { ! [ file size $filename ] } {
01448         set type empty
01449         if { ! [ catch {file readlink $filename} ] } {
01450             lappend type link
01451         }
01452         return $type
01453     }
01454     set bin_rx {[\x00-\x08\x0b\x0e-\x1f]}
01455 
01456     if { [ catch {
01457         set fid [ open $filename r ]
01458         fconfigure $fid -translation binary
01459         fconfigure $fid -buffersize 1024
01460         fconfigure $fid -buffering full
01461         set test [ read $fid 1024 ]
01462         ::close $fid
01463     } err ] } {
01464         catch { ::close $fid }
01465         return -code error "::fileutil::fileType: $err"
01466     }
01467 
01468     if { [ regexp $bin_rx $test ] } {
01469         set type binary
01470         set binary 1
01471     } else {
01472         set type text
01473         set binary 0
01474     }
01475 
01476     # SF Tcllib bug [795585]. Allowing whitespace between #!
01477     # and path of script interpreter
01478 
01479     set metakit 0
01480 
01481     if { [ regexp {^\#\!\s*(\S+)} $test -> terp ] } {
01482         lappend type script $terp
01483     } elseif {[regexp "\\\[manpage_begin " $test]} {
01484     lappend type doctools
01485     } elseif {[regexp "\\\[toc_begin " $test]} {
01486     lappend type doctoc
01487     } elseif {[regexp "\\\[index_begin " $test]} {
01488     lappend type docidx
01489     } elseif { $binary && [ regexp {^[\x7F]ELF} $test ] } {
01490         lappend type executable elf
01491     } elseif { $binary && [string match "MZ*" $test] } {
01492         if { [scan [string index $test 24] %c] < 64 } {
01493             lappend type executable dos
01494         } else {
01495             binary scan [string range $test 60 61] s next
01496             set sig [string range $test $next [expr {$next + 1}]]
01497             if { $sig == "NE" || $sig == "PE" } {
01498                 lappend type executable [string tolower $sig]
01499             } else {
01500                 lappend type executable dos
01501             }
01502         }
01503     } elseif { $binary && [string match "BZh91AY\&SY*" $test] } {
01504         lappend type compressed bzip
01505     } elseif { $binary && [string match "\x1f\x8b*" $test] } {
01506         lappend type compressed gzip
01507     } elseif { $binary && [string range $test 257 262] == "ustar\x00" } {
01508         lappend type compressed tar
01509     } elseif { $binary && [string match "\x50\x4b\x03\x04*" $test] } {
01510         lappend type compressed zip
01511     } elseif { $binary && [string match "GIF*" $test] } {
01512         lappend type graphic gif
01513     } elseif { $binary && [string match "icns*" $test] } {
01514         lappend type graphic icns bigendian
01515     } elseif { $binary && [string match "snci*" $test] } {
01516         lappend type graphic icns smallendian
01517     } elseif { $binary && [string match "\x89PNG*" $test] } {
01518         lappend type graphic png
01519     } elseif { $binary && [string match "\xFF\xD8\xFF*" $test] } {
01520         binary scan $test x3H2x2a5 marker txt
01521         if { $marker == "e0" && $txt == "JFIF\x00" } {
01522             lappend type graphic jpeg jfif
01523         } elseif { $marker == "e1" && $txt == "Exif\x00" } {
01524             lappend type graphic jpeg exif
01525         }
01526     } elseif { $binary && [string match "MM\x00\**" $test] } {
01527         lappend type graphic tiff
01528     } elseif { $binary && [string match "BM*" $test] && [string range $test 6 9] == "\x00\x00\x00\x00" } {
01529         lappend type graphic bitmap
01530     } elseif { $binary && [string match "\%PDF\-*" $test] } {
01531         lappend type pdf
01532     } elseif { ! $binary && [string match -nocase "*<html>*" $test] } {
01533         lappend type html
01534     } elseif { [string match "\%\!PS\-*" $test] } {
01535        lappend type ps
01536        if { [string match "* EPSF\-*" $test] } {
01537            lappend type eps
01538        }
01539     } elseif { [string match -nocase "*<\?xml*" $test] } {
01540         lappend type xml
01541         if { [ regexp -nocase {\<\!DOCTYPE\s+(\S+)} $test -> doctype ] } {
01542             lappend type $doctype
01543         }
01544     } elseif { [string match {*BEGIN PGP MESSAGE*} $test] } {
01545         lappend type message pgp
01546     } elseif { $binary && [string match {IGWD*} $test] } {
01547         lappend type gravity_wave_data_frame
01548     } elseif {[string match "JL\x1a\x00*" $test] && ([file size $filename] >= 27)} {
01549     lappend type metakit smallendian
01550     set metakit 1
01551     } elseif {[string match "LJ\x1a\x00*" $test] && ([file size $filename] >= 27)} {
01552     lappend type metakit bigendian
01553     set metakit 1
01554     } elseif { $binary && [string match "RIFF*" $test] && [string range $test 8 11] == "WAVE" } {
01555         lappend type audio wave
01556     } elseif { $binary && [string match "ID3*" $test] } {
01557         lappend type audio mpeg
01558     } elseif { $binary && [binary scan $test S tmp] && [expr {$tmp & 0xFFE0}] == 65504 } {
01559         lappend type audio mpeg
01560     }
01561 
01562     # Additional checks of file contents at the end of the file,
01563     # possibly pointing into the middle too (attached metakit,
01564     # attached zip).
01565 
01566     ## Metakit File format: http://www.equi4.com/metakit/metakit-ff.html
01567     ## Metakit database attached ? ##
01568 
01569     if {!$metakit && ([file size $filename] >= 27)} {
01570     # The offsets in the footer are in always bigendian format
01571 
01572     if { [ catch {
01573         set fid [ open $filename r ]
01574         fconfigure $fid -translation binary
01575         fconfigure $fid -buffersize 1024
01576         fconfigure $fid -buffering full
01577         seek $fid -16 end
01578         set test [ read $fid 16 ]
01579         ::close $fid
01580     } err ] } {
01581         catch { ::close $fid }
01582         return -code error "::fileutil::fileType: $err"
01583     }
01584 
01585     binary scan $test IIII __ hdroffset __ __
01586     set hdroffset [expr {[file size $filename] - 16 - $hdroffset}]
01587 
01588     # Further checks iff the offset is actually inside the file.
01589 
01590     if {($hdroffset >= 0) && ($hdroffset < [file size $filename])} {
01591         # Seek to the specified location and try to match a metakit header
01592         # at this location.
01593 
01594         if { [ catch {
01595         set         fid [ open $filename r ]
01596         fconfigure $fid -translation binary
01597         fconfigure $fid -buffersize 1024
01598         fconfigure $fid -buffering full
01599         seek       $fid $hdroffset start
01600         set test [ read $fid 16 ]
01601         ::close $fid
01602         } err ] } {
01603         catch { ::close $fid }
01604         return -code error "::fileutil::fileType: $err"
01605         }
01606 
01607         if {[string match "JL\x1a\x00*" $test]} {
01608         lappend type attached metakit smallendian
01609         set metakit 1
01610         } elseif {[string match "LJ\x1a\x00*" $test]} {
01611         lappend type attached metakit bigendian
01612         set metakit 1
01613         }
01614     }
01615     }
01616 
01617     ## Zip File Format: http://zziplib.sourceforge.net/zzip-parse.html
01618     ## http://www.pkware.com/products/enterprise/white_papers/appnote.html
01619 
01620 
01621     ;## lastly, is it a link?
01622     if { ! [ catch {file readlink $filename} ] } {
01623         lappend type link
01624     }
01625     return $type
01626 }
01627 
01628 /*  ::fileutil::tempdir --*/
01629 /* */
01630 /*  Return the correct directory to use for temporary files.*/
01631 /*  Python attempts this sequence, which seems logical:*/
01632 /* */
01633 /*        1. The directory named by the `TMPDIR' environment variable.*/
01634 /* */
01635 /*        2. The directory named by the `TEMP' environment variable.*/
01636 /* */
01637 /*        3. The directory named by the `TMP' environment variable.*/
01638 /* */
01639 /*        4. A platform-specific location:*/
01640 /*             * On Macintosh, the `Temporary Items' folder.*/
01641 /* */
01642 /*             * On Windows, the directories `C:\\TEMP', `C:\\TMP',*/
01643 /*               `\\TEMP', and `\\TMP', in that order.*/
01644 /* */
01645 /*             * On all other platforms, the directories `/tmp',*/
01646 /*               `/var/tmp', and `/usr/tmp', in that order.*/
01647 /* */
01648 /*        5. As a last resort, the current working directory.*/
01649 /* */
01650 /*  The code here also does*/
01651 /* */
01652 /*  0. The directory set by invoking tempdir with an argument.*/
01653 /*     If this is present it is used exclusively.*/
01654 /* */
01655 /*  Arguments:*/
01656 /*  None.*/
01657 /* */
01658 /*  Side Effects:*/
01659 /*  None.*/
01660 /* */
01661 /*  Results:*/
01662 /*  The directory for temporary files.*/
01663 
01664 ret  ::fileutil::tempdir (type args) {
01665     if {[llength $args] > 1} {
01666     return -code error {wrong#args: should be "::fileutil::tempdir ?path?"}
01667     } elseif {[llength $args] == 1} {
01668     variable tempdir    [lindex $args 0]
01669     variable tempdirSet 1
01670     return
01671     }
01672     return [Normalize [TempDir]]
01673 }
01674 
01675 ret  ::fileutil::tempdirReset () {
01676     variable tempdir    {}
01677     variable tempdirSet 0
01678     return
01679 }
01680 
01681 ret  ::fileutil::TempDir () {
01682     global tcl_platform env
01683     variable tempdir
01684     variable tempdirSet
01685 
01686     set attempdirs [list]
01687     set problems   {}
01688 
01689     if {$tempdirSet} {
01690     lappend attempdirs $tempdir
01691     lappend problems {User/Application specified tempdir}
01692     } else {
01693     foreach tmp {TMPDIR TEMP TMP} {
01694         if { [info exists env($tmp)] } {
01695         lappend attempdirs $env($tmp)
01696         } else {
01697         lappend problems "No environment variable $tmp"
01698         }
01699     }
01700 
01701     switch $tcl_platform(platform) {
01702         windows {
01703         lappend attempdirs "C:\\TEMP" "C:\\TMP" "\\TEMP" "\\TMP"
01704         }
01705         macintosh {
01706         set tmpdir $env(TRASH_FOLDER)  ;# a better place?
01707         }
01708         default {
01709         lappend attempdirs \
01710             [file join / tmp] \
01711             [file join / var tmp] \
01712             [file join / usr tmp]
01713         }
01714     }
01715 
01716     lappend attempdirs [pwd]
01717     }
01718 
01719     foreach tmp $attempdirs {
01720     if { [file isdirectory $tmp] && [file writable $tmp] } {
01721         return $tmp
01722     } elseif { ![file isdirectory $tmp] } {
01723         lappend problems "Not a directory: $tmp"
01724     } else {
01725         lappend problems "Not writable: $tmp"
01726     }
01727     }
01728 
01729     # Fail if nothing worked.
01730     return -code error "Unable to determine a proper directory for temporary files\n[join $problems \n]"
01731 }
01732 
01733 namespace ::fileutil {
01734     variable tempdir    {}
01735     variable tempdirSet 0
01736 }
01737 
01738 /*  ::fileutil::tempfile --*/
01739 /* */
01740 /*    generate a temporary file name suitable for writing to*/
01741 /*    the file name will be unique, writable and will be in the */
01742 /*    appropriate system specific temp directory*/
01743 /*    Code taken from http://mini.net/tcl/772 attributed to*/
01744 /*     Igor Volobouev and anon.*/
01745 /* */
01746 /*  Arguments:*/
01747 /*    prefix     - a prefix for the filename, p*/
01748 /*  Results:*/
01749 /*    returns a file name*/
01750 /* */
01751 
01752 ret  ::fileutil::tempfile (optional prefix ={)} {
01753     return [Normalize [TempFile $prefix]]
01754 }
01755 
01756 proc ::fileutil::TempFile {prefix} {
01757     set tmpdir [tempdir]
01758 
01759     set chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
01760     set nrand_chars 10
01761     set maxtries 10
01762     set access [list RDWR CREAT EXCL TRUNC]
01763     set permission 0600
01764     set channel ""
01765     set checked_dir_writable 0
01766     set mypid [pid]
01767     for {set i 0} {$i < $maxtries} {incr i} {
01768      newname =  $prefix
01769     for { j =  0} {$j < $nrand_chars} {incr j} {
01770         append newname [string index $chars \
01771             [expr {int(rand()*62)}]]
01772     }
01773      newname =  [file join $tmpdir $newname]
01774     if {[file exists $newname]} {
01775         after 1
01776     } else {
01777         if {[catch {open $newname $access $permission} channel]} {
01778         if {!$checked_dir_writable} {
01779              dirname =  [file dirname $newname]
01780             if {![file writable $dirname]} {
01781             return -code error "Directory $dirname is not writable"
01782             }
01783              checked = _dir_writable 1
01784         }
01785         } else {
01786         /*  Success*/
01787         close $channel
01788         return $newname
01789         }
01790     }
01791     }
01792     if {[string compare $channel ""]} {
01793     return -code error "Failed to open a temporary file: $channel"
01794     } else {
01795     return -code error "Failed to find an unused temporary file name"
01796     }
01797 }
01798 
01799 /*  ::fileutil::install --*/
01800 /* */
01801 /*  Tcl version of the 'install' command, which copies files from*/
01802 /*  one places to another and also optionally sets some attributes*/
01803 /*  such as group, owner, and permissions.*/
01804 /* */
01805 /*  Arguments:*/
01806 /*  -m      Change the file permissions to the specified*/
01807 /*                        value.  Valid arguments are those accepted by*/
01808 /*          file attributes -permissions*/
01809 /* */
01810 /*  Results:*/
01811 /*  None.*/
01812 
01813 /*  TODO - add options for group/owner manipulation.*/
01814 
01815 ret  ::fileutil::install (type args) {
01816     set options {
01817     {m.arg "" "Set permission mode"}
01818     }
01819     set usage ": [lindex [info level 0] 0]\
01820 \[options] source destination \noptions:"
01821     array set params [::cmdline::getoptions args $options $usage]
01822     # Args should now just be the source and destination.
01823     if { [llength $args] < 2 } {
01824     return -code error $usage
01825     }
01826     set src [lindex $args 0]
01827     set dst [lindex $args 1]
01828     file copy -force $src $dst
01829     if { $params(m) != "" } {
01830     set targets [::fileutil::find $dst]
01831     foreach fl $targets {
01832         file attributes $fl -permissions $params(m)
01833     }
01834     }
01835 }
01836 
01837 /*  ### ### ### ######### ######### #########*/
01838 
01839 ret  ::fileutil::LexNormalize (type sp) {
01840     set spx [file split $sp]
01841 
01842     # Resolution of embedded relative modifiers (., and ..).
01843 
01844     if {
01845     ([lsearch -exact $spx . ] < 0) &&
01846     ([lsearch -exact $spx ..] < 0)
01847     } {
01848     # Quick path out if there are no relative modifiers
01849     return $sp
01850     }
01851 
01852     set absolute [expr {![string equal [file pathtype $sp] relative]}]
01853     # A volumerelative path counts as absolute for our purposes.
01854 
01855     set sp $spx
01856     set np {}
01857     set noskip 1
01858 
01859     while {[llength $sp]} {
01860     set ele    [lindex $sp 0]
01861     set sp     [lrange $sp 1 end]
01862     set islast [expr {[llength $sp] == 0}]
01863 
01864     if {[string equal $ele ".."]} {
01865         if {
01866         ($absolute  && ([llength $np] >  1)) ||
01867         (!$absolute && ([llength $np] >= 1))
01868         } {
01869         # .. : Remove the previous element added to the
01870         # new path, if there actually is enough to remove.
01871         set np [lrange $np 0 end-1]
01872         }
01873     } elseif {[string equal $ele "."]} {
01874         # Ignore .'s, they stay at the current location
01875         continue
01876     } else {
01877         # A regular element.
01878         lappend np $ele
01879     }
01880     }
01881     if {[llength $np] > 0} {
01882     return [eval [linsert $np 0 file join]]
01883     # 8.5: return [file join {*}$np]
01884     }
01885     return {}
01886 }
01887 
01888 /*  ### ### ### ######### ######### #########*/
01889 /*  Forward compatibility. Some routines require path normalization,*/
01890 /*  something we have supported by the builtin 'file' only since Tcl*/
01891 /*  8.4. For versions of Tcl before that, to be supported by the*/
01892 /*  module, we implement a normalizer in Tcl itself. Slow, but working.*/
01893 
01894 if {[package vcompare [package provide Tcl] 8.4] < 0} {
01895     /*  Pre 8.4. We do not have 'file normalize'. We create an*/
01896     /*  approximation for it based on earlier commands.*/
01897 
01898     /*  ... Hm. This is lexical normalization. It does not resolve*/
01899     /*  symlinks in the path to their origin.*/
01900 
01901     ret  ::fileutil::Normalize (type sp) {
01902     set sp [file split $sp]
01903 
01904     # Conversion of the incoming path to absolute.
01905     if {[string equal [file pathtype [lindex $sp 0]] "relative"]} {
01906         set sp [file split [eval [list file join [pwd]] $sp]]
01907     }
01908 
01909     # Resolution of symlink components, and embedded relative
01910     # modifiers (., and ..).
01911 
01912     set np {}
01913     set noskip 1
01914     while {[llength $sp]} {
01915         set ele    [lindex $sp 0]
01916         set sp     [lrange $sp 1 end]
01917         set islast [expr {[llength $sp] == 0}]
01918 
01919         if {[string equal $ele ".."]} {
01920         if {[llength $np] > 1} {
01921             # .. : Remove the previous element added to the
01922             # new path, if there actually is enough to remove.
01923             set np [lrange $np 0 end-1]
01924         }
01925         } elseif {[string equal $ele "."]} {
01926         # Ignore .'s, they stay at the current location
01927         continue
01928         } else {
01929         # A regular element. If it is not the last component
01930         # then check if the combination is a symlink, and if
01931         # yes, resolve it.
01932 
01933         lappend np $ele
01934 
01935         if {!$islast && $noskip} {
01936             # The flag 'noskip' is technically not required,
01937             # just 'file exists'. However if a path P does not
01938             # exist, then all longer paths starting with P can
01939             # not exist either, and using the flag to store
01940             # this knowledge then saves us a number of
01941             # unnecessary stat calls. IOW this a performance
01942             # optimization.
01943 
01944             set p [eval file join $np]
01945             set noskip [file exists $p]
01946             if {$noskip} {
01947             if {[string equal link [file type $p]]} {
01948                 set dst [file readlink $p]
01949 
01950                 # We always push the destination in front of
01951                 # the source path (in expanded form). So that
01952                 # we handle .., .'s, and symlinks inside of
01953                 # this path as well. An absolute path clears
01954                 # the result, a relative one just removes the
01955                 # last, now resolved component.
01956 
01957                 set sp [eval [linsert [file split $dst] 0 linsert $sp 0]]
01958 
01959                 if {![string equal relative [file pathtype $dst]]} {
01960                 # Absolute|volrelative destination, clear
01961                 # result, we have to start over.
01962                 set np {}
01963                 } else {
01964                 # Relative link, just remove the resolved
01965                 # component again.
01966                 set np [lrange $np 0 end-1]
01967                 }
01968             }
01969             }
01970         }
01971         }
01972     }
01973     if {[llength $np] > 0} {
01974         return [eval file join $np]
01975     }
01976     return {}
01977     }
01978 } else {
01979     ret  ::fileutil::Normalize (type sp) {
01980     file normalize $sp
01981     }
01982 }
01983 
01984 /*  ::fileutil::relative --*/
01985 /* */
01986 /*  Taking two _directory_ paths, a base and a destination, computes the path*/
01987 /*  of the destination relative to the base.*/
01988 /* */
01989 /*  Arguments:*/
01990 /*  base    The path to make the destination relative to.*/
01991 /*  dst The destination path*/
01992 /* */
01993 /*  Results:*/
01994 /*  The path of the destination, relative to the base.*/
01995 
01996 ret  ::fileutil::relative (type base , type dst) {
01997     # Ensure that the link to directory 'dst' is properly done relative to
01998     # the directory 'base'.
01999 
02000     if {![string equal [file pathtype $base] [file pathtype $dst]]} {
02001     return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)"
02002     }
02003 
02004     set base [LexNormalize [file join [pwd] $base]]
02005     set dst  [LexNormalize [file join [pwd] $dst]]
02006 
02007     set save $dst
02008     set base [file split $base]
02009     set dst  [file split $dst]
02010 
02011     while {[string equal [lindex $dst 0] [lindex $base 0]]} {
02012     set dst  [lrange $dst  1 end]
02013     set base [lrange $base 1 end]
02014     if {![llength $dst]} {break}
02015     }
02016 
02017     set dstlen  [llength $dst]
02018     set baselen [llength $base]
02019 
02020     if {($dstlen == 0) && ($baselen == 0)} {
02021     # Cases:
02022     # (a) base == dst
02023 
02024     set dst .
02025     } else {
02026     # Cases:
02027     # (b) base is: base/sub = sub
02028     #     dst  is: base     = {}
02029 
02030     # (c) base is: base     = {}
02031     #     dst  is: base/sub = sub
02032 
02033     while {$baselen > 0} {
02034         set dst [linsert $dst 0 ..]
02035         incr baselen -1
02036     }
02037     # 8.5: set dst [file join {*}$dst]
02038     set dst [eval [linsert $dst 0 file join]]
02039     }
02040 
02041     return $dst
02042 }
02043 
02044 /*  ::fileutil::relativeUrl --*/
02045 /* */
02046 /*  Taking two _file_ paths, a base and a destination, computes the path*/
02047 /*  of the destination relative to the base, from the inside of the base.*/
02048 /* */
02049 /*  This is how a browser resolves relative links in a file, hence the*/
02050 /*  url in the command name.*/
02051 /* */
02052 /*  Arguments:*/
02053 /*  base    The file path to make the destination relative to.*/
02054 /*  dst The destination file path*/
02055 /* */
02056 /*  Results:*/
02057 /*  The path of the destination file, relative to the base file.*/
02058 
02059 ret  ::fileutil::relativeUrl (type base , type dst) {
02060     # Like 'relative', but for links from _inside_ a file to a
02061     # different file.
02062 
02063     if {![string equal [file pathtype $base] [file pathtype $dst]]} {
02064     return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)"
02065     }
02066 
02067     set base [LexNormalize [file join [pwd] $base]]
02068     set dst  [LexNormalize [file join [pwd] $dst]]
02069 
02070     set basedir [file dirname $base]
02071     set dstdir  [file dirname $dst]
02072 
02073     set dstdir  [relative $basedir $dstdir]
02074 
02075     # dstdir == '.' on input => dstdir output has trailing './'. Strip
02076     # this superfluous segment off.
02077 
02078     if {[string equal $dstdir "."]} {
02079     return [file tail $dst]
02080     } elseif {[string equal [file tail $dstdir] "."]} {
02081     return [file join [file dirname $dstdir] [file tail $dst]]
02082     } else {
02083     return [file join $dstdir [file tail $dst]]
02084     }
02085 }
02086 
02087 /*  ::fileutil::fullnormalize --*/
02088 /* */
02089 /*  Normalizes a path completely. I.e. a symlink in the last*/
02090 /*  element is resolved as well, not only symlinks in the higher*/
02091 /*  elements.*/
02092 /* */
02093 /*  Arguments:*/
02094 /*  path    The path to normalize*/
02095 /* */
02096 /*  Results:*/
02097 /*  The input path with all symlinks resolved.*/
02098 
02099 ret  ::fileutil::fullnormalize (type path) {
02100     # When encountering symlinks in a file copy operation Tcl copies
02101     # the link, not the contents of the file it references. There are
02102     # situations there this is not acceptable. For these this command
02103     # resolves all symbolic links in the path, including in the last
02104     # element of the path. A "file copy" using the return value of
02105     # this command copies an actual file, it will not encounter
02106     # symlinks.
02107 
02108     # BUG / WORKAROUND. Using the / instead of the join seems to work
02109     # around a bug in the path handling on windows which can break the
02110     # core 'file normalize' for symbolic links. This was exposed by
02111     # the find testsuite which could not reproduced outside. I believe
02112     # that there is some deep path bug in the core triggered under
02113     # special circumstances. Use of / likely forces a refresh through
02114     # the string rep and so avoids the problem with the path intrep.
02115 
02116     return [file dirname [Normalize $path/__dummy__]]
02117     #return [file dirname [Normalize [file join $path __dummy__]]]
02118 }
02119 

Generated on 21 Sep 2010 for Gui by  doxygen 1.6.1