traverse.tcl

Go to the documentation of this file.
00001 /*  traverse.tcl --*/
00002 /* */
00003 /*  Directory traversal.*/
00004 /* */
00005 /*  Copyright (c) 2006-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net>*/
00006 /* */
00007 /*  See the file "license.terms" for information on usage and redistribution*/
00008 /*  of this file, and for a DISCLAIMER OF ALL WARRANTIES.*/
00009 /*  */
00010 /*  RCS: @(#) $Id: traverse.tcl,v 1.4 2007/08/08 19:42:43 andreas_kupries Exp $*/
00011 
00012 package require Tcl 8.3
00013 package require snit    ; /*  OO core*/
00014 package require control ; /*  Helpers for control structures*/
00015 
00016 snit::type ::fileutil::traverse {
00017 
00018     /*  Incremental directory traversal.*/
00019 
00020     /*  API*/
00021     /*  create  %AUTO% basedirectory options... -> object*/
00022     /*  next    filevar                         -> boolean*/
00023     /*  foreach filevar script*/
00024     /*  files                                   -> list (path ...)*/
00025 
00026     /*  Options*/
00027     /*  -prefilter command-prefix*/
00028     /*  -filter    command-prefix*/
00029     /*  -errorcmd  command-prefix*/
00030 
00031     /*  Use cases*/
00032     /* */
00033     /*  (a) Basic incremental*/
00034     /*  - Create and configure a traversal object.*/
00035     /*  - Execute 'next' to retrieve one path at a time,*/
00036     /*    until the command returns False, signaling that*/
00037     /*    the iterator has exhausted the supply of paths.*/
00038     /*    (The path is stored in the named variable).*/
00039     /* */
00040     /*  The execution of 'next' can be done in a loop, or via event*/
00041     /*  processing.*/
00042 
00043     /*  (b) Basic loop*/
00044     /*  - Create and configure a traversal object.*/
00045     /*  - Run a script for each path, using 'foreach'.*/
00046     /*    This is a convenient standard wrapper around 'next'.*/
00047     /* */
00048     /*  The loop properly handles all possible Tcl result codes.*/
00049 
00050     /*  (c) Non-incremental, non-looping.*/
00051     /*  - Create and configure a traversal object.*/
00052     /*  - Retrieve a list of all paths via 'files'.*/
00053 
00054     /*  The -prefilter callback is executed for directories. Its result*/
00055     /*  determines if the traverser recurses into the directory or not.*/
00056     /*  The default is to always recurse into all directories. The call-*/
00057     /*  back is invoked with a single argument, the path of the*/
00058     /*  directory.*/
00059     /* */
00060     /*  The -filter callback is executed for all paths. Its result*/
00061     /*  determines if the current path is a valid result, and returned*/
00062     /*  by 'next'. The default is to accept all paths as valid. The*/
00063     /*  callback is invoked with a single argument, the path to check.*/
00064 
00065     /*  The -errorcmd callback is executed for all paths the traverser*/
00066     /*  has trouble with. Like being unable to cd into them, get their*/
00067     /*  status, etc. The default is to ignore any such problems. The*/
00068     /*  callback is invoked with a two arguments, the path for which the*/
00069     /*  error occured, and the error message. Errors thrown by the*/
00070     /*  filter callbacks are handled through this callback too. Errors*/
00071     /*  thrown by the error callback itself are not caught and ignored,*/
00072     /*  but allowed to pass to the caller, usually of 'next'.*/
00073 
00074     /*  Note: Low-level functionality, version and platform dependent is*/
00075     /*  implemented in procedures, and conditioally defined for optimal*/
00076     /*  use of features, etc. ...*/
00077 
00078     /*  Note: Traversal is done in depth-first pre-order.*/
00079 
00080     /*  Note: The options are handled only during*/
00081     /*  construction. Afterward they are read-only and attempts to*/
00082     /*  modify them will cause the system to throw errors.*/
00083 
00084     /*  ### ### ### ######### ######### #########*/
00085     /*  Implementation*/
00086 
00087     option -filter    -default {} -readonly 1
00088     option -prefilter -default {} -readonly 1
00089     option -errorcmd  -default {} -readonly 1
00090 
00091     constructor {basedir args} {
00092      _base =  $basedir
00093     $self configurelist $args
00094     return
00095     }
00096 
00097     ret  files () {
00098     set files {}
00099     $self foreach f {lappend files $f}
00100     return $files
00101     }
00102 
00103     ret  foreach (type fvar , type body) {
00104     upvar 1 $fvar currentfile
00105 
00106     # (Re-)initialize the traversal state on every call.
00107     $self Init
00108 
00109     while {[$self next currentfile]} {
00110         set code [catch {uplevel 1 $body} result]
00111 
00112         # decide what to do upon the return code:
00113         #
00114         #               0 - the body executed successfully
00115         #               1 - the body raised an error
00116         #               2 - the body invoked [return]
00117         #               3 - the body invoked [break]
00118         #               4 - the body invoked [continue]
00119         # everything else - return and pass on the results
00120         #
00121         switch -exact -- $code {
00122         0 {}
00123         1 {
00124             return -errorinfo [::control::ErrorInfoAsCaller uplevel foreach]  \
00125                 -errorcode $::errorCode -code error $result
00126         }
00127         3 {
00128             # FRINK: nocheck
00129             return
00130         }
00131         4 {}
00132         default {
00133             return -code $code $result
00134         }
00135         }
00136     }
00137     return
00138     }
00139 
00140     ret  next (type fvar) {
00141     upvar 1 $fvar currentfile
00142 
00143     # Initialize on first call.
00144     if {!$_init} {
00145         $self Init
00146     }
00147 
00148     # We (still) have valid paths in the result stack, return the
00149     # next one.
00150 
00151     if {[llength $_results]} {
00152         set top      [lindex   $_results end]
00153         set _results [lreplace $_results end end]
00154         set currentfile $top
00155         return 1
00156     }
00157 
00158     # Take the next directory waiting in the processing stack and
00159     # fill the result stack with all valid files and sub-
00160     # directories contained in it. Extend the processing queue
00161     # with all sub-directories not yet seen already (!circular
00162     # symlinks) and accepted by the prefilter. We stop iterating
00163     # when we either have no directories to process anymore, or
00164     # the result stack contains at least one path we can return.
00165 
00166     while {[llength $_pending]} {
00167         set top      [lindex   $_pending end]
00168         set _pending [lreplace $_pending end end]
00169 
00170         # Directory accessible? Skip if not.
00171         if {![ACCESS $top]} {
00172         Error $top "Inacessible directory"
00173         continue
00174         }
00175 
00176         # Expand the result stack with all files in the directory,
00177         # modulo filtering.
00178 
00179         foreach f [GLOBF $top] {
00180         if {![Valid $f]} continue
00181         lappend _results $f
00182         }
00183 
00184         # Expand the result stack with all sub-directories in the
00185         # directory, modulo filtering. Further expand the
00186         # processing stack with the same directories, if not seen
00187         # yet and modulo pre-filtering.
00188 
00189         foreach f [GLOBD $top] {
00190         if {
00191             [string equal [file tail $f]  "."] ||
00192             [string equal [file tail $f] ".."]
00193         } continue
00194 
00195         if {[Valid $f]} {
00196             lappend _results $f
00197         }
00198 
00199         set norm [fileutil::fullnormalize $f]
00200         if {[info exists _known($norm)]} continue
00201         set _known($norm) .
00202 
00203         if {[Recurse $f]} {
00204             lappend _pending $f
00205         }
00206         }
00207 
00208         # Stop expanding if we have paths to return.
00209 
00210         if {[llength $_results]} {
00211         set top    [lindex   $_results end]
00212         set _results [lreplace $_results end end]
00213         set currentfile $top
00214         return 1
00215         }
00216     }
00217 
00218     # Allow re-initialization with next call.
00219 
00220     set _init 0
00221     return 0
00222     }
00223 
00224     /*  ### ### ### ######### ######### #########*/
00225     /*  Traversal state*/
00226 
00227     /*  * Initialization flag. Checked in 'next', reset by next when no*/
00228     /*    more files are available. Set in 'Init'.*/
00229     /*  * Base directory (or file) to start the traversal from.*/
00230     /*  * Stack of prefiltered unknown directories waiting for*/
00231     /*    processing, i.e. expansion (TOP at end).*/
00232     /*  * Stack of valid paths waiting to be returned as results.*/
00233     /*  * Set of directories already visited (normalized paths), for*/
00234     /*    detection of circular symbolic links.*/
00235 
00236     variable _init         0  ; /*  Initialization flag.*/
00237     variable _base         {} ; /*  Base directory.*/
00238     variable _pending      {} ; /*  Processing stack.*/
00239     variable _results      {} ; /*  Result stack.*/
00240     variable _known -array {} ; /*  Seen paths.*/
00241 
00242     /*  ### ### ### ######### ######### #########*/
00243     /*  Internal helpers.*/
00244 
00245     ret  Init () {
00246     # Path ok as result?
00247     if {[Valid $_base]} {
00248         lappend _results $_base
00249     }
00250 
00251     # Expansion allowed by prefilter?
00252     if {[file isdirectory $_base] && [Recurse $_base]} {
00253         lappend _pending $_base
00254     }
00255 
00256     array unset _known *
00257 
00258     # System is set up now.
00259     set _init 1
00260     return
00261     }
00262 
00263     ret  Valid (type path) {
00264     upvar 1 options options
00265     if {![llength $options(-filter)]} {return 1}
00266     set code [catch {uplevel \#0 [linsert $options(-filter) end $path]} valid]
00267     if {!$code} {return $valid}
00268     Error $path $valid
00269     return 0
00270     }
00271 
00272     ret  Recurse (type path) {
00273     upvar 1 options options
00274     if {![llength $options(-prefilter)]} {return 1}
00275     set code [catch {uplevel \#0 [linsert $options(-prefilter) end $path]} valid]
00276     if {!$code} {return $valid}
00277     Error $path $valid
00278     return 0
00279     }
00280 
00281     ret  Error (type path , type msg) {
00282     upvar 1 options options
00283     if {![llength $options(-errorcmd)]} return
00284     uplevel \#0 [linsert $options(-errorcmd) end $path $msg]
00285     return
00286     }
00287 
00288     /** 
00289      * ### ### ### ######### ######### #########
00290  */
00291 }
00292 
00293 /*  ### ### ### ######### ######### #########*/
00294 /** 
00295  */
00296 
00297 /*  The next three helper commands for the traverser depend strongly on*/
00298 /*  the version of Tcl, and partially on the platform.*/
00299 
00300 /*  1. In Tcl 8.3 using -types f will return only true files, but not*/
00301 /*     links to files. This changed in 8.4+ where links to files are*/
00302 /*     returned as well. So for 8.3 we have to handle the links*/
00303 /*     separately (-types l) and also filter on our own.*/
00304 /*     Note that Windows file links are hard links which are reported by*/
00305 /*     -types f, but not -types l, so we can optimize that for the two*/
00306 /*     platforms.*/
00307 /* */
00308 /*  2. In Tcl 8.3 we also have a crashing bug in glob (SIGABRT, "stat on*/
00309 /*     a known file") when trying to perform 'glob -types {hidden f}' on*/
00310 /*     a directory without e'x'ecute permissions. We code around by*/
00311 /*     testing if we can cd into the directory (stat might return enough*/
00312 /*     information too (mode), but possibly also not portable).*/
00313 /* */
00314 /*     For Tcl 8.2 and 8.4+ glob simply delivers an empty result*/
00315 /*     (-nocomplain), without crashing. For them this command is defined*/
00316 /*     so that the bytecode compiler removes it from the bytecode.*/
00317 /* */
00318 /*     This bug made the ACCESS helper necessary.*/
00319 /*     We code around the problem by testing if we can cd into the*/
00320 /*     directory (stat might return enough information too (mode), but*/
00321 /*     possibly also not portable).*/
00322 
00323 if {[package vsatisfies [package present Tcl] 8.4]} {
00324     /*  Tcl 8.4+.*/
00325     /*  (Ad 1) We have -directory, and -types,*/
00326     /*  (Ad 2) Links are returned for -types f/d if they refer to files/dirs.*/
00327     /*  (Ad 3) No bug to code around*/
00328 
00329     ret  ::fileutil::traverse::ACCESS (type args) {return 1}
00330 
00331     ret  ::fileutil::traverse::GLOBF (type current) {
00332     concat \
00333         [glob -nocomplain -directory $current -types f          -- *] \
00334         [glob -nocomplain -directory $current -types {hidden f} -- *]
00335     }
00336 
00337     ret  ::fileutil::traverse::GLOBD (type current) {
00338     concat \
00339         [glob -nocomplain -directory $current -types d          -- *] \
00340         [glob -nocomplain -directory $current -types {hidden d} -- *]
00341     }
00342 
00343 } else {
00344     /*  8.3.*/
00345     /*  (Ad 1) We have -directory, and -types,*/
00346     /*  (Ad 2) Links are NOT returned for -types f/d, collect separately.*/
00347     /*         No symbolic file links on Windows.*/
00348     /*  (Ad 3) Bug to code around.*/
00349 
00350     ret  ::fileutil::traverse::ACCESS (type current) {
00351     if {[catch {
00352         set h [pwd] ; cd $current ; cd $h
00353     }]} {return 0}
00354     return 1
00355     }
00356 
00357     if {[string equal $::tcl_platform(platform) windows]} {
00358     ret  ::fileutil::traverse::GLOBF (type current) {
00359         concat \
00360         [glob -nocomplain -directory $current -types f          -- *] \
00361         [glob -nocomplain -directory $current -types {hidden f} -- *]]
00362     }
00363     } else {
00364     ret  ::fileutil::traverse::GLOBF (type current) {
00365         set l [concat \
00366                [glob -nocomplain -directory $current -types f          -- *] \
00367                [glob -nocomplain -directory $current -types {hidden f} -- *]]
00368 
00369         foreach x [concat \
00370                [glob -nocomplain -directory $current -types l          -- *] \
00371                [glob -nocomplain -directory $current -types {hidden l} -- *]] {
00372         if {![file isfile $x]} continue
00373         lappend l $x
00374         }
00375 
00376         return $l
00377     }
00378     }
00379 
00380     ret  ::fileutil::traverse::GLOBD (type current) {
00381     set l [concat \
00382            [glob -nocomplain -directory $current -types d          -- *] \
00383            [glob -nocomplain -directory $current -types {hidden d} -- *]]
00384 
00385     foreach x [concat \
00386                [glob -nocomplain -directory $current -types l          -- *] \
00387                [glob -nocomplain -directory $current -types {hidden l} -- *]] {
00388         if {![file isdirectory $x]} continue
00389         lappend l $x
00390     }
00391 
00392     return $l
00393     }
00394 }
00395 
00396 /*  ### ### ### ######### ######### #########*/
00397 /*  Ready*/
00398 
00399 package provide fileutil::traverse 0.3
00400 

Generated on 21 Sep 2010 for Gui by  doxygen 1.6.1