00001
00002
00003
00004
00005
00006
00007
00008
00009
00010
00011
00012
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
00030
00031
00032
00033
00034
00035
00036
00037
00038 ret ::fileutil::grep (type pattern , optional files ={)} {
00039 set result [list]
00040 if {[llength $files] == 0} {
00041
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
00066
00067
00068
00069
00070
00071
00072
00073
00074
00075
00076
00077
00078
00079
00080
00081 ret ::fileutil::find (optional basedir =. , optional filtercmd ={)} {
00082 set result {}
00083 filt = [string length $filtercmd]
00084
00085 if {[file isfile $basedir]} {
00086
00087
00088
00089 FADD $basedir
00090
00091 } elseif {[file isdirectory $basedir]} {
00092
00093
00094
00095
00096
00097
00098
00099
00100
00101
00102
00103
00104
00105
00106
00107
00108
00109
00110
00111
00112
00113
00114 pending = [list $basedir]
00115 at = 0
00116 array known = {}
00117
00118 while {$at < [llength $pending]} {
00119
00120 current = [lindex $pending $at]
00121 incr at
00122
00123
00124 ACCESS $current
00125
00126
00127
00128 foreach f [GLOBF $current] { FADD $f }
00129
00130 foreach f [GLOBD $current] {
00131
00132
00133 if {
00134 [string equal [file tail $f] "."] ||
00135 [string equal [file tail $f] ".."]
00136 } continue
00137
00138
00139 FADD $f
00140
00141
00142
00143
00144
00145
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
00162
00163
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
00184
00185
00186
00187
00188
00189
00190
00191
00192
00193
00194
00195
00196
00197
00198
00199
00200
00201
00202
00203
00204
00205
00206
00207
00208
00209
00210
00211
00212 if {[package vsatisfies [package present Tcl] 8.4]} {
00213
00214
00215
00216
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
00234
00235
00236
00237
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
00285
00286
00287 ret ::fileutil::ACCESS (type args) {}
00288
00289 if {[string equal $::tcl_platform(platform) windows]} {
00290
00291
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
00314
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
00339
00340
00341
00342
00343
00344
00345
00346
00347
00348
00349
00350
00351
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
00382
00383
00384
00385
00386
00387
00388
00389
00390
00391
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
00403
00404
00405
00406
00407
00408
00409
00410
00411
00412
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
00424
00425
00426
00427
00428
00429
00430
00431
00432
00433
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
00456
00457
00458
00459
00460
00461
00462
00463
00464
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
00476
00477
00478
00479
00480
00481
00482
00483
00484
00485
00486
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
00507
00508
00509
00510
00511
00512
00513
00514
00515
00516
00517
00518
00519
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
00569
00570
00571
00572
00573
00574
00575
00576
00577
00578
00579
00580
00581
00582
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
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
00626
00627
00628
00629
00630
00631
00632
00633
00634
00635
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
00776
00777
00778
00779
00780
00781
00782
00783
00784
00785
00786
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
00808
00809
00810
00811
00812
00813
00814
00815
00816
00817
00818
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
00841
00842
00843
00844
00845
00846
00847
00848
00849
00850
00851
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
00895
00896
00897
00898
00899
00900
00901
00902
00903
00904
00905
00906
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
00951
00952
00953
00954
00955
00956
00957
00958
00959
00960
00961
00962
00963
00964
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
01063
01064
01065
01066
01067
01068
01069
01070
01071
01072
01073
01074
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
01283
01284
01285
01286
01287
01288
01289
01290
01291
01292
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
01324
01325
01326
01327
01328
01329
01330
01331
01332
01333
01334
01335
01336
01337
01338
01339
01340
01341
01342
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
01397
01398
01399
01400
01401
01402
01403
01404
01405
01406
01407
01408
01409
01410
01411
01412
01413
01414
01415
01416
01417
01418
01419
01420
01421
01422
01423
01424
01425
01426
01427
01428
01429
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
01629
01630
01631
01632
01633
01634
01635
01636
01637
01638
01639
01640
01641
01642
01643
01644
01645
01646
01647
01648
01649
01650
01651
01652
01653
01654
01655
01656
01657
01658
01659
01660
01661
01662
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
01739
01740
01741
01742
01743
01744
01745
01746
01747
01748
01749
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
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
01800
01801
01802
01803
01804
01805
01806
01807
01808
01809
01810
01811
01812
01813
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
01890
01891
01892
01893
01894 if {[package vcompare [package provide Tcl] 8.4] < 0} {
01895
01896
01897
01898
01899
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
01985
01986
01987
01988
01989
01990
01991
01992
01993
01994
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
02045
02046
02047
02048
02049
02050
02051
02052
02053
02054
02055
02056
02057
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
02088
02089
02090
02091
02092
02093
02094
02095
02096
02097
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