diff options
author | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
---|---|---|
committer | William Joye <wjoye@cfa.harvard.edu> | 2016-10-27 19:39:39 (GMT) |
commit | ea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch) | |
tree | 6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/fileutil | |
parent | b5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff) | |
parent | 7c621da28f07e449ad90c387344f07a453927569 (diff) | |
download | blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.zip blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.gz blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.bz2 |
Merge commit '7c621da28f07e449ad90c387344f07a453927569' as 'tcllib'
Diffstat (limited to 'tcllib/modules/fileutil')
28 files changed, 11111 insertions, 0 deletions
diff --git a/tcllib/modules/fileutil/ChangeLog b/tcllib/modules/fileutil/ChangeLog new file mode 100644 index 0000000..6525c50 --- /dev/null +++ b/tcllib/modules/fileutil/ChangeLog @@ -0,0 +1,1006 @@ +2013-07-11 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Ticket [8b317b4a63]: Added code to the 8.4+ + * fileutil.tcl: implementations of GLOBF and GLOBD to guard + * fileutil.test: ourselves against VFS packages mishandling the + * pkgIndex.tcl: -types option of [glob]. vfs::zip is an example. + This mishandling causes glob to return the same data for the two + calls with "-types x" and "-types {hidden x}", generating lists + with duplicate entries. We now generally de-duplicate the result + ourselves. Bumped the package version to 1.14.6. Thanks to + <eugene.mindrov@gmail.com> for the investigation identifying + this problem. + +2013-02-14 Andreas Kupries <andreask@activestate.com> + + * decode.tcl: Bumped fileutil::decode to 0.2 to distinguish + * pkgIndex.tcl: properly from the 0.1.xxx version which existed in + AS/perforce before it moved to tcllib/fossil. That should have + been done as part of the move, and was forgotten. + +2013-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.15 ======================== + * + +2013-01-28 Andreas Kupries <andreask@activestate.com> + + * decode.tcl: New. Simple package to support writing decoders + * pkgIndex.tcl: for binary files. + +2013-01-08 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: [Bug 3147481], [Bug 3141568]. Fixed issues with + * fileutil.tcl: changes to [glob]'s behaviour in 8.5+, reported + * find.setup: by guardus@users.sourceforge.net. Plus [Bug 3599839] + * find.test: reminding us of these, and Win 7. Bumped version to + * pkgIndex.tcl: 1.14.5. Extended testsuite, can be repro'd under + Unix also. + +2012-08-29 Andreas Kupries <andreask@activestate.com> + + * traverse.tcl (Init): Fixed a bug where a symlink to the + * traverse.man: base directory was not handled correctly. + * traverse.test: It got expanded and should not have been. + * pkgIndex.tcl: Cause was missing initialization of the _known set + * find.setup: with the normalized base path. Added test for this. + Bumped version to 0.4.3. + +2011-12-13 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.14 ======================== + * + +2011-12-02 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * traverse.tcl: Mark directories as 'known' only if we decided + * traverse.man: to recurse into them. Otherwise a link to a + * pkgIndex.tcl: directory we choose to not follow may non- + deterministically prevent us from recursing into the original + directory, depending on the order we encounter them in. Bumped + package version to 0.4.2. + +2011-01-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.13 ======================== + * + +2010-06-16 Andreas Kupries <akupries@shaw.ca> + + * fileutil.tcl (::fileutil::fileType): Fumbled the extension of + * fileutil.man: the doctools/toc/idx detector in the last change. + * pkgIndex.tcl: Fixed the bogosity, merging the separate checks. + Bumped to version 1.14.4. + +2010-06-09 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::fileType): Extended to recognize tklib + * fileutil.man: diagram files, and extended the doctools/toc/idx + * pkgIndex.tcl: detector to accept markers in the same style as + for tklib diagrams. Bumped to version 1.14.3. + +2009-12-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.12 ======================== + * + +2009-11-24 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Extended the doctools/toc/idx detector with + * fileutil.man: checks for inclusion and exclusion markers + * pkgIndex.tcl: allowing the user to force acceptance or rejection + of files for complex situations (like an include file looking like + a doctools main file but not bein so, and the converse, a main + file lacking the regular marker, which is hidden in an included + file). Bumped to version 1.14.2. + +2009-10-27 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::stripPath): [Bug 2872536]. Fixed both + * fileutil.man: issues. Compare the canonical list representations + * pkgIndex.tcl: for the \\bar problem, and separate $prefix from + * strip.test: '*' by a space to prevent matching on a partial path + segment. Thanks to Ashok P. Nadkarni + <apnadkarni@users.sourceforge.net> for the report. Bumped the + version to 1.14.1. Extended the testsuite. + + * pathops.test: Cleaned up some win vs unix issues in the tests. + +2009-10-06 Andreas Kupries <andreask@activestate.com> + + * fileutil.test: Added fixes to test cleanups, to prevents errors + * inplace.test: when the testsuite is only run partially. + * test.test: + +2009-09-14 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Renamed the internal command 'LexNormalize' to + * fileutil.man: 'lexnormalize' and documented it, making it + * pkgIndex.tcl: public. Bumped version to 1.14. This closes and + accepts the feature request [SF Tcllib Bug 2855302]. Tests are + currently only indirect, through testing of commands 'jail' and + 'relativeUrl'. + +2009-04-01 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl (Copy): Fix special case with copying directories + * multiop.man: where the source is one level to deep, or copy fails. + * pkgIndex.tcl: Bumped version to 0.5.3. + +2009-02-10 Andreas Kupries <andreask@activestate.com> + + * traverse.man: Fixed bug in the requirements, this package + * traverse.tcl: needs fileutil (-> fullnormalize). Bumped + * pkgIndex.tcl: version to 0.4.1. + +2009-02-05 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::stripPath): Fixed handling + * fileutil.man: of letter-case on windows, comparisons + * strip.test: have to be case-insensitive. Bumped version + * pkgIndex.tcl: to 1.13.6. See [SF Tcllib Bug 2499641]. + +2008-12-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11.1 ======================== + * + +2008-12-02 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Fixed [Bug 2376321] (fileutil::TempDir, use + * fileutil.tcl: of env(TRASHFOLDER) for OSX). Bumped version + * pkgIndex.tcl: to 1.13.5. + +2008-10-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.11 ======================== + * + +2008-10-10 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * multiop.test: Canonicalized sorting of two test results. + * multi.test: + +2008-09-03 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * multiop.tcl: Sync'd to 2008 Sep 3 change in wip-core, using + * multiop.man: the new name of the wip processor component. + * pkgIndex.tcl: Bumped version to 0.5.2. + +2008-06-20 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl: Fixed usage of struct::stack. Now using the + * multiop.man: official construction command instead of an + * pkgIndex.tcl: internal command recently removed from the + struct::stack package. Bumped version to 0.5.1. + +2008-05-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * multi.man: Updated to changes in doctools (sub)section reference + handling. + +2008-02-27 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * traverse.tcl: Made snit dependency more strict, requesting 1.3+. + +2007-10-22 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Changed handling of broken symbolic links + * fileutil.man: by fileutil::find and fileutil::traverse. + * traverse.tcl: They are now recognized and returned. + * traverse.man: Extended the testsuite. Bumped fileutil + * pkgIndex.tcl: to v1.13.4 and fileutil::traverse to v0.4. + * find.setup: + * find.test: + * traverse.text: + +2007-09-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.10 ======================== + * + +2007-08-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * multiop.setup: Moved the common setup code for the testing of + * multiop.test: the multi-file operations into a separate file. + * multi.test: Added commands to query the state of objects. + * multiop.tcl: Updated both testsuite and documentation. Version + * multiop.man: bumped to 0.5. + * pkgIndex.tcl: + +2007-08-15 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl (ChDir, ChUp): Fixed the method calls in change dir + * multiop.man: operations, forget the $self. Version bumped to 0.4. + * pkgIndex.tcl: + +2007-08-13 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl (Expand): Fixed bug in recursive expansion, pattern + applies to the last segment of the path. + + * multiop.tcl: New features coming out of dog-fooding: Removed + * multiop.man: hardwired fixation on files, accept all paths by + * multiop.test: default, and added commands to allow the user to + * multi.test: limit the expansion to files, firectories, or links. + * pkgIndex.tcl: New command to allow the user to specify how + strict checking of the 'in(to)' argument is, and if empty + expansions are acceptable. Documentation updated. Testsuite + updated and extended. Version bumped to 0.3 + +2007-08-10 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl: New features - Recursive processing. + * multiop.test: Expand only mode. Save/restore set of files + * pkgIndex.tcl: to/from a variable. Platform checking + * multi.test: semi-conditional commands. Invoke a user command for + * multiop.man: the file set. Test suite extended, documentation + updated. Version bumped to 0.2 + + * fileutil.tcl: The rewrite of find changed how it invoked the + * fileutil.man: filter command. As that however is documented this + * pkgIndex.tcl: change is a bug. Restored the old way of invoking + * find.test: the filter command. Version bumped to 1.13.3. + Extended the testsuite to verify the API. + +2007-08-08 Andreas Kupries <andreask@activestate.com> + + * inplace.test: Fixed failures of some tests on Windows. + + * fileutil.tcl: Working around a possible bug in the handling of + path intreps by the core, on Windows. See commentary in + 'fullnormalize', and 'find.setup' (f_setupcircle). + + * find.test: Moved the support code into a separate file, the new + * find.setup: 'find.setup'. Added a testsuite for the package + * traverse.test: 'fileutil::traverse', using the same support + * traverse.tcl: code. Rewrote the internals of traverse.tcl based + * traverse.man: on the experience gained by the rewrite of 'find', + * pkgIndex.tcl: using similar re-focatorization of the platform + and core dependencies, and made the traversal state variables + easier to understand (separated the processing and result + stack). Bumped version of traverse to 0.3. + +2007-08-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * find.test: Reworked the setup/cleanup code, made the tests more + independent from each other. + + * traverse.man: New file, first documentation for + fileutil::traverse. + +2007-08-07 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::find): Command rewritten to use a + * pkgIndex.tcl: portable iterative traversal core. Platform and + * fileutil.man: Tcl version dependencies have been re-factored + into separate small commands. Should be more readable and + maintainable. Handling of circular symbolic links is now + portable (via a portable 'file normalize'). Order of paths in + the output changed, this however was never documented. Version + bumped to 1.13.2. + +2007-08-03 Andreas Kupries <andreask@activestate.com> + + * fileutil.test: Cleaned the setup/cleanup code up a bit. + + * filetype.test: Split the overly large file fileutil.test + * fileutil.test: into a series of smaller test files for + * find.test: specific commands. + * inplace.test: + * pathops.test: + * strip.test: + * test.test: + +2007-08-01 Andreas Kupries <andreask@activestate.com> + + * fileutil.test: Fixed problems with the tests for + fileutil::writeFile and companions. The regsub did not like + paths containing special characters like '+'. Replaced with a + 'string map' construction. + + * multiop.tcl: Language tweak. Allow 'from', and 'into' (and aliases) + * multiop.man: as qualifiers to 'the', i.e. allow specification after + * multiop.test: 'the'. Updates documentation and examples too. + + * multi.tcl: Added documentation and testsuite for the multiop + * multi.man: package built on top of the multiop objects. Bug + * multi.test: fixes. + * multiop.tcl: + * multiop.man: + * multiop.test: + +2007-07-31 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * multiop.man: Wrote the actual documentation for the core + * multiop.tcl: multi-file objects. Tweaked the syntax a bit. + +2007-07-31 Andreas Kupries <andreask@activestate.com> + + * multiop.tcl: New packages in module. Scatter/gather + * multiop.test: operations, multi-file operations. + * multiop.man: First user of 'wip' code. + * multi.tcl: + * pkgIndex.tcl: + +2007-07-27 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Fixed problems of 'relative' and 'relativeUrl' + * fileutil.man: with paths containing . and .., i.e. like + * fileutil.test: '/foo/bar/../common'. Done by making the paths + * pkgIndex.man: absolute and then using LexNormalize to remove + them. Added a fast path to LexNormalize to quickly return for + paths not containing . nor .. Also fixed bug in handling .. with + short relative paths. Extended the testsuite. Package bumped to + version 1.13.1 (patchlevel, bugfixes only). + +2007-06-18 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl (::fileutil::fullnormalize): Simplified the + implementation by recognizing that the complex + file split/lrange/eval/join combo is just 'file dirname'. + +2007-06-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * traverse.tcl: Fixed invokation of -filter callback when used for + * pkgIndex.tcl: directories. Wrongly used only the short path. Now + correctly uses the full name. Bumped package version to 0.2. + +2007-05-28 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Added command 'tempdirReset' to allow users to + * fileutil.man: clear the information set via 'tempdir path'. + * pkgIndex.tcl: Bumped package to version 1.13. Additionally + extended relative-url computation to make the result of an + edge-case nicer looking (result was ok, but not + optimal). Extended testsuite for this. Extended error output + when computation of relative path fails. + +2007-03-28 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::fileType): Added definitions to + * pkgIndex.tcl: recognize Apple .icns files. + * fileutil.man: Bumped version up to 1.12. + +2007-03-21 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Fixed all warnings due to use of now deprecated + commands. Added a section about how to give feedback. + +2007-03-12 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::TempDir): Added code to collect all + * pkgIndex.tcl: problems encountered, to be reported if no temp + * fileutil.man: directory could be found, to aid in the + debugging of the situation. Bumped version up to 1.11. + +2007-02-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Added a command [fileutil::fullnormalize]. + * fileutil.man: Documented it, extended the testsuite. Bumped + * filetuil.test: version up to 1.10. Removed nonsensical + * pkgIndex.tcl: [file join one_element] calls from the testsuite. + +2006-10-11 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Marked all 20 permission dependent tests as + 'notRoot' as they cannot fail when the superuser executes the + testsuite. + +2006-10-08 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Rewritten to use new features for handling the + environment. + +2006-10-03 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.9 ======================== + * + +2006-09-19 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Bumped version to 1.9 + * fileutil.man: + * pkgIndex.tcl: + +2006-09-14 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Fixed 8.3isms in the testsuite. Symbolic + permissions for 'file attributes' are not available in 8.2 yet + :(. Also fixed the tests using hardcoded wrong#args messages to + use the compatibility commands instead. + +2006-09-13 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Fixed 8.4ism in the testsuite of a 8.2+ package. + +2006-07-19 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (fileutil::jail): Fixed [Tcllib SF Bug 1525172], by + * fileutil.test: Ramon Ribo. Accepted the provided + solution. Extended the testsuite. + +2006-06-16 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl: Added commands to compute paths relative to some + * fileutil.man: base. Extended testsuite, documentation. + * fileutil.test: + * pkgIndex.tcl: + +2006-04-19 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Extended documentation. + * fileutil.test: Extended testsuite for x/exec. + * fileutil.tcl: Added x/exec'utable flag to the 'test' command. + +2006-03-22 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * traverse.tcl: Bug fixes. Added the missing declaration of the + dev/inode cache, and moved the restoration of the old working + directory in the 8.3 glob -directory emulation to their proper + place. + + * traverse.tcl: New file. Object based directory traversal, + * pkgIndex.tcl: incremental at the core, i.e. can be event- + driven. Iterative algorithm, no trouble with deep directory + structures anymore. Not documented yet, no testsuite yet. + +2006-03-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Removed some hardcoded paths into my + system. Thanks to Michael Schlenker for catching this. + +2006-03-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Extended the tempdir command to allow the user + * fileutil.man: to set a directory to use, exclusively. Updated + * fileutil.test: both documentation and testsuite. + + * fileutil.tcl: Added a 'test' command for the quick testing of + * fileutil.man: several properties of a given path. Extended + * fileutil.test: both documentation and testsuite. + +2006-02-09 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Updated the documentation to cover all the new + commands. + + * fileutil.test: Extended testsuite to cover the command + 'updateInPlace'. + + * fileutil.tcl: Fixed bug in implementation of 'updateInPlace', we + wrote the unchanged input back instead of the transformation + result. + +2006-02-08 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Extended the testsuite to cover the commands + 'appendToFile', 'insertIntoFile', 'removeFromFile', and + 'replaceInFile'. + + * fileutil.tcl: Added argument validation to the commands + 'appendToFile', 'insertIntoFile', 'removeFromFile', and + 'replaceInFile'. Modified their implementation to use shorter + implementations for the degenerate cases. + +2006-02-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Extended testsuite to cover the command + 'writeFile'. + + * fileutil.tcl: Fixed problems in common option processor and + supporting commands. + +2006-02-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Added commands 'writeFile', 'appendToFile', + 'insertIntoFile', 'removeFromFile', 'replaceInFile', and + 'updateInPlace'. Extended 'cat' with option processing for + -encoding, ... + + * fileutil.test: Extended testsuite of 'cat', and made + this part self-contained with regard to temp files. + +2006-01-28 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Fixed use and cleanup of temp.files. + +2006-01-22 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: More boilerplate simplified via use of test support. + +2006-01-19 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Hooked into the new common test support code. + +2006-01-18 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Fixed a typo, clarified usage of the filtercmd for + find (Boolean result), added an example for that as well. This + fixes the [SF Tcllib Bug 1409083] submitted by Glenn Jackman + <glennjnn@users.sourceforge.net>. + +2005-10-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.8 ======================== + * + +2005-09-19 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test (tempdir-1.2): Added code to remove the env + variable TEMP as well. It is set on my host and can thus + interfere with the test. + +2005-06-21 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Fixed a bug in find, where it excluded symbolic + * fileutil.tcl: links if the destination was circular. This is + wrong. The link itself can be listed in the result, it just must + not be followed. Otherwise even non-circular links are not listed. + +2005-02-14 Andreas Kupries <andreask@activestate.com> + + * fileutil.man (find): Documented the implicit arguments to the + filter command (current working directory). This fixes [SF + Tcllib Bug 1048995]. + +2005-02-10 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (fileutil::jail): Fixed a problem reported by Pat + Thoyts. Test jail-2.1 failed on windows. The reason was improper + handling of volume-relative paths. They have to be handled like + absolute paths, but were not. Changed the initial if condition a + bit to fix this. + +2005-02-09 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Made glob patterns platform-dependent. This fixes + [SF Tcllib Bug 1098039]. + +2005-02-08 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Added test case for the handling of dot-files by + [find]. This is for [SF Tcllib Bug 1098039]. + +2005-02-07 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Completed implementation and testing of the 'jail' + command which ensures that a path is inside a specific + directory. Implemented a 'LexNormalize' helper + command. + + * fileutil.test: Added additional test for 'jail' beyond our + current suite. + +2005-02-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Implemented command 'stripPath'. Like 'stripPwd' + for a general prefix path [SF Tcllib RFE 1040372]. + + * fileutil.test: Added tests for 'stripPath. + * fileutil.man: Documented 'stripPath'. + +2005-02-04 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Added tests for the 'Normalize' forward + compatibility wrapper. Got them from the Tcl testsuite for 'file + normalize'. Added tests for the new 'jail' command. + + * fileutil.tcl: Wrote a forward compatibility implementation of + 'file normalize', for use with a Tcl core < 8.4. Updated the + (non)use of 'file normalize' by 'tempdir' and 'tempfile'. + Updated the namespace export clause. Implemented the 'jail' + command [SF Tcllib RFE 111076]. + +2005-02-01 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Extended tests for the case of inacessible + directories. + + * fileutil.tcl (find): Fixed SF [Tcllib Bug 1111153]. This is the + same as [Tcllib SF Bug 862491]. Catching problems now when + cd'ing into the chosen base directory, and ignoring all + directories for which the [cd] fails (permissions). + +2005-01-28 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Fixed the doctools syntax error in the new text + from the last entry. + +2005-01-26 David N. Welton <davidw@dedasys.com> + + * fileutil.man: Added a small note on the measure used to count + elements in fileutil::stripN. + +2004-10-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.7 ======================== + * + +2004-09-29 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Introduced a "makeBinaryFile" command to ensure + the correct generation of the example files which contain + binary. Thanks to Greg Baker <gregb@ifost.org.au> for noticing + the problem on his Mac OS X box and helping in debugging it. + +2004-09-27 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Fixed platform problems in testsuite, bad + characters in filenames for Windows. + +2004-06-16 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (fileType): Added code to recognize files written + in the doctools, doctoc, and docidx documentation formats. + +2004-05-30 Aaron Faupell <afaupell@users.sourceforge.net> + + * fileutil.tcl: added tests for dos executables, tar, zip, bmp, wav, and mp3 to fileType + * fileutil.man: updated to reflect new types for fileType + +2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Updated version number to sync with 1.6.1 + * fileutil.man: release. + * pkgIndex.tcl: + +2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.6.1 ======================== + * + +2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Rel. engineering. Updated version number + * fileutil.man: of fileutil to reflect its changes, to 1.6.1. + * pkgIndex.tcl: + +2004-05-23 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Cleaning up after Aaron. Updated the test + filetype-1.12 to look for the extended return value of fileType + when applied to jpeg images. The last checkin changed this, but + the test was not updated as well, most likely not even + run. Found and corrected during release preparation and testing. + +2004-05-11 Aaron Faupell <afaupell@users.sourceforge.net> + + * fileutil.tcl: updated the jpeg test to recognize exif format + +2004-02-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.6 ======================== + * + +2004-02-09 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: The tempdir tests used advanced test syntax not + available before 8.4 (IIRC). Not useable for 8.2. Ditto the lot of + the install tests also used 8.3 and 8.4 features. Straightened + the bad syntax out (killed the tests), and added proper + constraints to the tests. + + * fileutil.tcl: Ooops. fileutil is certified for usage with Tcl + 8.2, there is no [file normalize] before 8.4. Made the usage + (See 2004-02-03) conditional. + +2004-02-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Modified the tests a bit to use glob characters + in file names to check that the code is robust against that too. + + See http://bugs.activestate.com/show_bug.cgi?id=29491 + +2004-02-03 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (tempdir, tempfile): Now [file normalize]'ing the + results of these two commands. More windows friendly. + +2004-01-28 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl (fileutil::find): Fixed bug in handling a single + file as base directory: The initial list was not set up as a + list, and the test results (see below) were not listified either + for these cases. That broke down for paths containing + spaces. Also fixed: Neither the windows nor the pre-8.4 version + of the command had been modified to deal with a single file. + Oversight from the change @ 2003-10-22 by David N. Welton + <davidw@dedasys.com>. + + Regularized a number of error returns. + + * fileutil.test: Changed testsuite to use files and directories + containing spaces in their names. A number of tests + failed. Modified the tempdir tests to better exclude and + manipulate the environment to enforce the wanted result. + +2004-01-23 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (fileType): The check for an attached metakit + database left a channel on the tested file open. This causes the + application to leak channels, and on windows the file is also + locked against deletion. Fixed. + +2003-11-15 David N. Welton <davidw@dedasys.com> + + * fileutil.man: Added new test and docs for change below. + + * fileutil.tcl (::fileutil::cat): Make cat accept multiple files + (bug [830075]). + +2003-10-27 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Better formatting of the new docs. + +2003-10-25 David N. Welton <davidw@dedasys.com> + + * fileutil.man: Documented 'fileutil::tempdir' algorithm. + +2003-10-23 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Updated documentation, added description of + 'fileutil::tempdir'. + +2003-10-23 David N. Welton <davidw@dedasys.com> + + * fileutil.test (tempdir): Added a few very simple tests for + tempdir under Unix. + + * fileutil.tcl (::fileutil::tempdir): Added tempdir + implementation. + (::fileutil::tempfile): Use new tempdir command. + +2003-10-22 David N. Welton <davidw@dedasys.com> + + * fileutil.tcl (::fileutil::find): Change Unix version of find + command to accept a file (as opposed to a directory) as an + argument. This reflects the behavior of the Unix find + command. This change also needs to be added to the windows + version, but should probably be done by someone who can test it + on that platform. + (::fileutil::install): -m option only works on Unix, as far as I + know. + (::fileutil::install): Make sure correct thing is chmod'ed. + + * fileutil.test: Added tests for find to check that it works ok + with a single file as an argument. Also, error out if initial + file does not correspond to 'isfile' or 'isdirectory'. + +2003-09-03 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (fileutil::fileType): Fixed the SourceForge Tcllib + bug [795585]. We are now allowing whitespace between the #! and + path of the script interpreter. Extended to identify metakit + databases, also when attached to another file. + + (tempfile): Added 'global env', access to 'env' was bad, causing + malfunction on windows. + +2003-08-12 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: Fixed a bad call of [cmd] in the documentation of + the new install command. Always use 'sak validate moldule' to + validate the whole module, or 'sak text module' when wishing to + validate only the documentation. + +2003-08-06 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (find): The fix for SF tcllib bug [647974] + introduced a tcl command new to tcl 8.4. This command is + [file system]. Changing the code to exclude dev/inode checking + only for 8.4 and above. For versions of Tcl below 8.4. the fix + is not required as they do not have virtual filesystem + support. This fixes SF tcllib bug [784157]. + +2003-08-06 David N. Welton <davidw@dedasys.com> + + * fileutil.man: Added documentation for the new 'install' command. + + * fileutil.tcl (::fileutil::install): Added 'install' command, + which is similar in functionality to the Unix install command - it + is basically file copy with some additional features. + + * fileutil.test: Added tests for 'install' fileutil command. + +2003-07-04 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * Bumped version to 1.5.1 as this is a bug fix. Any bug fixes and + more minor changes coming in the future do not have to bump the + version number anymore until the next release. Only a major + change warrants another bump before the release. + + * fileutil.tcl: Fixed SF tcllib bug [647974]. We now ignore + device/inode information if the current path is in a virtual + filesystem. We also assume now that VFS's do not support links, + thus also obviating the need for the data, circles cannot occur. + +2003-05-09 Andreas Kupries <andreask@activestate.com> + + * fileutil.tcl (::fileutil::tempfile): Braced [expr]. + +2003-05-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * + * Released and tagged Tcllib 1.4 ======================== + * + +2003-05-01 Pat Thoyts <patthoyts@users.sourceforge.net> + + * fileutil.tcl: The touch command cannot be implemented in Tcl + * fileutil.test: versions < 8.3 so do not define the command. + * fileutil.man: Noted in documentation and skipped tests. + +2003-04-23 Andreas Kupries <andreask@pliers.activestate.com> + + * fileutil.man: + * fileutil.tcl: Added command [tempfile]. Was part of tcllib patch + 611595, but has a better place here. + +2003-04-11 Andreas Kupries <andreask@activestate.com> + + * fileutil.man: + * fileutil.tcl: + * pkgIndex.tcl: Set version of the package to to 1.5. + +2003-04-02 Andreas Kupries <andreask@activestate.com> + + * fileutil.test (fileutil): Fixed tcllib SF bug #714214 reported + by Pat Thoyts, by working around the 'makeFile' command provided + with tcltest. It seems to have issues when doing binary data. + +2003-03-24 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl (fileutil::touch): Applied patch #688965 provided + by Glenn Jackman <glennjnn@users.sourceforge.net>. This patch + provides a better message when asking the [fileutil::touch] + command for help. + +2003-03-24 Andreas Kupries <andreask@activestate.com> + + * fileutil.test: + * fileutil.man: + * fileutil.tcl: Fixed bug #707009, reported by Helmut Giese + <hgiese@users.sourceforge.net>, also updated the documentation + and the testsuite. + +2003-01-28 David N. Welton <davidw@dedasys.com> + + * fileutil.tcl (::fileutil::fileType): Use 'string match' instead + of regexp. Require Tcl 8.2. + +2003-01-16 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: More semantic markup, less visual one. + +2002-10-08 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: + * fileutil.man: + * fileutil.test: Accepted enhanced format detection by Philip + Ehrens <pehrens@ligo.caltech.edu>. + +2002-05-21 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl (cat): Fixed bug #556504, reported by Michael + A. Cleverly <cleverly@users.sourceforge.net>. The fix was + provided by Michael too. The problem was reading files which are + reported as size 0, but actually have content, just dynamically + generated (Linux /proc is an example of an fs containing such + files). + +2002-05-14 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Documented the two new commands (stripN, + stripPwd). + + * fileutil.tcl: Made up my mind about SF Bug #462015. The proposed + interface change to [find] is rejected to keep the interface of + the library procedure simple and without hidden surprises = + KISS. Added a command [stripPwd] instead which can be used by + the caller of [find] to make the returned paths relative to the + current working directory. Also added [stripN] to strip a fixed + number of elements from the beginning of a path. + +2002-04-12 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.man: Added doctools manpage. + * fileutils.n: Updated to reflect change of version. + +2002-03-20 Eric Melski <ericm@ajubasolutions.com> + + * Bumped version to 1.4 + + * fileutil.n: + * fileutil.test: + * fileutil.tcl: Added fileType command posted to comp.lang.tcl by + Phil Ehrens, with some minor modifications. + +2002-01-15 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * Bumped version to 1.3 + +2001-12-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: Restricted tests 2.2 and 2.3 to the directory + structure created for the test and not the whole directory the + test is run in. Bugfix for item #486572. + +2001-11-06 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.test: + * fileutil.n: + * fileutil.tcl: Applied patch #477805 by Glenn Jackman + <glennjnn@users.sourceforge.net> implementing the unix 'touch' + command. Contains documentation and testsuite for the new + command too. + +2001-09-05 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Restricted export list to public API. + [456255]. Patch by Hemang Lavana + <hemanglavana@users.sourceforge.net> + +2001-08-21 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * All of the changes below are related to tcllib Patch [449531] by + Anselm Lingnau <lingnau@users.sourceforge.net>. Instead of + taking in the proposed highlevel 'fileinput' I added some of the + more low-level commands from Tclx which can be used to + create/compose 'fileinput'. + + * pkgIndex.tcl: Moved version of fileutil to 1.2. + + * fileutil.test: Added tests for the new commands. Moved version + of fileutil to 1.2. + + * fileutil.n: Added documentation of the new commands. Moved + version of fileutil to 1.2. + + * fileutil.tcl (findByPattern, foreachLine): New commands, modeled + after TclX's 'recursive_glob' and 'for_file'. Moved version of + fileutil to 1.2. + +2001-07-31 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.n: Added manpage documenting the commands. tcllib Bug + [446584]. + +2001-06-21 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: Fixed dubious code reported by frink. + +2001-03-20 Andreas Kupries <andreas_kupries@users.sourceforge.net> + + * fileutil.tcl: [Bug #410104, Patch #410106] + New implementation of ::fileutil::find for unixoid OSs using + stat and device/inode configuration to detect and break circular + softlink structures. This implementation also skips un'stat'able + files and directories. + + * fileutil.test: Added fileutil-1.4 testing the circle breaker + (only under unix). + +2000-03-10 Eric Melski <ericm@scriptics.com> + + * fileutil.test: + * fileutil.tcl: Added cat function, duplicates standard UNIX "cat" + utility. + +2000-03-09 Eric Melski <ericm@scriptics.com> + + * fileutil.test: Collected tests into one file; adapted tests for + use in/out of tcllib test framework. + diff --git a/tcllib/modules/fileutil/cross-index-trav.inc b/tcllib/modules/fileutil/cross-index-trav.inc new file mode 100644 index 0000000..a51b823 --- /dev/null +++ b/tcllib/modules/fileutil/cross-index-trav.inc @@ -0,0 +1,16 @@ +[example { + package require fileutil::traverse + + proc NoLinks {fileName} { + if {[string equal [file type $fileName] link]} { + return 0 + } + return 1 + } + + fileutil::traverse T /sys/devices -prefilter NoLinks + T foreach p { + puts $p + } + T destroy +}] diff --git a/tcllib/modules/fileutil/cross-index.inc b/tcllib/modules/fileutil/cross-index.inc new file mode 100644 index 0000000..5abce12 --- /dev/null +++ b/tcllib/modules/fileutil/cross-index.inc @@ -0,0 +1,12 @@ +[example { + /sys/class/tty/tty0 --> ../../dev/tty0 + /sys/class/tty/tty1 --> ../../dev/tty1 + /sys/class/tty/tty2 --> ../../dev/tty1 + + /sys/dev/tty0/bus + /sys/dev/tty0/subsystem --> ../../class/tty + /sys/dev/tty1/bus + /sys/dev/tty1/subsystem --> ../../class/tty + /sys/dev/tty2/bus + /sys/dev/tty2/subsystem --> ../../class/tty +}] diff --git a/tcllib/modules/fileutil/decode.tcl b/tcllib/modules/fileutil/decode.tcl new file mode 100644 index 0000000..a9d205a --- /dev/null +++ b/tcllib/modules/fileutil/decode.tcl @@ -0,0 +1,191 @@ +# -*- tcl -*- +# ### ### ### ######### ######### ######### +## Copyright (c) 2008-2009 ActiveState Software Inc. +## Andreas Kupries +## BSD License +## +# Package to help the writing of file decoders. Provides generic +# low-level support commands. + +package require Tcl 8.4 + +namespace eval ::fileutil::decode { + namespace export mark go rewind at + namespace export byte short-le long-le nbytes skip + namespace export unsigned match recode getval + namespace export clear get put putloc setbuf +} + +# ### ### ### ######### ######### ######### +## + +proc ::fileutil::decode::open {fname} { + variable chan + set chan [::open $fname r] + fconfigure $chan \ + -translation binary \ + -encoding binary \ + -eofchar {} + return +} + +proc ::fileutil::decode::close {} { + variable chan + ::close $chan +} + +# ### ### ### ######### ######### ######### +## + +proc ::fileutil::decode::mark {} { + variable chan + variable mark + set mark [tell $chan] + return +} + +proc ::fileutil::decode::go {to} { + variable chan + seek $chan $to start + return +} + +proc ::fileutil::decode::rewind {} { + variable chan + variable mark + if {$mark == {}} { + return -code error "No mark to rewind to" + } + seek $chan $mark start + set mark {} + return +} + +proc ::fileutil::decode::at {} { + variable chan + return [tell $chan] +} + +# ### ### ### ######### ######### ######### +## + +proc ::fileutil::decode::byte {} { + variable chan + variable val [read $chan 1] + binary scan $val c val + return +} + +proc ::fileutil::decode::short-le {} { + variable chan + variable val [read $chan 2] + binary scan $val s val + return +} + +proc ::fileutil::decode::long-le {} { + variable chan + variable val [read $chan 4] + binary scan $val i val + return +} + +proc ::fileutil::decode::nbytes {n} { + variable chan + variable val [read $chan $n] + return +} + +proc ::fileutil::decode::skip {n} { + variable chan + #read $chan $n + seek $chan $n current + return +} + +# ### ### ### ######### ######### ######### +## + +proc ::fileutil::decode::unsigned {} { + variable val + if {$val >= 0} return + set val [format %u [expr {$val & 0xffffffff}]] + return +} + +proc ::fileutil::decode::match {eval} { + variable val + + #puts "Match: Expected $eval, Got: [format 0x%08x $val]" + + if {$val == $eval} {return 1} + rewind + return 0 +} + +proc ::fileutil::decode::recode {cmdpfx} { + variable val + lappend cmdpfx $val + set val [uplevel 1 $cmdpfx] + return +} + +proc ::fileutil::decode::getval {} { + variable val + return $val +} + +# ### ### ### ######### ######### ######### +## + +proc ::fileutil::decode::clear {} { + variable buf {} + return +} + +proc ::fileutil::decode::get {} { + variable buf + return $buf +} + +proc ::fileutil::decode::setbuf {list} { + variable buf $list + return +} + +proc ::fileutil::decode::put {name} { + variable buf + variable val + lappend buf $name $val + return +} + +proc ::fileutil::decode::putloc {name} { + variable buf + variable chan + lappend buf $name [tell $chan] + return +} + +# ### ### ### ######### ######### ######### +## + +namespace eval ::fileutil::decode { + # Stream to read from + variable chan {} + + # Last value read from the stream, or modified through decoder + # operations. + variable val {} + + # Remembered location in the stream + variable mark {} + + # Buffer for accumulating structured results + variable buf {} +} + +# ### ### ### ######### ######### ######### +## Ready +package provide fileutil::decode 0.2 +return diff --git a/tcllib/modules/fileutil/filetype.test b/tcllib/modules/fileutil/filetype.test new file mode 100644 index 0000000..55123cf --- /dev/null +++ b/tcllib/modules/fileutil/filetype.test @@ -0,0 +1,193 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: filetype.test,v 1.1 2007/08/03 23:07:25 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +support { + useTcllibFile fumagic/fumagic.testsupport ; # Filetype helpers. + use cmdline/cmdline.tcl cmdline +} +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +set path [makeFile {} bogus] +removeFile bogus + +# ------------------------------------------------------------------------- + +test fileType-1.1 {test file non-existance} { + set res [catch {fileutil::fileType $path} msg] + list $res $msg +} [list 1 "file not found: '$path'"] + +test fileType-1.2 {test file directory} { + set f [makeDirectory fileTypeTest] + set res [catch {fileutil::fileType $f} msg] + removeDirectory fileTypeTest + list $res $msg +} [list 0 [list directory]] + +test fileType-1.3 {test file empty} { + set f [makeEmptyFile] + set res [catch {fileutil::fileType $f} msg] + removeEmptyFile + list $res $msg +} [list 0 [list empty]] + +test fileType-1.4 {test simple binary} { + set f [makeBinFile] + set res [catch {fileutil::fileType $f} msg] + removeBinFile + list $res $msg +} [list 0 [list binary]] + +test fileType-1.5 {test elf executable} { + set f [makeElfFile] + set res [catch {fileutil::fileType $f} msg] + removeElfFile + list $res $msg +} [list 0 [list binary executable elf]] + +test fileType-1.6 {test simple text} { + set f [makeTextFile] + set res [catch {fileutil::fileType $f} msg] + removeTextFile + list $res $msg +} [list 0 [list text]] + +test fileType-1.7 {test script file} { + set f [makeScriptFile] + set res [catch {fileutil::fileType $f} msg] + removeScriptFile + list $res $msg +} [list 0 [list text script /bin/tclsh]] + +test fileType-1.8 {test html text} { + set f [makeHtmlFile] + set res [catch {fileutil::fileType $f} msg] + removeHtmlFile + list $res $msg +} [list 0 [list text html]] + +test fileType-1.9 {test xml text} { + set f [makeXmlFile] + set res [catch {fileutil::fileType $f} msg] + removeXmlFile + list $res $msg +} [list 0 [list text xml]] + +test fileType-1.10 {test xml with dtd text} { + set f [makeXmlDTDFile] + set res [catch {fileutil::fileType $f} msg] + removeXmlDTDFile + list $res $msg +} [list 0 [list text xml foobar]] + +test fileType-1.11 {test PGP message} { + set f [makePGPFile] + set res [catch {fileutil::fileType $f} msg] + removePGPFile + list $res $msg +} [list 0 [list text message pgp]] + +test fileType-1.12 {test binary graphic jpeg} { + set f [makeJpegFile] + set res [catch {fileutil::fileType $f} msg] + removeJpegFile + list $res $msg +} [list 0 [list binary graphic jpeg jfif]] + +test fileType-1.13 {test binary graphic gif} { + set f [makeGifFile] + set res [catch {fileutil::fileType $f} msg] + removeGifFile + list $res $msg +} [list 0 [list binary graphic gif]] + +test fileType-1.14 {test binary graphic png} { + set f [makePngFile] + set res [catch {fileutil::fileType $f} msg] + removePngFile + list $res $msg +} [list 0 [list binary graphic png]] + +test fileType-1.15 {test binary graphic tiff} { + set f [makeTiffFile] + set res [catch {fileutil::fileType $f} msg] + removeTiffFile + list $res $msg +} [list 0 [list binary graphic tiff]] + +test fileType-1.16 {test binary pdf} { + set f [makePdfFile] + set res [catch {fileutil::fileType $f} msg] + removePdfFile + list $res $msg +} [list 0 [list binary pdf]] + +test fileType-1.17 {test text ps} { + set f [makePSFile] + set res [catch {fileutil::fileType $f} msg] + removePSFile + list $res $msg +} [list 0 [list text ps eps]] + +test fileType-1.18 {test text eps} { + set f [makeEPSFile] + set res [catch {fileutil::fileType $f} msg] + removeEPSFile + list $res $msg +} [list 0 [list text ps eps]] + +test fileType-1.19 {test binary gravity_wave_data_frame} { + set f [makeIgwdFile] + set res [catch {fileutil::fileType $f} msg] + removeIgwdFile + list $res $msg +} [list 0 [list binary gravity_wave_data_frame]] + +test fileType-1.20 {test binary compressed bzip} { + set f [makeBzipFile] + set res [catch {fileutil::fileType $f} msg] + removeBzipFile + list $res $msg +} [list 0 [list binary compressed bzip]] + +test fileType-1.21 {test binary compressed gzip} { + set f [makeGzipFile] + set res [catch {fileutil::fileType $f} msg] + removeGzipFile + list $res $msg +} [list 0 [list binary compressed gzip]] + +test fileType-1.22 {text pdf} { + set f [localPath test-data/pdf4tcl_01.pdf] + set res [catch {fileutil::fileType $f} msg] + list $res $msg +} [list 0 [list text pdf]] + +# ------------------------------------------------------------------------- + +unset path +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/fileutil.man b/tcllib/modules/fileutil/fileutil.man new file mode 100644 index 0000000..370c47d --- /dev/null +++ b/tcllib/modules/fileutil/fileutil.man @@ -0,0 +1,522 @@ +[vset PACKAGE_VERSION 1.15] +[comment {-*- tcl -*- doctools manpage}] +[manpage_begin fileutil n [vset PACKAGE_VERSION]] +[keywords cat] +[keywords {file utilities}] +[keywords grep] +[keywords {temp file}] +[keywords test] +[keywords touch] +[keywords type] +[moddesc {file utilities}] +[titledesc {Procedures implementing some file utilities}] +[category {Programming tools}] +[require Tcl 8] +[require fileutil [opt [vset PACKAGE_VERSION]]] +[description] +[para] + +This package provides implementations of standard unix utilities. + +[list_begin definitions] + +[call [cmd ::fileutil::lexnormalize] [arg path]] + +This command performs purely lexical normalization on the [arg path] and returns +the changed path as its result. Symbolic links in the path are [emph not] resolved. + +[para] +Examples: +[example { + fileutil::lexnormalize /foo/./bar + => /foo/bar + + fileutil::lexnormalize /foo/../bar + => /bar +}] + +[call [cmd ::fileutil::fullnormalize] [arg path]] + +This command resolves all symbolic links in the [arg path] and returns +the changed path as its result. + +In contrast to the builtin [cmd {file normalize}] this command +resolves a symbolic link in the last element of the path as well. + +[call [cmd ::fileutil::test] [arg path] [arg codes] [opt [arg msgvar]] [opt [arg label]]] + +A command for the testing of several properties of a [arg path]. The +properties to test for are specified in [arg codes], either as a list +of keywords describing the properties, or as a string where each +letter is a shorthand for a property to test. The recognized keywords, +shorthands, and associated properties are shown in the list below. The +tests are executed in the order given to the command. + +[para] + +The result of the command is a boolean value. It will be true if and +only if the [arg path] passes all the specified tests. + +In the case of the [arg path] not passing one or more test the first +failing test will leave a message in the variable referenced by + +[arg msgvar], if such is specified. The message will be prefixed with +[arg label], if it is specified. + +[emph Note] that the variabled referenced by [arg msgvar] is not touched at +all if all the tests pass. + +[para] +[list_begin definitions] +[def "[emph r]ead"] +[cmd {file readable}] +[def "[emph w]rite"] +[cmd {file writable}] +[def "[emph e]xists"] +[cmd {file exists}] +[def "e[emph x]ec"] +[cmd {file executable}] +[def "[emph f]ile"] +[cmd {file isfile}] +[def "[emph d]ir"] +[cmd {file isdirectory}] +[list_end] + +[call [cmd ::fileutil::cat] ([opt [arg options]] [arg file])...] + +A tcl implementation of the UNIX [syscmd cat] command. Returns the +contents of the specified file(s). The arguments are files to read, +with interspersed options configuring the process. If there are +problems reading any of the files, an error will occur, and no data +will be returned. + +[para] + +The options accepted are [option -encoding], [option -translation], +[option -eofchar], and [option --]. With the exception of the last all +options take a single value as argument, as specified by the tcl +builtin command [cmd fconfigure]. The [option --] has to be used to +terminate option processing before a file if that file's name begins +with a dash. + +[para] + +Each file can have its own set of options coming before it, and for +anything not specified directly the defaults are inherited from the +options of the previous file. The first file inherits the system +default for unspecified options. + +[call [cmd ::fileutil::writeFile] [opt [arg options]] [arg file] [arg data]] + +The command replaces the current contents of the specified [arg file] +with [arg data], with the process configured by the options. The +command accepts the same options as [cmd ::fileutil::cat]. The +specification of a non-existent file is legal and causes the command +to create the file (and all required but missing directories). + +[call [cmd ::fileutil::appendToFile] [opt [arg options]] [arg file] [arg data]] + +This command is like [cmd ::fileutil::writeFile], except that the +previous contents of [arg file] are not replaced, but appended to. The +command accepts the same options as [cmd ::fileutil::cat] + +[call [cmd ::fileutil::insertIntoFile] [opt [arg options]] [arg file] [arg at] [arg data]] + +This comment is similar to [cmd ::fileutil::appendToFile], except that +the new data is not appended at the end, but inserted at a specified +location within the file. In further contrast this command has to be +given the path to an existing file. It will not create a missing file, +but throw an error instead. + +[para] + +The specified location [arg at] has to be an integer number in the +range [const 0] ... [lb]file size [arg file][rb]. [const 0] will cause +insertion of the new data before the first character of the existing +content, whereas [lb]file size [arg file][rb] causes insertion after +the last character of the existing content, i.e. appending. + +[para] + +The command accepts the same options as [cmd ::fileutil::cat]. + +[call [cmd ::fileutil::removeFromFile] [opt [arg options]] [arg file] [arg at] [arg n]] + +This command is the complement to [cmd ::fileutil::insertIntoFile], removing [arg n] characters from the [arg file], starting at location [arg at]. + +The specified location [arg at] has to be an integer number in the +range [const 0] ... [lb]file size [arg file][rb] - [arg n]. [const 0] +will cause the removal of the new data to start with the first +character of the existing content, + +whereas [lb]file size [arg file][rb] - [arg n] causes the removal of +the tail of the existing content, i.e. the truncation of the file. + +[para] + +The command accepts the same options as [cmd ::fileutil::cat]. + +[call [cmd ::fileutil::replaceInFile] [opt [arg options]] [arg file] [arg at] [arg n] [arg data]] + +This command is a combination of [cmd ::fileutil::removeFromFile] and +[cmd ::fileutil::insertIntoFile]. It first removes the part of the +contents specified by the arguments [arg at] and [arg n], and then +inserts [arg data] at the given location, effectively replacing the +removed by content with [arg data]. + +All constraints imposed on [arg at] and [arg n] by +[cmd ::fileutil::removeFromFile] and [cmd ::fileutil::insertIntoFile] +are obeyed. + +[para] + +The command accepts the same options as [cmd ::fileutil::cat]. + +[call [cmd ::fileutil::updateInPlace] [opt [arg options]] [arg file] [arg cmd]] + +This command can be seen as the generic core functionality of +[cmd ::fileutil::replaceInFile]. + +It first reads the contents of the specified [arg file], then runs the +command prefix [arg cmd] with that data appended to it, and at last +writes the result of that invokation back as the new contents of the +file. + +[para] + +If the executed command throws an error the [arg file] is not changed. + +[para] + +The command accepts the same options as [cmd ::fileutil::cat]. + +[call [cmd ::fileutil::fileType] [arg filename]] + +An implementation of the UNIX [syscmd file] command, which uses +various heuristics to guess the type of a file. Returns a list +specifying as much type information as can be determined about the +file, from most general (eg, "binary" or "text") to most specific (eg, +"gif"). For example, the return value for a GIF file would be "binary +graphic gif". The command will detect the following types of files: +directory, empty, binary, text, script (with interpreter), executable +elf, executable dos, executable ne, executable pe, graphic gif, graphic +jpeg, graphic png, graphic tiff, graphic bitmap, html, xml (with doctype +if available), message pgp, binary pdf, text ps, text eps, binary +gravity_wave_data_frame, compressed bzip, compressed gzip, compressed +zip, compressed tar, audio wave, audio mpeg, and link. It further +detects doctools, doctoc, and docidx documentation files, and +tklib diagrams. + +[call [cmd ::fileutil::find] [opt "[arg basedir] [opt [arg filtercmd]]"]] + +An implementation of the unix command [syscmd find]. Adapted from the +Tcler's Wiki. Takes at most two arguments, the path to the directory +to start searching from and a command to use to evaluate interest in +each file. The path defaults to [file .], i.e. the current +directory. The command defaults to the empty string, which means that +all files are of interest. The command takes care [emph not] to +lose itself in infinite loops upon encountering circular link +structures. The result of the command is a list containing the paths +to the interesting files. + +[para] + +The [arg filtercmd], if specified, is interpreted as a command prefix +and one argument is added to it, the name of the file or directory +find is currently looking at. Note that this name is [emph not] fully +qualified. It has to be joined it with the result of [cmd pwd] to get +an absolute filename. + +[para] + +The result of [arg filtercmd] is a boolean value that indicates if the +current file should be included in the list of interesting files. + +[para] +Example: +[para] +[example { + # find .tcl files + package require fileutil + proc is_tcl {name} {return [string match *.tcl $name]} + set tcl_files [fileutil::find . is_tcl] +}] + +[call [cmd ::fileutil::findByPattern] [arg basedir] \ + [opt [option -regexp]|[option -glob]] [opt [option --]] \ + [arg patterns]] + +This command is based upon the [package TclX] command + +[cmd recursive_glob], except that it doesn't allow recursion over more +than one directory at a time. It uses [cmd ::fileutil::find] +internally and is thus able to and does follow symbolic links, +something the [package TclX] command does not do. First argument is +the directory to start the search in, second argument is a list of +[arg patterns]. The command returns a list of all files reachable +through [arg basedir] whose names match at least one of the +patterns. The options before the pattern-list determine the style of +matching, either regexp or glob. glob-style matching is the default if +no options are given. Usage of the option [option --] stops option +processing. This allows the use of a leading '-' in the patterns. + +[call [cmd ::fileutil::foreachLine] [arg {var filename cmd}]] + +The command reads the file [arg filename] and executes the script + +[arg cmd] for every line in the file. During the execution of the +script the variable [arg var] is set to the contents of the current +line. The return value of this command is the result of the last +invocation of the script [arg cmd] or the empty string if the file was +empty. + +[call [cmd ::fileutil::grep] [arg pattern] [opt [arg files]]] + +Implementation of [syscmd grep]. Adapted from the Tcler's Wiki. The +first argument defines the [arg pattern] to search for. This is +followed by a list of [arg files] to search through. The list is +optional and [const stdin] will be used if it is missing. The result +of the procedures is a list containing the matches. Each match is a +single element of the list and contains filename, number and contents +of the matching line, separated by a colons. + +[call [cmd ::fileutil::install] [opt "[option -m] [arg "mode"]"] [arg source] [arg destination]] + +The [cmd install] command is similar in functionality to the [syscmd install] +command found on many unix systems, or the shell script +distributed with many source distributions (unix/install-sh in the Tcl +sources, for example). It copies [arg source], which can be either a +file or directory to [arg destination], which should be a directory, +unless [arg source] is also a single file. The [opt -m] option lets +the user specify a unix-style mode (either octal or symbolic - see +[cmd {file attributes}]. + +[call [cmd ::fileutil::stripN] [arg path] [arg n]] + +Removes the first [arg n] elements from the specified [arg path] and +returns the modified path. If [arg n] is greater than the number of +components in [arg path] an empty string is returned. The number of +components in a given path may be determined by performing +[cmd llength] on the list returned by [cmd {file split}]. + +[call [cmd ::fileutil::stripPwd] [arg path]] + +If, and only if the [arg path] is inside of the directory returned by +[lb][cmd pwd][rb] (or the current working directory itself) it is made +relative to that directory. In other words, the current working +directory is stripped from the [arg path]. The possibly modified path +is returned as the result of the command. If the current working +directory itself was specified for [arg path] the result is the string +"[const .]". + +[call [cmd ::fileutil::stripPath] [arg prefix] [arg path]] + +If, and only of the [arg path] is inside of the directory + +[file prefix] (or the prefix directory itself) it is made relative to +that directory. In other words, the prefix directory is stripped from +the [arg path]. The possibly modified path is returned as the result +of the command. + +If the prefix directory itself was specified for [arg path] the result +is the string "[const .]". + +[call [cmd ::fileutil::jail] [arg jail] [arg path]] + +This command ensures that the [arg path] is not escaping the directory +[arg jail]. It always returns an absolute path derived from [arg path] +which is within [arg jail]. + +[para] + +If [arg path] is an absolute path and already within [arg jail] it is +returned unmodified. + +[para] + +An absolute path outside of [arg jail] is stripped of its root element +and then put into the [arg jail] by prefixing it with it. The same +happens if [arg path] is relative, except that nothing is stripped of +it. Before adding the [arg jail] prefix the [arg path] is lexically +normalized to prevent the caller from using [const ..] segments in +[arg path] to escape the jail. + +[call [cmd ::fileutil::touch] [opt [option -a]] [opt [option -c]] [opt [option -m]] [opt "[option -r] [arg ref_file]"] [opt "[option -t] [arg time]"] [arg filename] [opt [arg ...]]] + +Implementation of [syscmd touch]. Alter the atime and mtime of the +specified files. If [option -c], do not create files if they do not +already exist. If [option -r], use the atime and mtime from + +[arg ref_file]. If [option -t], use the integer clock value + +[arg time]. It is illegal to specify both [option -r] and + +[option -t]. If [option -a], only change the atime. If [option -m], +only change the mtime. + +[para] +[emph {This command is not available for Tcl versions less than 8.3.}] + +[call [cmd ::fileutil::tempdir]] + +The command returns the path of a directory where the caller can +place temporary files, such as [file /tmp] on Unix systems. The +algorithm we use to find the correct directory is as follows: + +[list_begin enumerated] + +[enum] +The directory set by an invokation of [cmd ::fileutil::tempdir] with +an argument. If this is present it is tried exclusively and none of +the following item are tried. + +[enum] +The directory named in the TMPDIR environment variable. + +[enum] +The directory named in the TEMP environment variable. + +[enum] +The directory named in the TMP environment variable. + +[enum] +A platform specific location: + +[list_begin definitions] +[def {Windows}] + +[file "C:\\TEMP"], [file "C:\\TMP"], [file "\\TEMP"], +and [file "\\TMP"] are tried in that order. + +[def {(classic) Macintosh}] + +The TRASH_FOLDER environment variable is used. This is most likely +not correct. + +[def {Unix}] + +The directories [file /tmp], [file /var/tmp], and [file /usr/tmp] are +tried in that order. + +[list_end] +[list_end] +[para] + +The algorithm utilized is mainly that used in the Python standard +library. The exception is the first item, the ability to have the +search overridden by a user-specified directory. + +[call [cmd ::fileutil::tempdir] [arg path]] + +In this mode the command sets the [arg path] as the first and only +directory to try as a temp. directory. See the previous item for the +use of the set directory. The command returns the empty string. + +[call [cmd ::fileutil::tempdirReset]] + +Invoking this command clears the information set by the +last call of [lb][cmd ::fileutil::tempdir] [arg path][rb]. +See the last item too. + +[call [cmd ::fileutil::tempfile] [opt [arg prefix]]] + +The command generates a temporary file name suitable for writing to, +and the associated file. The file name will be unique, and the file +will be writable and contained in the appropriate system specific temp +directory. The name of the file will be returned as the result of the +command. + +[para] + +The code was taken from [uri http://wiki.tcl.tk/772], attributed to +Igor Volobouev and anon. + +[call [cmd ::fileutil::maketempdir] \ + [opt "[option -prefix] [arg str]"] \ + [opt "[option -suffix] [arg str]"] \ + [opt "[option -dir] [arg str]"]] + +The command generates a temporary directory suitable for writing to. +The directory name will be unique, and the directory will be writable +and contained in the appropriate system specific temp directory. The +name of the directory will be returned as the result of the command. + +[para] The three options can used to tweak the behaviour of the command: + +[list_begin options] +[opt_def -prefix str] The initial, fixed part of the directory name. Defaults to [const tmp] if not specified. +[opt_def -suffix str] The fixed tail of the directory. Defaults to the empty string if not specified. +[opt_def -dir str] The directory to place the new directory into. Defaults to the result of [cmd fileutil::tempdir] if not specified. +[list_end] + +[para]The initial code for this was supplied by [uri mailto:aplicacionamedida@gmail.com {Miguel Martinez Lopez}]. + +[call [cmd ::fileutil::relative] [arg base] [arg dst]] + +This command takes two directory paths, both either absolute or relative +and computes the path of [arg dst] relative to [arg base]. This relative +path is returned as the result of the command. As implied in the previous +sentence, the command is not able to compute this relationship between the +arguments if one of the paths is absolute and the other relative. + +[para] + +[emph Note:] The processing done by this command is purely lexical. +Symbolic links are [emph not] taken into account. + +[call [cmd ::fileutil::relativeUrl] [arg base] [arg dst]] + +This command takes two file paths, both either absolute or relative +and computes the path of [arg dst] relative to [arg base], as seen +from inside of the [arg base]. This is the algorithm how a browser +resolves a relative link found in the currently shown file. + +[para] + +The computed relative path is returned as the result of the command. +As implied in the previous sentence, the command is not able to compute +this relationship between the arguments if one of the paths is absolute +and the other relative. + +[para] + +[emph Note:] The processing done by this command is purely lexical. +Symbolic links are [emph not] taken into account. + +[list_end] + +[section {Warnings and Incompatibilities}] + +[list_begin definitions] + +[def [const 1.14.9]] +In this version [cmd fileutil::find]'s broken system for handling +symlinks was replaced with one working correctly and properly +enumerating all the legal non-cyclic paths under a base directory. + +[para] While correct this means that certain pathological directory +hierarchies with cross-linked sym-links will now take about O(n**2) +time to enumerate whereas the original broken code managed O(n) due to +its brokenness. + +[para] A concrete example and extreme case is the [file /sys] +hierarchy under Linux where some hundred devices exist under both +[file /sys/devices] and [file /sys/class] with the two sub-hierarchies +linking to the other, generating millions of legal paths to enumerate. +The structure, reduced to three devices, roughly looks like + +[include include/cross-index.inc] + +[para] The command [cmd fileutil::find] currently has no way to escape +this. When having to handle such a pathological hierarchy It is +recommended to switch to package [package fileutil::traverse] and the +same-named command it provides, and then use the [option -prefilter] +option to prevent the traverser from following symbolic links, like so: + +[include include/cross-index-trav.inc] + +[list_end] + +[vset CATEGORY fileutil] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/fileutil/fileutil.tcl b/tcllib/modules/fileutil/fileutil.tcl new file mode 100644 index 0000000..b72864d --- /dev/null +++ b/tcllib/modules/fileutil/fileutil.tcl @@ -0,0 +1,2295 @@ +# fileutil.tcl -- +# +# Tcl implementations of standard UNIX utilities. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2002 by Phil Ehrens <phil@slug.org> (fileType) +# Copyright (c) 2005-2013 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# RCS: @(#) $Id: fileutil.tcl,v 1.78 2010/06/17 04:46:19 andreas_kupries Exp $ + +package require Tcl 8.2 +package require cmdline +package provide fileutil 1.15 + +namespace eval ::fileutil { + namespace export \ + grep find findByPattern cat touch foreachLine \ + jail stripPwd stripN stripPath tempdir tempfile \ + install fileType writeFile appendToFile \ + insertIntoFile removeFromFile replaceInFile \ + updateInPlace test tempdirReset maketempdir +} + +# ::fileutil::grep -- +# +# Implementation of grep. Adapted from the Tcler's Wiki. +# +# Arguments: +# pattern pattern to search for. +# files list of files to search; if NULL, uses stdin. +# +# Results: +# results list of matches + +proc ::fileutil::grep {pattern {files {}}} { + set result [list] + if {[llength $files] == 0} { + # read from stdin + set lnum 0 + while {[gets stdin line] >= 0} { + incr lnum + if {[regexp -- $pattern $line]} { + lappend result "${lnum}:${line}" + } + } + } else { + foreach filename $files { + set file [open $filename r] + set lnum 0 + while {[gets $file line] >= 0} { + incr lnum + if {[regexp -- $pattern $line]} { + lappend result "${filename}:${lnum}:${line}" + } + } + close $file + } + } + return $result +} + +# ::fileutil::find == + +# Below is the core command, which is portable across Tcl versions and +# platforms. Functionality which is common or platform and/or Tcl +# version dependent, has been factored out/ encapsulated into separate +# (small) commands. Only these commands may have multiple variant +# implementations per the available features of the Tcl core / +# platform. +# +# These commands are +# +# FADD - Add path result, performs filtering. Portable! +# GLOBF - Return files in a directory. Tcl version/platform dependent. +# GLOBD - Return dirs in a directory. Tcl version/platform dependent. +# ACCESS - Check directory for accessibility. Tcl version/platform dependent. + +proc ::fileutil::find {{basedir .} {filtercmd {}}} { + set result {} + set filt [string length $filtercmd] + + if {[file isfile $basedir]} { + # The base is a file, and therefore only possible result, + # modulo filtering. + + FADD $basedir + + } elseif {[file isdirectory $basedir]} { + # For a directory as base we do an iterative recursion through + # the directory hierarchy starting at the base. We use a queue + # (Tcl list) of directories we have to check. We access it by + # index, and stop when we have reached beyond the end of the + # list. This is faster than removing elements from the be- + # ginning of the list, as that entails copying down a possibly + # large list of directories, making it O(n*n). The index is + # faster, O(n), at the expense of memory. Nothing is deleted + # from the list until we have processed all directories in the + # hierarchy. + # + # We scan each directory at least twice. First for files, then + # for directories. The scans may internally make several + # passes (normal vs hidden files). + # + # Looped directory structures due to symbolic links are + # handled by _fully_ normalizing directory paths and checking + # if we encountered the normalized form before. The array + # 'known' is our cache where we record the known normalized + # paths. + + set pending [list $basedir] + set at 0 + array set parent {} + array set norm {} + Enter {} $basedir + + while {$at < [llength $pending]} { + # Get next directory not yet processed. + set current [lindex $pending $at] + incr at + + # Is the directory accessible? Continue if not. + ACCESS $current + + # Files first, then the sub-directories ... + + foreach f [GLOBF $current] { FADD $f } + + foreach f [GLOBD $current] { + # Ignore current and parent directory, this needs + # explicit filtering outside of the filter command. + if { + [string equal [file tail $f] "."] || + [string equal [file tail $f] ".."] + } continue + + # Extend result, modulo filtering. + FADD $f + + # Detection of symlink loops via a portable path + # normalization computing a canonical form of the path + # followed by a check if that canonical form was + # encountered before. If ok, record directory for + # expansion in future iterations. + + Enter $current $f + if {[Cycle $f]} continue + + lappend pending $f + } + } + } else { + return -code error "$basedir does not exist" + } + + return $result +} + +proc ::fileutil::Enter {parent path} { + upvar 1 parent _parent norm _norm + set _parent($path) $parent + set _norm($path) [fullnormalize $path] + return +} + +proc ::fileutil::Cycle {path} { + upvar 1 parent _parent norm _norm + set nform $_norm($path) + set paren $_parent($path) + while {$paren ne {}} { + if {$_norm($paren) eq $nform} { return yes } + set paren $_parent($paren) + } + return no +} + +# Helper command for fileutil::find. Performs the filtering of the +# result per a filter command for the candidates found by the +# traversal core, see above. This is portable. + +proc ::fileutil::FADD {filename} { + upvar 1 result result filt filt filtercmd filtercmd + if {!$filt} { + lappend result $filename + return + } + + set here [pwd] + cd [file dirname $filename] + + if {[uplevel 2 [linsert $filtercmd end [file tail $filename]]]} { + lappend result $filename + } + + cd $here + return +} + +# The next three helper commands for fileutil::find depend strongly on +# the version of Tcl, and partially on the platform. + +# 1. The -directory and -types switches were added to glob in Tcl +# 8.3. This means that we have to emulate them for Tcl 8.2. +# +# 2. In Tcl 8.3 using -types f will return only true files, but not +# links to files. This changed in 8.4+ where links to files are +# returned as well. So for 8.3 we have to handle the links +# separately (-types l) and also filter on our own. +# Note that Windows file links are hard links which are reported by +# -types f, but not -types l, so we can optimize that for the two +# platforms. +# +# Note further that we have to handle broken links on our own. They +# are not returned by glob yet we want them in the output. +# +# 3. In Tcl 8.3 we also have a crashing bug in glob (SIGABRT, "stat on +# a known file") when trying to perform 'glob -types {hidden f}' on +# a directory without e'x'ecute permissions. We code around by +# testing if we can cd into the directory (stat might return enough +# information too (mode), but possibly also not portable). +# +# For Tcl 8.2 and 8.4+ glob simply delivers an empty result +# (-nocomplain), without crashing. For them this command is defined +# so that the bytecode compiler removes it from the bytecode. +# +# This bug made the ACCESS helper necessary. +# We code around the problem by testing if we can cd into the +# directory (stat might return enough information too (mode), but +# possibly also not portable). + +if {[package vsatisfies [package present Tcl] 8.5]} { + # Tcl 8.5+. + # We have to check readability of "current" on our own, glob + # changed to error out instead of returning nothing. + + proc ::fileutil::ACCESS {args} {} + + proc ::fileutil::GLOBF {current} { + if {![file readable $current] || + [BadLink $current]} { + return {} + } + + set res [lsort -unique [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]]] + + # Look for broken links (They are reported as neither file nor directory). + foreach l [lsort -unique [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]]] { + if {[file isfile $l]} continue + if {[file isdirectory $l]} continue + lappend res $l + } + return [lsort -unique $res] + } + + proc ::fileutil::GLOBD {current} { + if {![file readable $current] || + [BadLink $current]} { + return {} + } + + lsort -unique [concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *]] + } + + proc ::fileutil::BadLink {current} { + if {[file type $current] ne "link"} { return no } + + set dst [file join [file dirname $current] [file readlink $current]] + + if {![file exists $dst] || + ![file readable $dst]} { + return yes + } + + return no + } +} elseif {[package vsatisfies [package present Tcl] 8.4]} { + # Tcl 8.4+. + # (Ad 1) We have -directory, and -types, + # (Ad 2) Links are returned for -types f/d if they refer to files/dirs. + # (Ad 3) No bug to code around + + proc ::fileutil::ACCESS {args} {} + + proc ::fileutil::GLOBF {current} { + set res [lsort -unique [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]]] + + # Look for broken links (They are reported as neither file nor directory). + foreach l [lsort -unique [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]]] { + if {[file isfile $l]} continue + if {[file isdirectory $l]} continue + lappend res $l + } + return [lsort -unique $res] + } + + proc ::fileutil::GLOBD {current} { + lsort -unique [concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *]] + } + +} elseif {[package vsatisfies [package present Tcl] 8.3]} { + # 8.3. + # (Ad 1) We have -directory, and -types, + # (Ad 2) Links are NOT returned for -types f/d, collect separately. + # No symbolic file links on Windows. + # (Ad 3) Bug to code around. + + proc ::fileutil::ACCESS {current} { + if {[catch { + set h [pwd] ; cd $current ; cd $h + }]} {return -code continue} + return + } + + if {[string equal $::tcl_platform(platform) windows]} { + proc ::fileutil::GLOBF {current} { + concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]] + } + } else { + proc ::fileutil::GLOBF {current} { + set l [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]] + + foreach x [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]] { + if {[file isdirectory $x]} continue + # We have now accepted files, links to files, and broken links. + lappend l $x + } + + return $l + } + } + + proc ::fileutil::GLOBD {current} { + set l [concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *]] + + foreach x [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]] { + if {![file isdirectory $x]} continue + lappend l $x + } + + return $l + } +} else { + # 8.2. + # (Ad 1,2,3) We do not have -directory, nor -types. Full emulation required. + + proc ::fileutil::ACCESS {args} {} + + if {[string equal $::tcl_platform(platform) windows]} { + # Hidden files cannot be handled by Tcl 8.2 in glob. We have + # to punt. + + proc ::fileutil::GLOBF {current} { + set current \\[join [split $current {}] \\] + set res {} + foreach x [glob -nocomplain -- [file join $current *]] { + if {[file isdirectory $x]} continue + if {[catch {file type $x}]} continue + # We have now accepted files, links to files, and + # broken links. We may also have accepted a directory + # as well, if the current path was inaccessible. This + # however will cause 'file type' to throw an error, + # hence the second check. + lappend res $x + } + return $res + } + + proc ::fileutil::GLOBD {current} { + set current \\[join [split $current {}] \\] + set res {} + foreach x [glob -nocomplain -- [file join $current *]] { + if {![file isdirectory $x]} continue + lappend res $x + } + return $res + } + } else { + # Hidden files on Unix are dot-files. We emulate the switch + # '-types hidden' by using an explicit pattern. + + proc ::fileutil::GLOBF {current} { + set current \\[join [split $current {}] \\] + set res {} + foreach x [glob -nocomplain -- [file join $current *] [file join $current .*]] { + if {[file isdirectory $x]} continue + if {[catch {file type $x}]} continue + # We have now accepted files, links to files, and + # broken links. We may also have accepted a directory + # as well, if the current path was inaccessible. This + # however will cause 'file type' to throw an error, + # hence the second check. + + lappend res $x + } + return $res + } + + proc ::fileutil::GLOBD {current} { + set current \\[join [split $current {}] \\] + set res {} + foreach x [glob -nocomplain -- $current/* [file join $current .*]] { + if {![file isdirectory $x]} continue + lappend res $x + } + return $res + } + } +} + +# ::fileutil::findByPattern -- +# +# Specialization of find. Finds files based on their names, +# which have to match the specified patterns. Options are used +# to specify which type of patterns (regexp-, glob-style) is +# used. +# +# Arguments: +# basedir Directory to start searching from. +# args Options (-glob, -regexp, --) followed by a +# list of patterns to search for. +# +# Results: +# files a list of interesting files. + +proc ::fileutil::findByPattern {basedir args} { + set pos 0 + set cmd ::fileutil::FindGlob + foreach a $args { + incr pos + switch -glob -- $a { + -- {break} + -regexp {set cmd ::fileutil::FindRegexp} + -glob {set cmd ::fileutil::FindGlob} + -* {return -code error "Unknown option $a"} + default {incr pos -1 ; break} + } + } + + set args [lrange $args $pos end] + + if {[llength $args] != 1} { + set pname [lindex [info level 0] 0] + return -code error \ + "wrong#args for \"$pname\", should be\ + \"$pname basedir ?-regexp|-glob? ?--? patterns\"" + } + + set patterns [lindex $args 0] + return [find $basedir [list $cmd $patterns]] +} + + +# ::fileutil::FindRegexp -- +# +# Internal helper. Filter command used by 'findByPattern' +# to match files based on regular expressions. +# +# Arguments: +# patterns List of regular expressions to match against. +# filename Name of the file to match against the patterns. +# Results: +# interesting A boolean flag. Set to true if the file +# matches at least one of the patterns. + +proc ::fileutil::FindRegexp {patterns filename} { + foreach p $patterns { + if {[regexp -- $p $filename]} { + return 1 + } + } + return 0 +} + +# ::fileutil::FindGlob -- +# +# Internal helper. Filter command used by 'findByPattern' +# to match files based on glob expressions. +# +# Arguments: +# patterns List of glob expressions to match against. +# filename Name of the file to match against the patterns. +# Results: +# interesting A boolean flag. Set to true if the file +# matches at least one of the patterns. + +proc ::fileutil::FindGlob {patterns filename} { + foreach p $patterns { + if {[string match $p $filename]} { + return 1 + } + } + return 0 +} + +# ::fileutil::stripPwd -- +# +# If the specified path references is a path in [pwd] (or [pwd] itself) it +# is made relative to [pwd]. Otherwise it is left unchanged. +# In the case of [pwd] itself the result is the string '.'. +# +# Arguments: +# path path to modify +# +# Results: +# path The (possibly) modified path. + +proc ::fileutil::stripPwd {path} { + + # [file split] is used to generate a canonical form for both + # paths, for easy comparison, and also one which is easy to modify + # using list commands. + + set pwd [pwd] + if {[string equal $pwd $path]} { + return "." + } + + set pwd [file split $pwd] + set npath [file split $path] + + if {[string match ${pwd}* $npath]} { + set path [eval [linsert [lrange $npath [llength $pwd] end] 0 file join ]] + } + return $path +} + +# ::fileutil::stripN -- +# +# Removes N elements from the beginning of the path. +# +# Arguments: +# path path to modify +# n number of elements to strip +# +# Results: +# path The modified path + +proc ::fileutil::stripN {path n} { + set path [file split $path] + if {$n >= [llength $path]} { + return {} + } else { + return [eval [linsert [lrange $path $n end] 0 file join]] + } +} + +# ::fileutil::stripPath -- +# +# If the specified path references/is a path in prefix (or prefix itself) it +# is made relative to prefix. Otherwise it is left unchanged. +# In the case of it being prefix itself the result is the string '.'. +# +# Arguments: +# prefix prefix to strip from the path. +# path path to modify +# +# Results: +# path The (possibly) modified path. + +if {[string equal $tcl_platform(platform) windows]} { + + # Windows. While paths are stored with letter-case preserved al + # comparisons have to be done case-insensitive. For reference see + # SF Tcllib Bug 2499641. + + proc ::fileutil::stripPath {prefix path} { + # [file split] is used to generate a canonical form for both + # paths, for easy comparison, and also one which is easy to modify + # using list commands. + + set prefix [file split $prefix] + set npath [file split $path] + + if {[string equal -nocase $prefix $npath]} { + return "." + } + + if {[string match -nocase "${prefix} *" $npath]} { + set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]] + } + return $path + } +} else { + proc ::fileutil::stripPath {prefix path} { + # [file split] is used to generate a canonical form for both + # paths, for easy comparison, and also one which is easy to modify + # using list commands. + + set prefix [file split $prefix] + set npath [file split $path] + + if {[string equal $prefix $npath]} { + return "." + } + + if {[string match "${prefix} *" $npath]} { + set path [eval [linsert [lrange $npath [llength $prefix] end] 0 file join ]] + } + return $path + } +} + +# ::fileutil::jail -- +# +# Ensures that the input path 'filename' stays within the the +# directory 'jail'. In this way it preventsuser-supplied paths +# from escaping the jail. +# +# Arguments: +# jail The path to the directory the other must +# not escape from. +# filename The path to prevent from escaping. +# +# Results: +# path The (possibly) modified path surely within +# the confines of the jail. + +proc fileutil::jail {jail filename} { + if {![string equal [file pathtype $filename] "relative"]} { + # Although the path to check is absolute (or volumerelative on + # windows) we cannot perform a simple prefix check to see if + # the path is inside the jail or not. We have to normalize + # both path and jail and then we can check. If the path is + # outside we make the original path relative and prefix it + # with the original jail. We do make the jail pseudo-absolute + # by prefixing it with the current working directory for that. + + # Normalized jail. Fully resolved sym links, if any. Our main + # complication is that normalize does not resolve symlinks in the + # last component of the path given to it, so we add a bogus + # component, resolve, and then strip it off again. That is why the + # code is so large and long. + + set njail [eval [list file join] [lrange [file split \ + [Normalize [file join $jail __dummy__]]] 0 end-1]] + + # Normalize filename. Fully resolved sym links, if + # any. S.a. for an explanation of the complication. + + set nfile [eval [list file join] [lrange [file split \ + [Normalize [file join $filename __dummy__]]] 0 end-1]] + + if {[string match ${njail}* $nfile]} { + return $filename + } + + # Outside the jail, put it inside. ... We normalize the input + # path lexically for this, to prevent escapes still lurking in + # the original path. (We cannot use the normalized path, + # symlinks may have bent it out of shape in unrecognizable ways. + + return [eval [linsert [lrange [file split \ + [lexnormalize $filename]] 1 end] 0 file join [pwd] $jail]] + } else { + # The path is relative, consider it as outside + # implicitly. Normalize it lexically! to prevent escapes, then + # put the jail in front, use PWD to ensure absoluteness. + + return [eval [linsert [file split [lexnormalize $filename]] 0 \ + file join [pwd] $jail]] + } +} + + +# ::fileutil::test -- +# +# Simple API to testing various properties of +# a path (read, write, file/dir, existence) +# +# Arguments: +# path path to test +# codes names of the properties to test +# msgvar Name of variable to leave an error +# message in. Optional. +# label Label for error message, optional +# +# Results: +# ok boolean flag, set if the path passes +# all tests. + +namespace eval ::fileutil { + variable test + array set test { + read {readable {Read access is denied}} + write {writable {Write access is denied}} + exec {executable {Is not executable}} + exists {exists {Does not exist}} + file {isfile {Is not a file}} + dir {isdirectory {Is not a directory}} + } +} + +proc ::fileutil::test {path codes {msgvar {}} {label {}}} { + variable test + + if {[string equal $msgvar ""]} { + set msg "" + } else { + upvar 1 $msgvar msg + } + + if {![string equal $label ""]} {append label { }} + + if {![regexp {^(read|write|exec|exists|file|dir)} $codes]} { + # Translate single characters into proper codes + set codes [string map { + r read w write e exists x exec f file d dir + } [split $codes {}]] + } + + foreach c $codes { + foreach {cmd text} $test($c) break + if {![file $cmd $path]} { + set msg "$label\"$path\": $text" + return 0 + } + } + + return 1 +} + +# ::fileutil::cat -- +# +# Tcl implementation of the UNIX "cat" command. Returns the contents +# of the specified files. +# +# Arguments: +# args names of the files to read, interspersed with options +# to set encodings, translations, or eofchar. +# +# Results: +# data data read from the file. + +proc ::fileutil::cat {args} { + # Syntax: (?options? file)+ + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + if {![llength $args]} { + # Argument processing stopped with arguments missing. + return -code error \ + "wrong#args: should be\ + [lindex [info level 0] 0] ?-eofchar|-translation|-encoding arg?+ file ..." + } + + # We go through the arguments using foreach and keeping track of + # the index we are at. We do not shift the arguments out to the + # left. That is inherently quadratic, copying everything down. + + set opts {} + set mode maybeopt + set channels {} + + foreach a $args { + if {[string equal $mode optarg]} { + lappend opts $a + set mode maybeopt + continue + } elseif {[string equal $mode maybeopt]} { + if {[string match -* $a]} { + switch -exact -- $a { + -encoding - + -translation - + -eofchar { + lappend opts $a + set mode optarg + continue + } + -- { + set mode file + continue + } + default { + return -code error \ + "Bad option \"$a\",\ + expected one of\ + -encoding, -eofchar,\ + or -translation" + } + } + } + # Not an option, but a file. Change mode and fall through. + set mode file + } + # Process file arguments + + if {[string equal $a -]} { + # Stdin reference is special. + + # Test that the current options are all ok. + # For stdin we have to avoid closing it. + + set old [fconfigure stdin] + set fail [catch { + SetOptions stdin $opts + } msg] ; # {} + SetOptions stdin $old + + if {$fail} { + return -code error $msg + } + + lappend channels [list $a $opts 0] + } else { + if {![file exists $a]} { + return -code error "Cannot read file \"$a\", does not exist" + } elseif {![file isfile $a]} { + return -code error "Cannot read file \"$a\", is not a file" + } elseif {![file readable $a]} { + return -code error "Cannot read file \"$a\", read access is denied" + } + + # Test that the current options are all ok. + set c [open $a r] + set fail [catch { + SetOptions $c $opts + } msg] ; # {} + close $c + if {$fail} { + return -code error $msg + } + + lappend channels [list $a $opts [file size $a]] + } + + # We may have more options and files coming after. + set mode maybeopt + } + + if {![string equal $mode maybeopt]} { + # Argument processing stopped with arguments missing. + return -code error \ + "wrong#args: should be\ + [lindex [info level 0] 0] ?-eofchar|-translation|-encoding arg?+ file ..." + } + + set data "" + foreach c $channels { + foreach {fname opts size} $c break + + if {[string equal $fname -]} { + set old [fconfigure stdin] + SetOptions stdin $opts + append data [read stdin] + SetOptions stdin $old + continue + } + + set c [open $fname r] + SetOptions $c $opts + + if {$size > 0} { + # Used the [file size] command to get the size, which + # preallocates memory, rather than trying to grow it as + # the read progresses. + append data [read $c $size] + } else { + # if the file has zero bytes it is either empty, or + # something where [file size] reports 0 but the file + # actually has data (like the files in the /proc + # filesystem on Linux). + append data [read $c] + } + close $c + } + + return $data +} + +# ::fileutil::writeFile -- +# +# Write the specified data into the named file, +# creating it if necessary. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to write. +# data The data to write into the file +# +# Results: +# None. + +proc ::fileutil::writeFile {args} { + # Syntax: ?options? file data + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec Writable $args opts fname data + + # Now perform the requested operation. + + file mkdir [file dirname $fname] + set c [open $fname w] + SetOptions $c $opts + puts -nonewline $c $data + close $c + return +} + +# ::fileutil::appendToFile -- +# +# Append the specified data at the end of the named file, +# creating it if necessary. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to extend. +# data The data to extend the file with. +# +# Results: +# None. + +proc ::fileutil::appendToFile {args} { + # Syntax: ?options? file data + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec Writable $args opts fname data + + # Now perform the requested operation. + + file mkdir [file dirname $fname] + set c [open $fname a] + SetOptions $c $opts + set at [tell $c] + puts -nonewline $c $data + close $c + return $at +} + +# ::fileutil::insertIntoFile -- +# +# Insert the specified data into the named file, +# creating it if necessary, at the given locaton. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to extend. +# data The data to extend the file with. +# +# Results: +# None. + +proc ::fileutil::insertIntoFile {args} { + + # Syntax: ?options? file at data + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec ReadWritable $args opts fname at data + + set max [file size $fname] + CheckLocation $at $max insertion + + if {[string length $data] == 0} { + # Another degenerate case, inserting nothing. + # Leave the file well enough alone. + return + } + + foreach {c o t} [Open2 $fname $opts] break + + # The degenerate cases of both appending and insertion at the + # beginning of the file allow more optimized implementations of + # the operation. + + if {$at == 0} { + puts -nonewline $o $data + fcopy $c $o + } elseif {$at == $max} { + fcopy $c $o + puts -nonewline $o $data + } else { + fcopy $c $o -size $at + puts -nonewline $o $data + fcopy $c $o + } + + Close2 $fname $t $c $o + return +} + +# ::fileutil::removeFromFile -- +# +# Remove n characters from the named file, +# starting at the given locaton. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to extend. +# at Location to start the removal from. +# n Number of characters to remove. +# +# Results: +# None. + +proc ::fileutil::removeFromFile {args} { + + # Syntax: ?options? file at n + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec ReadWritable $args opts fname at n + + set max [file size $fname] + CheckLocation $at $max removal + CheckLength $n $at $max removal + + if {$n == 0} { + # Another degenerate case, removing nothing. + # Leave the file well enough alone. + return + } + + foreach {c o t} [Open2 $fname $opts] break + + # The degenerate cases of both removal from the beginning or end + # of the file allow more optimized implementations of the + # operation. + + if {$at == 0} { + seek $c $n current + fcopy $c $o + } elseif {($at + $n) == $max} { + fcopy $c $o -size $at + # Nothing further to copy. + } else { + fcopy $c $o -size $at + seek $c $n current + fcopy $c $o + } + + Close2 $fname $t $c $o + return +} + +# ::fileutil::replaceInFile -- +# +# Remove n characters from the named file, +# starting at the given locaton, and replace +# it with the given data. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to extend. +# at Location to start the removal from. +# n Number of characters to remove. +# data The replacement data. +# +# Results: +# None. + +proc ::fileutil::replaceInFile {args} { + + # Syntax: ?options? file at n data + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec ReadWritable $args opts fname at n data + + set max [file size $fname] + CheckLocation $at $max replacement + CheckLength $n $at $max replacement + + if { + ($n == 0) && + ([string length $data] == 0) + } { + # Another degenerate case, replacing nothing with + # nothing. Leave the file well enough alone. + return + } + + foreach {c o t} [Open2 $fname $opts] break + + # Check for degenerate cases and handle them separately, + # i.e. strip the no-op parts out of the general implementation. + + if {$at == 0} { + if {$n == 0} { + # Insertion instead of replacement. + + puts -nonewline $o $data + fcopy $c $o + + } elseif {[string length $data] == 0} { + # Removal instead of replacement. + + seek $c $n current + fcopy $c $o + + } else { + # General replacement at front. + + seek $c $n current + puts -nonewline $o $data + fcopy $c $o + } + } elseif {($at + $n) == $max} { + if {$n == 0} { + # Appending instead of replacement + + fcopy $c $o + puts -nonewline $o $data + + } elseif {[string length $data] == 0} { + # Truncating instead of replacement + + fcopy $c $o -size $at + # Nothing further to copy. + + } else { + # General replacement at end + + fcopy $c $o -size $at + puts -nonewline $o $data + } + } else { + if {$n == 0} { + # General insertion. + + fcopy $c $o -size $at + puts -nonewline $o $data + fcopy $c $o + + } elseif {[string length $data] == 0} { + # General removal. + + fcopy $c $o -size $at + seek $c $n current + fcopy $c $o + + } else { + # General replacement. + + fcopy $c $o -size $at + seek $c $n current + puts -nonewline $o $data + fcopy $c $o + } + } + + Close2 $fname $t $c $o + return +} + +# ::fileutil::updateInPlace -- +# +# Run command prefix on the contents of the +# file and replace them with the result of +# the command. +# +# Arguments: +# options... Options and arguments. +# filename Path to the file to extend. +# cmd Command prefix to run. +# +# Results: +# None. + +proc ::fileutil::updateInPlace {args} { + # Syntax: ?options? file cmd + # options = -encoding ENC + # | -translation TRA + # | -eofchar ECH + # | -- + + Spec ReadWritable $args opts fname cmd + + # readFile/cat inlined ... + + set c [open $fname r] + SetOptions $c $opts + set data [read $c] + close $c + + # Transformation. Abort and do not modify the target file if an + # error was raised during this step. + + lappend cmd $data + set code [catch {uplevel 1 $cmd} res] + if {$code} { + return -code $code $res + } + + # writeFile inlined, with careful preservation of old contents + # until we are sure that the write was ok. + + if {[catch { + file rename -force $fname ${fname}.bak + + set o [open $fname w] + SetOptions $o $opts + puts -nonewline $o $res + close $o + + file delete -force ${fname}.bak + } msg]} { + if {[file exists ${fname}.bak]} { + catch { + file rename -force ${fname}.bak $fname + } + return -code error $msg + } + } + return +} + +proc ::fileutil::Writable {fname mv} { + upvar 1 $mv msg + if {[file exists $fname]} { + if {![file isfile $fname]} { + set msg "Cannot use file \"$fname\", is not a file" + return 0 + } elseif {![file writable $fname]} { + set msg "Cannot use file \"$fname\", write access is denied" + return 0 + } + } + return 1 +} + +proc ::fileutil::ReadWritable {fname mv} { + upvar 1 $mv msg + if {![file exists $fname]} { + set msg "Cannot use file \"$fname\", does not exist" + return 0 + } elseif {![file isfile $fname]} { + set msg "Cannot use file \"$fname\", is not a file" + return 0 + } elseif {![file writable $fname]} { + set msg "Cannot use file \"$fname\", write access is denied" + return 0 + } elseif {![file readable $fname]} { + set msg "Cannot use file \"$fname\", read access is denied" + return 0 + } + return 1 +} + +proc ::fileutil::Spec {check alist ov fv args} { + upvar 1 $ov opts $fv fname + + set n [llength $args] ; # Num more args + incr n ; # Count path as well + + set opts {} + set mode maybeopt + + set at 0 + foreach a $alist { + if {[string equal $mode optarg]} { + lappend opts $a + set mode maybeopt + incr at + continue + } elseif {[string equal $mode maybeopt]} { + if {[string match -* $a]} { + switch -exact -- $a { + -encoding - + -translation - + -eofchar { + lappend opts $a + set mode optarg + incr at + continue + } + -- { + # Stop processing. + incr at + break + } + default { + return -code error \ + "Bad option \"$a\",\ + expected one of\ + -encoding, -eofchar,\ + or -translation" + } + } + } + # Not an option, but a file. + # Stop processing. + break + } + } + + if {([llength $alist] - $at) != $n} { + # Argument processing stopped with arguments missing, or too + # many + return -code error \ + "wrong#args: should be\ + [lindex [info level 1] 0] ?-eofchar|-translation|-encoding arg? file $args" + } + + set fname [lindex $alist $at] + incr at + foreach \ + var $args \ + val [lrange $alist $at end] { + upvar 1 $var A + set A $val + } + + # Check given path ... + + if {![eval [linsert $check end $a msg]]} { + return -code error $msg + } + + return +} + +proc ::fileutil::Open2 {fname opts} { + set c [open $fname r] + set t [tempfile] + set o [open $t w] + + SetOptions $c $opts + SetOptions $o $opts + + return [list $c $o $t] +} + +proc ::fileutil::Close2 {f temp in out} { + close $in + close $out + + file copy -force $f ${f}.bak + file rename -force $temp $f + file delete -force ${f}.bak + return +} + +proc ::fileutil::SetOptions {c opts} { + if {![llength $opts]} return + eval [linsert $opts 0 fconfigure $c] + return +} + +proc ::fileutil::CheckLocation {at max label} { + if {![string is integer -strict $at]} { + return -code error \ + "Expected integer but got \"$at\"" + } elseif {$at < 0} { + return -code error \ + "Bad $label point $at, before start of data" + } elseif {$at > $max} { + return -code error \ + "Bad $label point $at, behind end of data" + } +} + +proc ::fileutil::CheckLength {n at max label} { + if {![string is integer -strict $n]} { + return -code error \ + "Expected integer but got \"$n\"" + } elseif {$n < 0} { + return -code error \ + "Bad $label size $n" + } elseif {($at + $n) > $max} { + return -code error \ + "Bad $label size $n, going behind end of data" + } +} + +# ::fileutil::foreachLine -- +# +# Executes a script for every line in a file. +# +# Arguments: +# var name of the variable to contain the lines +# filename name of the file to read. +# cmd The script to execute. +# +# Results: +# None. + +proc ::fileutil::foreachLine {var filename cmd} { + upvar 1 $var line + set fp [open $filename r] + + # -future- Use try/eval from tcllib/control + catch { + set code 0 + set result {} + while {[gets $fp line] >= 0} { + set code [catch {uplevel 1 $cmd} result] + if {($code != 0) && ($code != 4)} {break} + } + } + close $fp + + if {($code == 0) || ($code == 3) || ($code == 4)} { + return $result + } + if {$code == 1} { + global errorCode errorInfo + return \ + -code $code \ + -errorcode $errorCode \ + -errorinfo $errorInfo \ + $result + } + return -code $code $result +} + +# ::fileutil::touch -- +# +# Tcl implementation of the UNIX "touch" command. +# +# touch [-a] [-m] [-c] [-r ref_file] [-t time] filename ... +# +# Arguments: +# -a change the access time only, unless -m also specified +# -m change the modification time only, unless -a also specified +# -c silently prevent creating a file if it did not previously exist +# -r ref_file use the ref_file's time instead of the current time +# -t time use the specified time instead of the current time +# ("time" is an integer clock value, like [clock seconds]) +# filename ... the files to modify +# +# Results +# None. +# +# Errors: +# Both of "-r" and "-t" cannot be specified. + +if {[package vsatisfies [package provide Tcl] 8.3]} { + namespace eval ::fileutil { + namespace export touch + } + + proc ::fileutil::touch {args} { + # Don't bother catching errors, just let them propagate up + + set options { + {a "set the atime only"} + {m "set the mtime only"} + {c "do not create non-existant files"} + {r.arg "" "use time from ref_file"} + {t.arg -1 "use specified time"} + } + set usage ": [lindex [info level 0] 0]\ + \[options] filename ...\noptions:" + array set params [::cmdline::getoptions args $options $usage] + + # process -a and -m options + set set_atime [set set_mtime "true"] + if { $params(a) && ! $params(m)} {set set_mtime "false"} + if {! $params(a) && $params(m)} {set set_atime "false"} + + # process -r and -t + set has_t [expr {$params(t) != -1}] + set has_r [expr {[string length $params(r)] > 0}] + if {$has_t && $has_r} { + return -code error "Cannot specify both -r and -t" + } elseif {$has_t} { + set atime [set mtime $params(t)] + } elseif {$has_r} { + file stat $params(r) stat + set atime $stat(atime) + set mtime $stat(mtime) + } else { + set atime [set mtime [clock seconds]] + } + + # do it + foreach filename $args { + if {! [file exists $filename]} { + if {$params(c)} {continue} + close [open $filename w] + } + if {$set_atime} {file atime $filename $atime} + if {$set_mtime} {file mtime $filename $mtime} + } + return + } +} + +# ::fileutil::fileType -- +# +# Do some simple heuristics to determine file type. +# +# +# Arguments: +# filename Name of the file to test. +# +# Results +# type Type of the file. May be a list if multiple tests +# are positive (eg, a file could be both a directory +# and a link). In general, the list proceeds from most +# general (eg, binary) to most specific (eg, gif), so +# the full type for a GIF file would be +# "binary graphic gif" +# +# At present, the following types can be detected: +# +# directory +# empty +# binary +# text +# script <interpreter> +# executable [elf, dos, ne, pe] +# binary graphic [gif, jpeg, png, tiff, bitmap, icns] +# ps, eps, pdf +# html +# xml <doctype> +# message pgp +# compressed [bzip, gzip, zip, tar] +# audio [mpeg, wave] +# gravity_wave_data_frame +# link +# doctools, doctoc, and docidx documentation files. +# + +proc ::fileutil::fileType {filename} { + ;## existence test + if { ! [ file exists $filename ] } { + set err "file not found: '$filename'" + return -code error $err + } + ;## directory test + if { [ file isdirectory $filename ] } { + set type directory + if { ! [ catch {file readlink $filename} ] } { + lappend type link + } + return $type + } + ;## empty file test + if { ! [ file size $filename ] } { + set type empty + if { ! [ catch {file readlink $filename} ] } { + lappend type link + } + return $type + } + set bin_rx {[\x00-\x08\x0b\x0e-\x1f]} + + if { [ catch { + set fid [ open $filename r ] + fconfigure $fid -translation binary + fconfigure $fid -buffersize 1024 + fconfigure $fid -buffering full + set test [ read $fid 1024 ] + ::close $fid + } err ] } { + catch { ::close $fid } + return -code error "::fileutil::fileType: $err" + } + + if { [ regexp $bin_rx $test ] } { + set type binary + set binary 1 + } else { + set type text + set binary 0 + } + + # SF Tcllib bug [795585]. Allowing whitespace between #! + # and path of script interpreter + + set metakit 0 + + if { [ regexp {^\#\!\s*(\S+)} $test -> terp ] } { + lappend type script $terp + } elseif {([regexp "\\\[manpage_begin " $test] && + !([regexp -- {--- !doctools ---} $test] || [regexp -- "!tcl\.tk//DSL doctools//EN//" $test])) || + ([regexp -- {--- doctools ---} $test] || [regexp -- "tcl\.tk//DSL doctools//EN//" $test])} { + lappend type doctools + } elseif {([regexp "\\\[toc_begin " $test] && + !([regexp -- {--- !doctoc ---} $test] || [regexp -- "!tcl\.tk//DSL doctoc//EN//" $test])) || + ([regexp -- {--- doctoc ---} $test] || [regexp -- "tcl\.tk//DSL doctoc//EN//" $test])} { + lappend type doctoc + } elseif {([regexp "\\\[index_begin " $test] && + !([regexp -- {--- !docidx ---} $test] || [regexp -- "!tcl\.tk//DSL docidx//EN//" $test])) || + ([regexp -- {--- docidx ---} $test] || [regexp -- "tcl\.tk//DSL docidx//EN//" $test])} { + lappend type docidx + } elseif {[regexp -- "tcl\\.tk//DSL diagram//EN//" $test]} { + lappend type tkdiagram + } elseif { $binary && [ regexp {^[\x7F]ELF} $test ] } { + lappend type executable elf + } elseif { $binary && [string match "MZ*" $test] } { + if { [scan [string index $test 24] %c] < 64 } { + lappend type executable dos + } else { + binary scan [string range $test 60 61] s next + set sig [string range $test $next [expr {$next + 1}]] + if { $sig == "NE" || $sig == "PE" } { + lappend type executable [string tolower $sig] + } else { + lappend type executable dos + } + } + } elseif { $binary && [string match "BZh91AY\&SY*" $test] } { + lappend type compressed bzip + } elseif { $binary && [string match "\x1f\x8b*" $test] } { + lappend type compressed gzip + } elseif { $binary && [string range $test 257 262] == "ustar\x00" } { + lappend type compressed tar + } elseif { $binary && [string match "\x50\x4b\x03\x04*" $test] } { + lappend type compressed zip + } elseif { $binary && [string match "GIF*" $test] } { + lappend type graphic gif + } elseif { $binary && [string match "icns*" $test] } { + lappend type graphic icns bigendian + } elseif { $binary && [string match "snci*" $test] } { + lappend type graphic icns smallendian + } elseif { $binary && [string match "\x89PNG*" $test] } { + lappend type graphic png + } elseif { $binary && [string match "\xFF\xD8\xFF*" $test] } { + binary scan $test x3H2x2a5 marker txt + if { $marker == "e0" && $txt == "JFIF\x00" } { + lappend type graphic jpeg jfif + } elseif { $marker == "e1" && $txt == "Exif\x00" } { + lappend type graphic jpeg exif + } + } elseif { $binary && [string match "MM\x00\**" $test] } { + lappend type graphic tiff + } elseif { $binary && [string match "BM*" $test] && [string range $test 6 9] == "\x00\x00\x00\x00" } { + lappend type graphic bitmap + } elseif { ! $binary && [string match -nocase "*\<html\>*" $test] } { + lappend type html + } elseif {[string match "\%PDF\-*" $test] } { + lappend type pdf + } elseif { [string match "\%\!PS\-*" $test] } { + lappend type ps + if { [string match "* EPSF\-*" $test] } { + lappend type eps + } + } elseif { [string match -nocase "*\<\?xml*" $test] } { + lappend type xml + if { [ regexp -nocase {\<\!DOCTYPE\s+(\S+)} $test -> doctype ] } { + lappend type $doctype + } + } elseif { [string match {*BEGIN PGP MESSAGE*} $test] } { + lappend type message pgp + } elseif { $binary && [string match {IGWD*} $test] } { + lappend type gravity_wave_data_frame + } elseif {[string match "JL\x1a\x00*" $test] && ([file size $filename] >= 27)} { + lappend type metakit smallendian + set metakit 1 + } elseif {[string match "LJ\x1a\x00*" $test] && ([file size $filename] >= 27)} { + lappend type metakit bigendian + set metakit 1 + } elseif { $binary && [string match "RIFF*" $test] && [string range $test 8 11] == "WAVE" } { + lappend type audio wave + } elseif { $binary && [string match "ID3*" $test] } { + lappend type audio mpeg + } elseif { $binary && [binary scan $test S tmp] && [expr {$tmp & 0xFFE0}] == 65504 } { + lappend type audio mpeg + } + + # Additional checks of file contents at the end of the file, + # possibly pointing into the middle too (attached metakit, + # attached zip). + + ## Metakit File format: http://www.equi4.com/metakit/metakit-ff.html + ## Metakit database attached ? ## + + if {!$metakit && ([file size $filename] >= 27)} { + # The offsets in the footer are in always bigendian format + + if { [ catch { + set fid [ open $filename r ] + fconfigure $fid -translation binary + fconfigure $fid -buffersize 1024 + fconfigure $fid -buffering full + seek $fid -16 end + set test [ read $fid 16 ] + ::close $fid + } err ] } { + catch { ::close $fid } + return -code error "::fileutil::fileType: $err" + } + + binary scan $test IIII __ hdroffset __ __ + set hdroffset [expr {[file size $filename] - 16 - $hdroffset}] + + # Further checks iff the offset is actually inside the file. + + if {($hdroffset >= 0) && ($hdroffset < [file size $filename])} { + # Seek to the specified location and try to match a metakit header + # at this location. + + if { [ catch { + set fid [ open $filename r ] + fconfigure $fid -translation binary + fconfigure $fid -buffersize 1024 + fconfigure $fid -buffering full + seek $fid $hdroffset start + set test [ read $fid 16 ] + ::close $fid + } err ] } { + catch { ::close $fid } + return -code error "::fileutil::fileType: $err" + } + + if {[string match "JL\x1a\x00*" $test]} { + lappend type attached metakit smallendian + set metakit 1 + } elseif {[string match "LJ\x1a\x00*" $test]} { + lappend type attached metakit bigendian + set metakit 1 + } + } + } + + ## Zip File Format: http://zziplib.sourceforge.net/zzip-parse.html + ## http://www.pkware.com/products/enterprise/white_papers/appnote.html + + + ;## lastly, is it a link? + if { ! [ catch {file readlink $filename} ] } { + lappend type link + } + return $type +} + +# ::fileutil::tempdir -- +# +# Return the correct directory to use for temporary files. +# Python attempts this sequence, which seems logical: +# +# 1. The directory named by the `TMPDIR' environment variable. +# +# 2. The directory named by the `TEMP' environment variable. +# +# 3. The directory named by the `TMP' environment variable. +# +# 4. A platform-specific location: +# * On Macintosh, the `Temporary Items' folder. +# +# * On Windows, the directories `C:\\TEMP', `C:\\TMP', +# `\\TEMP', and `\\TMP', in that order. +# +# * On all other platforms, the directories `/tmp', +# `/var/tmp', and `/usr/tmp', in that order. +# +# 5. As a last resort, the current working directory. +# +# The code here also does +# +# 0. The directory set by invoking tempdir with an argument. +# If this is present it is used exclusively. +# +# Arguments: +# None. +# +# Side Effects: +# None. +# +# Results: +# The directory for temporary files. + +proc ::fileutil::tempdir {args} { + if {[llength $args] > 1} { + return -code error {wrong#args: should be "::fileutil::tempdir ?path?"} + } elseif {[llength $args] == 1} { + variable tempdir [lindex $args 0] + variable tempdirSet 1 + return + } + return [Normalize [TempDir]] +} + +proc ::fileutil::tempdirReset {} { + variable tempdir {} + variable tempdirSet 0 + return +} + +proc ::fileutil::TempDir {} { + global tcl_platform env + variable tempdir + variable tempdirSet + + set attempdirs [list] + set problems {} + + if {$tempdirSet} { + lappend attempdirs $tempdir + lappend problems {User/Application specified tempdir} + } else { + foreach tmp {TMPDIR TEMP TMP} { + if { [info exists env($tmp)] } { + lappend attempdirs $env($tmp) + } else { + lappend problems "No environment variable $tmp" + } + } + + switch $tcl_platform(platform) { + windows { + lappend attempdirs "C:\\TEMP" "C:\\TMP" "\\TEMP" "\\TMP" + } + macintosh { + lappend attempdirs $env(TRASH_FOLDER) ;# a better place? + } + default { + lappend attempdirs \ + [file join / tmp] \ + [file join / var tmp] \ + [file join / usr tmp] + } + } + + lappend attempdirs [pwd] + } + + foreach tmp $attempdirs { + if { [file isdirectory $tmp] && [file writable $tmp] } { + return $tmp + } elseif { ![file isdirectory $tmp] } { + lappend problems "Not a directory: $tmp" + } else { + lappend problems "Not writable: $tmp" + } + } + + # Fail if nothing worked. + return -code error "Unable to determine a proper directory for temporary files\n[join $problems \n]" +} + +namespace eval ::fileutil { + variable tempdir {} + variable tempdirSet 0 +} + +# ::fileutil::maketempdir -- + +proc ::fileutil::maketempdir {args} { + return [Normalize [MakeTempDir $args]] +} + +proc ::fileutil::MakeTempDir {config} { + # Setup of default configuration. + array set options {} + set options(-suffix) "" + set options(-prefix) "tmp" + set options(-dir) [tempdir] + + # TODO: Check for and reject options not in -suffix, -prefix, -dir + # Merge user configuration, overwrite defaults. + array set options $config + + # See also "tempfile" below. Could be shareable internal configuration. + set chars abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 + set nrand_chars 10 + set maxtries 10 + + for {set i 0} {$i < $maxtries} {incr i} { + # Build up the candidate name. See also "tempfile". + set directory_name $options(-prefix) + for {set j 0} {$j < $nrand_chars} {incr j} { + append directory_name \ + [string index $chars [expr {int(rand() * 62)}]] + } + append directory_name $options(-suffix) + set path [file join $options(-dir) $directory_name] + + # Try to create. Try again if already exists, or trouble + # with creation and setting of perms. + # + # Note: The last looks as if it is able to leave partial + # directories behind (created, trouble with perms). But + # deleting ... Might pull the rug out from somebody else. + + if {[file exists $path]} continue + if {[catch { + file mkdir $path + file attributes $path -permissions 0700 + }]} continue + + return $path + } + return -code error "Failed to find an unused temporary directory name" +} + +# ::fileutil::tempfile -- +# +# generate a temporary file name suitable for writing to +# the file name will be unique, writable and will be in the +# appropriate system specific temp directory +# Code taken from http://mini.net/tcl/772 attributed to +# Igor Volobouev and anon. +# +# Arguments: +# prefix - a prefix for the filename, p +# Results: +# returns a file name +# + +proc ::fileutil::tempfile {{prefix {}}} { + return [Normalize [TempFile $prefix]] +} + +proc ::fileutil::TempFile {prefix} { + set tmpdir [tempdir] + + set chars "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + set nrand_chars 10 + set maxtries 10 + set access [list RDWR CREAT EXCL] + set permission 0600 + set channel "" + set checked_dir_writable 0 + + for {set i 0} {$i < $maxtries} {incr i} { + set newname $prefix + for {set j 0} {$j < $nrand_chars} {incr j} { + append newname [string index $chars \ + [expr {int(rand()*62)}]] + } + set newname [file join $tmpdir $newname] + + if {[catch {open $newname $access $permission} channel]} { + if {!$checked_dir_writable} { + set dirname [file dirname $newname] + if {![file writable $dirname]} { + return -code error "Directory $dirname is not writable" + } + set checked_dir_writable 1 + } + } else { + # Success + close $channel + return $newname + } + + } + if {[string compare $channel ""]} { + return -code error "Failed to open a temporary file: $channel" + } else { + return -code error "Failed to find an unused temporary file name" + } +} + +# ::fileutil::install -- +# +# Tcl version of the 'install' command, which copies files from +# one places to another and also optionally sets some attributes +# such as group, owner, and permissions. +# +# Arguments: +# -m Change the file permissions to the specified +# value. Valid arguments are those accepted by +# file attributes -permissions +# +# Results: +# None. + +# TODO - add options for group/owner manipulation. + +proc ::fileutil::install {args} { + set options { + {m.arg "" "Set permission mode"} + } + set usage ": [lindex [info level 0] 0]\ +\[options] source destination \noptions:" + array set params [::cmdline::getoptions args $options $usage] + # Args should now just be the source and destination. + if { [llength $args] < 2 } { + return -code error $usage + } + set src [lindex $args 0] + set dst [lindex $args 1] + file copy -force $src $dst + if { $params(m) != "" } { + set targets [::fileutil::find $dst] + foreach fl $targets { + file attributes $fl -permissions $params(m) + } + } +} + +# ### ### ### ######### ######### ######### + +proc ::fileutil::lexnormalize {sp} { + set spx [file split $sp] + + # Resolution of embedded relative modifiers (., and ..). + + if { + ([lsearch -exact $spx . ] < 0) && + ([lsearch -exact $spx ..] < 0) + } { + # Quick path out if there are no relative modifiers + return $sp + } + + set absolute [expr {![string equal [file pathtype $sp] relative]}] + # A volumerelative path counts as absolute for our purposes. + + set sp $spx + set np {} + set noskip 1 + + while {[llength $sp]} { + set ele [lindex $sp 0] + set sp [lrange $sp 1 end] + set islast [expr {[llength $sp] == 0}] + + if {[string equal $ele ".."]} { + if { + ($absolute && ([llength $np] > 1)) || + (!$absolute && ([llength $np] >= 1)) + } { + # .. : Remove the previous element added to the + # new path, if there actually is enough to remove. + set np [lrange $np 0 end-1] + } + } elseif {[string equal $ele "."]} { + # Ignore .'s, they stay at the current location + continue + } else { + # A regular element. + lappend np $ele + } + } + if {[llength $np] > 0} { + return [eval [linsert $np 0 file join]] + # 8.5: return [file join {*}$np] + } + return {} +} + +# ### ### ### ######### ######### ######### +## Forward compatibility. Some routines require path normalization, +## something we have supported by the builtin 'file' only since Tcl +## 8.4. For versions of Tcl before that, to be supported by the +## module, we implement a normalizer in Tcl itself. Slow, but working. + +if {[package vcompare [package provide Tcl] 8.4] < 0} { + # Pre 8.4. We do not have 'file normalize'. We create an + # approximation for it based on earlier commands. + + # ... Hm. This is lexical normalization. It does not resolve + # symlinks in the path to their origin. + + proc ::fileutil::Normalize {sp} { + set sp [file split $sp] + + # Conversion of the incoming path to absolute. + if {[string equal [file pathtype [lindex $sp 0]] "relative"]} { + set sp [file split [eval [list file join [pwd]] $sp]] + } + + # Resolution of symlink components, and embedded relative + # modifiers (., and ..). + + set np {} + set noskip 1 + while {[llength $sp]} { + set ele [lindex $sp 0] + set sp [lrange $sp 1 end] + set islast [expr {[llength $sp] == 0}] + + if {[string equal $ele ".."]} { + if {[llength $np] > 1} { + # .. : Remove the previous element added to the + # new path, if there actually is enough to remove. + set np [lrange $np 0 end-1] + } + } elseif {[string equal $ele "."]} { + # Ignore .'s, they stay at the current location + continue + } else { + # A regular element. If it is not the last component + # then check if the combination is a symlink, and if + # yes, resolve it. + + lappend np $ele + + if {!$islast && $noskip} { + # The flag 'noskip' is technically not required, + # just 'file exists'. However if a path P does not + # exist, then all longer paths starting with P can + # not exist either, and using the flag to store + # this knowledge then saves us a number of + # unnecessary stat calls. IOW this a performance + # optimization. + + set p [eval file join $np] + set noskip [file exists $p] + if {$noskip} { + if {[string equal link [file type $p]]} { + set dst [file readlink $p] + + # We always push the destination in front of + # the source path (in expanded form). So that + # we handle .., .'s, and symlinks inside of + # this path as well. An absolute path clears + # the result, a relative one just removes the + # last, now resolved component. + + set sp [eval [linsert [file split $dst] 0 linsert $sp 0]] + + if {![string equal relative [file pathtype $dst]]} { + # Absolute|volrelative destination, clear + # result, we have to start over. + set np {} + } else { + # Relative link, just remove the resolved + # component again. + set np [lrange $np 0 end-1] + } + } + } + } + } + } + if {[llength $np] > 0} { + return [eval file join $np] + } + return {} + } +} else { + proc ::fileutil::Normalize {sp} { + file normalize $sp + } +} + +# ::fileutil::relative -- +# +# Taking two _directory_ paths, a base and a destination, computes the path +# of the destination relative to the base. +# +# Arguments: +# base The path to make the destination relative to. +# dst The destination path +# +# Results: +# The path of the destination, relative to the base. + +proc ::fileutil::relative {base dst} { + # Ensure that the link to directory 'dst' is properly done relative to + # the directory 'base'. + + if {![string equal [file pathtype $base] [file pathtype $dst]]} { + return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)" + } + + set base [lexnormalize [file join [pwd] $base]] + set dst [lexnormalize [file join [pwd] $dst]] + + set save $dst + set base [file split $base] + set dst [file split $dst] + + while {[string equal [lindex $dst 0] [lindex $base 0]]} { + set dst [lrange $dst 1 end] + set base [lrange $base 1 end] + if {![llength $dst]} {break} + } + + set dstlen [llength $dst] + set baselen [llength $base] + + if {($dstlen == 0) && ($baselen == 0)} { + # Cases: + # (a) base == dst + + set dst . + } else { + # Cases: + # (b) base is: base/sub = sub + # dst is: base = {} + + # (c) base is: base = {} + # dst is: base/sub = sub + + while {$baselen > 0} { + set dst [linsert $dst 0 ..] + incr baselen -1 + } + # 8.5: set dst [file join {*}$dst] + set dst [eval [linsert $dst 0 file join]] + } + + return $dst +} + +# ::fileutil::relativeUrl -- +# +# Taking two _file_ paths, a base and a destination, computes the path +# of the destination relative to the base, from the inside of the base. +# +# This is how a browser resolves relative links in a file, hence the +# url in the command name. +# +# Arguments: +# base The file path to make the destination relative to. +# dst The destination file path +# +# Results: +# The path of the destination file, relative to the base file. + +proc ::fileutil::relativeUrl {base dst} { + # Like 'relative', but for links from _inside_ a file to a + # different file. + + if {![string equal [file pathtype $base] [file pathtype $dst]]} { + return -code error "Unable to compute relation for paths of different pathtypes: [file pathtype $base] vs. [file pathtype $dst], ($base vs. $dst)" + } + + set base [lexnormalize [file join [pwd] $base]] + set dst [lexnormalize [file join [pwd] $dst]] + + set basedir [file dirname $base] + set dstdir [file dirname $dst] + + set dstdir [relative $basedir $dstdir] + + # dstdir == '.' on input => dstdir output has trailing './'. Strip + # this superfluous segment off. + + if {[string equal $dstdir "."]} { + return [file tail $dst] + } elseif {[string equal [file tail $dstdir] "."]} { + return [file join [file dirname $dstdir] [file tail $dst]] + } else { + return [file join $dstdir [file tail $dst]] + } +} + +# ::fileutil::fullnormalize -- +# +# Normalizes a path completely. I.e. a symlink in the last +# element is resolved as well, not only symlinks in the higher +# elements. +# +# Arguments: +# path The path to normalize +# +# Results: +# The input path with all symlinks resolved. + +proc ::fileutil::fullnormalize {path} { + # When encountering symlinks in a file copy operation Tcl copies + # the link, not the contents of the file it references. There are + # situations there this is not acceptable. For these this command + # resolves all symbolic links in the path, including in the last + # element of the path. A "file copy" using the return value of + # this command copies an actual file, it will not encounter + # symlinks. + + # BUG / WORKAROUND. Using the / instead of the join seems to work + # around a bug in the path handling on windows which can break the + # core 'file normalize' for symbolic links. This was exposed by + # the find testsuite which could not reproduced outside. I believe + # that there is some deep path bug in the core triggered under + # special circumstances. Use of / likely forces a refresh through + # the string rep and so avoids the problem with the path intrep. + + return [file dirname [Normalize $path/__dummy__]] + #return [file dirname [Normalize [file join $path __dummy__]]] +} diff --git a/tcllib/modules/fileutil/fileutil.test b/tcllib/modules/fileutil/fileutil.test new file mode 100644 index 0000000..48f1e01 --- /dev/null +++ b/tcllib/modules/fileutil/fileutil.test @@ -0,0 +1,499 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2013 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: fileutil.test,v 1.56 2009/10/06 20:07:18 andreas_kupries Exp $ + +# TODO: Bug [8b317b4a63]: Create test cases for this bug. This +# requires the use of a custom VFS as the native filesystem does not +# contain the bug we are guarding ourselves against. + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +support { + useTcllibFile fumagic/fumagic.testsupport + use cmdline/cmdline.tcl cmdline +} +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +proc gt_setup {} { + global tcl_platform gt gtfa gtfb + + set gt [makeDirectory grepTest] + set gtfa [makeFile "zoop" [file join $gt {file [1]}]] + set gtfb {} + + if {[string equal $::tcl_platform(platform) windows]} return + + set gtfb [makeFile "zoo\nbart" [file join $gt {file* 2}]] + return +} + +proc gt_cleanup {} { + removeDirectory grepTest + + + rename gt_setup {} + rename gt_cleanup {} + unset ::gt ::gtfa ::gtfb + return +} + +# ------------------------------------------------------------------------- + +gt_setup + +test grep-1.1 {normal grep} {macOrUnix} { + lsort [fileutil::grep "zoo" [glob [file join $gt *]]] +} [list "$gtfa:1:zoop" "$gtfb:1:zoo"] + +test grep-1.2 {more restrictive grep} { + lsort [fileutil::grep "zoo." [glob [file join $gt *]]] +} [list "$gtfa:1:zoop"] + +test grep-1.3 {more restrictive grep} {macOrUnix} { + lsort [fileutil::grep "bar" [glob [file join $gt *]]] +} [list "$gtfb:2:bart"] + +gt_cleanup + +# ------------------------------------------------------------------------- + +test foreachline-1.0 {foreachLine} { + set path [makeFile "foo\nbar\nbaz\n" {cat [1]}] + + set res "" + ::fileutil::foreachLine line $path { + append res /$line + } + + removeFile {cat [1]} + set res +} {/foo/bar/baz} + +# ------------------------------------------------------------------------- + +proc t_setup {} { + global tt + + set tt [makeDirectory touchTest] + makeFile "blah" [file join touchTest {file [1]}] +} + +proc t_cleanup {} { + removeDirectory touchTest + rename t_setup {} + rename t_cleanup {} + unset ::tt + catch { unset ::a1 } + catch { unset ::m1} + catch { unset ::a2} + catch { unset ::m2} + catch { unset ::f} + catch { unset ::r} + return +} + +# ------------------------------------------------------------------------- + +t_setup + +test touch-1.1 {create file} tcl8.3plus { + set f [file join $tt here] + fileutil::touch $f + file exists $f +} 1 + +test touch-1.2 {'-c' prevents file creation} tcl8.3plus { + set f [file join $tt nothere] + fileutil::touch -c $f + file exists $f +} 0 + +test touch-1.3 {'-c' has no effect on existing files} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch -c $f + file exists $f +} 1 + +test touch-1.4 {test relative times} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + set a1 [file atime $f] + set m1 [file mtime $f] + after 1001 + fileutil::touch $f + set a2 [file atime $f] + set m2 [file mtime $f] + list [expr {$a1 == $m1}] [expr {$a2 == $m2}] [expr {$a1 < $a2}] [expr {$m1 < $m2}] +} {1 1 1 1} + +test touch-1.5 {test relative times using -a} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + set a1 [file atime $f] + set m1 [file mtime $f] + after 1001 + fileutil::touch -a $f + set a2 [file atime $f] + set m2 [file mtime $f] + list [expr {$a1 == $m1}] [expr {$a2 == $m2}] [expr {$a1 < $a2}] [expr {$m1 < $m2}] +} {1 0 1 0} + +test touch-1.6 {test relative times using -m} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + set a1 [file atime $f] + set m1 [file mtime $f] + after 1001 + fileutil::touch -m $f + set a2 [file atime $f] + set m2 [file mtime $f] + list [expr {$a1 == $m1}] [expr {$a2 == $m2}] [expr {$a1 < $a2}] [expr {$m1 < $m2}] +} {1 0 0 1} + +test touch-1.7 {test relative times using -a and -m} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + set a1 [file atime $f] + set m1 [file mtime $f] + after 1001 + fileutil::touch -a -m $f + set a2 [file atime $f] + set m2 [file mtime $f] + list [expr {$a1 == $m1}] [expr {$a2 == $m2}] [expr {$a1 < $a2}] [expr {$m1 < $m2}] +} {1 1 1 1} + +test touch-1.8 {test -t} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -t 42 $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == 42}] [expr {$m1 == 42}] +} {1 1} + +test touch-1.9 {test -t with -a} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -t 42 -a $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == 42}] [expr {$m1 == 42}] +} [list 1 0] + +test touch-1.10 {test -t with -m} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -t 42 -m $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == 42}] [expr {$m1 == 42}] +} [list 0 1] + +test touch-1.11 {test -t with -a and -m} tcl8.3plus { + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -t 42 -a -m $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == 42}] [expr {$m1 == 42}] +} {1 1} + +test touch-1.12 {test -r} tcl8.3plus { + set r [info script] + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -r $r $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == [file atime $r]}] [expr {$m1 == [file mtime $r]}] +} {1 1} + +test touch-1.13 {test -r with -a} tcl8.3plus { + set r [info script] + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -r $r -a $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == [file atime $r]}] [expr {$m1 == [file mtime $r]}] +} {1 0} + +test touch-1.14 {test -r with -m} tcl8.3plus { + set r [info script] + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -r $r -m $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == [file atime $r]}] [expr {$m1 == [file mtime $r]}] +} {0 1} + +test touch-1.15 {test -r with -a and -m} tcl8.3plus { + set r [info script] + set f [file join $tt {file [1]}] + fileutil::touch $f + after 1001 + fileutil::touch -r $r -m -a $f + set a1 [file atime $f] + set m1 [file mtime $f] + list [expr {$a1 == [file atime $r]}] [expr {$m1 == [file mtime $r]}] +} {1 1} + +t_cleanup + +# ---------------------------------------------------------------- + +proc i_setup {} { + global tcl_platform + makeDirectory installDst + makeDirectory installSrc + + makeDirectory [file join installSrc subdir] + makeFile "blah" [file join installSrc {file [1]}] + + # Make a second subdirectory to install, unix-only + if {$tcl_platform(platform) != "unix" } return + + makeDirectory [file join installSrc subdir2] + makeFile "blah" [file join installSrc subdir subfile1] + makeFile "blah" [file join installSrc subdir subfile2] + makeFile "blah" [file join installSrc subdir subfile3] + + foreach fl {1 2 3} { + set fn [file join installSrc subdir2 subfile$fl] + makeFile "blah" $fn + + # Give it some "bad" permissions. + file attributes $fn -permissions 0600 + } + return +} + +proc i_cleanup {} { + removeDirectory installDst + removeDirectory installSrc + + rename i_setup {} + rename i_cleanup {} + return +} + +# ---------------------------------------------------------------- + +i_setup + +test install-1.1 {install a file} { + fileutil::install [file join installSrc {file [1]}] installDst + file exists [file join installDst {file [1]}] +} {1} + +makeDirectory installDst + +test install-2.1 {install a directory} {tcl8.4plus} { + list [catch { + fileutil::install [file join installSrc subdir] installDst + set result [lsort [glob -tails -directory [file join installDst subdir] [file join . / *]]] + file delete -force installDst + set result + } err] $err +} {0 {subfile1 subfile2 subfile3}} + +makeDirectory installDst + +test install-2.2 {install a directory} {tcl8.3plus} { + list [catch { + fileutil::install [file join installSrc subdir] installDst + set result [lsort [glob -directory [file join installDst subdir] [file join . / *]]] + file delete -force installDst + set result + } err] $err +} {0 {installDst/subdir/subfile1 installDst/subdir/subfile2 installDst/subdir/subfile3}} + +makeDirectory installDst + +test install-3.1 {install a directory, set permissions} {unix tcl8.3plus} { + set res {} + fileutil::install -m go+rw [file join installSrc subdir2] installDst + foreach fl [glob [file join installDst subdir2 *]] { + append res [file attributes $fl -permissions] + } + set res +} {006660066600666} + +i_cleanup + +# ------------------------------------------------------------------------- + +proc tmp_setup {} { + global xpath res + + # Set up an exclusive directory to search. This cannot be unset, + # hence the location of these tests after the regular + # tempdir/tempfile tests. + + removeDirectory x + set xpath [makeDirectory x] + set res {} + removeDirectory x + return +} + +proc tmp_cleanup {} { + rename tmp_setup {} + rename tmp_cleanup {} + removeDirectory x + unset ::xpath + unset ::res + return +} + +# ------------------------------------------------------------------------- + +tmp_setup + +test tempdir-1.1 {return the correct directorary for temporary files} {unix} { + set ::env(TMPDIR) [pwd] ;# Most high-priority source, and existing directory! + set res [::fileutil::tempdir] + unset ::env(TMPDIR) + set res +} [pwd] + +test tempdir-1.2 {return the correct directorary for temporary files} {unix} { + catch {unset ::env(TMPDIR)} + catch {unset ::env(TEMPDIR)} + catch {unset ::env(TMP)} + catch {unset ::env(TEMP)} + ::fileutil::tempdir +} {/tmp} + +test tempfile-1.1 {generate temporary file name and file} { + set filename [::fileutil::tempfile] + set res [file exists $filename] + file delete $filename + unset filename + set res +} {1} + +test tempfile-1.2 {generate writable temporary file name} { + set filename [::fileutil::tempfile] + set res [file writable $filename] + file delete $filename + unset filename + set res +} {1} + +test tempfile-1.3 {generate 100 unique temporary filenames} { + set filenames [list] + for {set i 0} {$i<100} {incr i} { + lappend filenames [::fileutil::tempfile] + } + foreach f $filenames { + file delete $f + } + set i +} {100} + +test tempdir-1.3 {tempdir, user-specified, bad} { + catch {::fileutil::tempdir x y} msg + set msg +} {wrong#args: should be "::fileutil::tempdir ?path?"} + +test tempdir-1.4 {tempdir, user-specified, bad} { + ::fileutil::tempdir [makeDirectory x] + removeDirectory x + + catch {::fileutil::tempdir} msg + removeDirectory x + + lindex [split $msg \n] 0 ; # First line only. +} {Unable to determine a proper directory for temporary files} + +test tempdir-1.5 {tempdir, user-specified, ok} { + ::fileutil::tempdir [makeDirectory x] + + set res [::fileutil::tempdir] + removeDirectory x + set res +} $xpath + +test tempfile-1.4 {temp file in user specified directory} { + ::fileutil::tempdir [makeDirectory x] + + set filename [::fileutil::tempfile TEST] + file delete $filename + ::fileutil::tempdirReset + + removeDirectory x + string match $xpath/TEST* $filename +} 1 + +# ------------------------------------------------------------------------- + +test maketempdir-1.1 {generate temporary directory} { + set filename [::fileutil::maketempdir] + set res [file exists $filename] + file delete $filename + unset filename + set res +} {1} + +test maketempdir-1.2 {generate writable temporary directory} { + set filename [::fileutil::maketempdir] + set res [file writable $filename] + file delete $filename + unset filename + set res +} {1} + +test maketempdir-1.3 {generate 100 unique temporary directories} { + set filenames [list] + for {set i 0} {$i<100} {incr i} { + lappend filenames [::fileutil::maketempdir] + } + foreach f $filenames { + file delete $f + } + set i +} {100} + +test maketempdir-1.4 {temp directory in user specified directory} { + set filename [::fileutil::maketempdir -dir $xpath -prefix TEST] + file delete $filename + string match $xpath/TEST* $filename +} 1 + +# ------------------------------------------------------------------------- + +tmp_cleanup + +# ------------------------------------------------------------------------- + +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/find.setup b/tcllib/modules/fileutil/find.setup new file mode 100644 index 0000000..e64f45f --- /dev/null +++ b/tcllib/modules/fileutil/find.setup @@ -0,0 +1,432 @@ +# -*- tcl -*- +# Support code for the tests of the find command (and incremental find). +# +# Copyright (c) 2007-2015 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: find.setup,v 1.3 2012/08/29 20:42:19 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +# Build a sample tree to search +# Structure +# +# dir +# +--{find 1} +# +--{find 2} +# | +--{file* 2} (This file is unix only) +# +--{file 1} +# +# dir2 +# +-- dotfiles +# +-- .foo +# +-- foo + +proc f_setup {} { + makeDirectory {find 1} + makeDirectory [file join {find 1} {find 2}] + makeFile "" [file join {find 1} {file [1]}] + + if {[string equal $::tcl_platform(platform) windows]} return + + makeFile "test" [file join {find 1} {find 2} {file* 2}] + return +} + +proc f_cleanup {} { + # Remove sym link first. Not doing this causes the file delete for + # the directory to fail (on Windows, Unix would have been fine). + catch {removeFile [file join {find 1} {find 2} {file 3}]} + removeDirectory {find 1} + return +} + +# Extend the previous sample tree with circular symbolic +# links. Unix-only. +# +# dir +# +--{find 1} +# +--{find 2} <----------+ +# | +--{file* 2} | +# | +--{file 3} --> ../{find 2} -+ +# +--{file [1]} + +proc f_setupcircle {} { + f_setup + + set fthree [file join {find 1} {find 2} {file 3}] + set path [makeFile "" $fthree] + removeFile $fthree + + # Added use of 'file link' for Tcl 8.4+, on windows, to have a + # modicum of x-platform testing regarding the handling of symbolic + # links. + + set target [file join .. {find 2}] + + if { + [string equal $::tcl_platform(platform) windows] && + [package vsatisfies [package require Tcl] 8.4] + } { + if {[string equal $::tcl_platform(platform) windows]} { + # Windows doesn't like the .. in the target, it needs an + # absolute path. + + # NOTE/BUG Even so the 'fullnormalize' in the traverser + # returns bogus results for the link, whereas use of file + # normalize and fullnormalize in a simple tclsh, + # i.e. outside of the testing is ok. + + # It seems if the 'file join' in fullnormalize is replaced + # by a plain / then the results are ok again => The + # handling of paths on Windows by the Tcl core is bogus in + # some way which breaks the core 'normalize'. + + set here [pwd] + cd [file dirname [tempPath $fthree]] + file link [file tail $fthree] [file normalize $target] + cd $here + } else { + file link [tempPath $fthree] $target + } + return + } + + exec ln -s $target [tempPath $fthree] + return +} + +# Change previous sample tree so that its circular symbolic +# link points to the base directory. Unix-only. +# +# dir +# +--{find 1} <----------+ +# +--{find 2} | +# | +--{file* 2} | +# | +--{file 3} --> ../../find 1 + +# +--{file [1]} + +proc f_setupcircle2 {} { + f_setup + + set fthree [file join {find 1} {find 2} {file 3}] + set path [makeFile "" $fthree] + removeFile $fthree + + # Added use of 'file link' for Tcl 8.4+, on windows, to have a + # modicum of x-platform testing regarding the handling of symbolic + # links. + + set target [file join .. .. {find 1}] + + if { + [string equal $::tcl_platform(platform) windows] && + [package vsatisfies [package require Tcl] 8.4] + } { + if {[string equal $::tcl_platform(platform) windows]} { + # Windows doesn't like the .. in the target, it needs an + # absolute path. + + # NOTE/BUG Even so the 'fullnormalize' in the traverser + # returns bogus results for the link, whereas use of file + # normalize and fullnormalize in a simple tclsh, + # i.e. outside of the testing is ok. + + # It seems if the 'file join' in fullnormalize is replaced + # by a plain / then the results are ok again => The + # handling of paths on Windows by the Tcl core is bogus in + # some way which breaks the core 'normalize'. + + set here [pwd] + cd [file dirname [tempPath $fthree]] + file link [file tail $fthree] [file normalize $target] + cd $here + } else { + file link [tempPath $fthree] $target + } + return + } + + exec ln -s $target [tempPath $fthree] + return +} + +# Extend the regular sample tree with a broken symbolic link. Unix-only. +# +# dir +# +--{find 1} +# +--{find 2} +# | +--{file* 2} +# | +--{file 3} --> BROKEN +# +--{file [1]} + + +proc f_setupbroken {} { + f_setup + + set fthree [file join {find 1} {find 2} {file 3}] + set path [makeFile "" $fthree] + removeFile $fthree + + # Added use of 'file link' for Tcl 8.4+, on windows, to have a + # modicum of x-platform testing regarding the handling of symbolic + # links. + + set target BROKEN + + if { + [string equal $::tcl_platform(platform) windows] && + [package vsatisfies [package require Tcl] 8.4] + } { + makeFile {} [file dirname $fthree]/BROKEN + + if {[string equal $::tcl_platform(platform) windows]} { + # Windows doesn't like the .. in the target, it needs an + # absolute path. + + # NOTE/BUG Even so the 'fullnormalize' in the traverser + # returns bogus results for the link, whereas use of file + # normalize and fullnormalize in a simple tclsh, + # i.e. outside of the testing is ok. + + # It seems if the 'file join' in fullnormalize is replaced + # by a plain / then the results are ok again => The + # handling of paths on Windows by the Tcl core is bogus in + # some way which breaks the core 'normalize'. + + set here [pwd] + cd [file dirname [tempPath $fthree]] + file link [file tail $fthree] [file normalize $target] + cd $here + } else { + file link [tempPath $fthree] $target + } + removeFile [file dirname $fthree]/BROKEN + return + } + + exec ln -s $target [tempPath $fthree] + return +} + +proc f_setupdot {} { + makeDirectory dotfiles + makeFile "" [file join dotfiles foo] + makeFile "" [file join dotfiles .foo] + return +} + + + +# Complex directory tree with DAG-links and circular links. We want to +# break the latter, but not the former. I.e. DAG-links allow us to +# find a file by multiple paths, and we wish to see these all. +# +# Paths Links Seen Broken Why +# dir/a | a +# dir/b | a/c +# dir/a/c | a/c/g == a +# dir/a/d | a/c/h +# dir/a/c/g --> .. | a/c/h/e == c +# dir/a/c/h --> ../../b | a/c/h/f +# dir/a/c/i | a/c/i +# dir/b/e --> ../a/c | a/d +# dir/b/f | b +# | b/e +# | b/e/g +# | b/e/g/c +# | b/e/g/c/g == b/e/g +# | b/e/g/c/h == b +# | b/e/g/d +# | b/e/h == b +# | b/e/i +# | b/f + +proc pathmap {args} { + set res {} + foreach p $args { + lappend res [tempPath $p] + } + return $res +} + +proc f_setupcircle3 {} { + + makeDirectory z/a + makeDirectory z/a/c + makeDirectory z/b + makeFile "" z/a/d + makeFile "" z/a/c/i + makeFile "" z/b/f + + f_link z/a/c/g ../../a + f_link z/a/c/h ../../b + f_link z/b/e ../a/c + return +} + +proc f_cleanup3 {} { + # Remove sym links first. Not doing this causes the file delete for + # the directory to fail (on Windows, Unix would have been fine). + catch { removeFile z/a/c/g } + catch { removeFile z/a/c/h } + catch { removeFile z/b/e } + removeDirectory z + return +} + +proc f_link {src target} { + # Added use of 'file link' for Tcl 8.4+, on windows, to have a + # modicum of x-platform testing regarding the handling of symbolic + # links. + + if { + [string equal $::tcl_platform(platform) windows] && + [package vsatisfies [package require Tcl] 8.4] + } { + if {[string equal $::tcl_platform(platform) windows]} { + # Windows doesn't like the .. in the target, it needs an + # absolute path. + + # NOTE/BUG Even so the 'fullnormalize' in the traverser + # returns bogus results for the link, whereas use of file + # normalize and fullnormalize in a simple tclsh, + # i.e. outside of the testing is ok. + + # It seems if the 'file join' in fullnormalize is replaced + # by a plain / then the results are ok again => The + # handling of paths on Windows by the Tcl core is bogus in + # some way which breaks the core 'normalize'. + + set here [pwd] + cd [file dirname [tempPath $src]] + file link [file tail $src] [file normalize $target] + cd $here + } else { + file link [tempPath $src] $target + } + return + } + + exec ln -s $target [tempPath $src] + return +} + + +proc f_cleanupdot {} { + removeDirectory dotfiles + return +} + +proc f_setupnostat {} { + # Finding inaccessible directories. Unix only, as I do not know + # how to make the directory inaccessible on Windows, and then + # reaccessible again. + + makeDirectory find3 + makeDirectory find3/find4 + makeFile {} find3/find4/file5 + + if {[string equal $::tcl_platform(platform) windows]} return + exec chmod -x [tempPath find3/find4] + return +} + +proc f_cleanupnostat {} { + if {![string equal $::tcl_platform(platform) windows]} { + exec chmod +x [tempPath find3/find4] + } + removeDirectory find3 + return +} + +proc f_setupnoread {} { + # Finding unreadable directories. + + makeDirectory find3 + makeDirectory find3/find4 + makeFile {} find3/find4/file5 + + if {[string equal $::tcl_platform(platform) windows]} { + file attributes -readonly 1 [tempPath find3/find4] + } else { + exec chmod -r [tempPath find3/find4] + } + return +} + +proc f_cleanupnoread {} { + if {[string equal $::tcl_platform(platform) windows]} { + file attributes -readonly 0 [tempPath find3/find4] + } else { + exec chmod +r [tempPath find3/find4] + } + removeDirectory find3 + return +} + + +proc f_setup_crossindex {} { + makeDirectory s + makeDirectory s/c + makeDirectory s/c/t + + makeDirectory s/d + makeDirectory s/d/t0 + makeDirectory s/d/t1 + makeDirectory s/d/t2 + + makeFile "" s/d/t0/b + makeFile "" s/d/t1/b + makeFile "" s/d/t2/b + + f_link s/c/t/t0 ../../d/t0 + f_link s/c/t/t1 ../../d/t1 + f_link s/c/t/t2 ../../d/t2 + + f_link s/d/t0/s ../../c/t + f_link s/d/t1/s ../../c/t + f_link s/d/t2/s ../../c/t + return +} + +proc f_cleanup_crossindex {} { + removeFile s/d/t0/b + removeFile s/d/t1/b + removeFile s/d/t2/b + removeDirectory s + return +} + +proc f_cleanall {} { + rename f_link {} + rename f_setup {} + rename f_cleanup {} + rename f_cleanup3 {} + rename f_setupcircle {} + rename f_setupcircle2 {} + rename f_setupcircle3 {} + rename f_setupdot {} + rename f_cleanupdot {} + rename f_setupnostat {} + rename f_cleanupnostat {} + rename f_setupnoread {} + rename f_cleanupnoread {} + rename f_setup_crossindex {} + rename f_cleanup_crossindex {} + rename f_cleanall {} + rename fileIsBiggerThan {} + catch {unset ::res} + return +} + +# ------------------------------------------------------------------------- + +proc fileIsBiggerThan {s f} { + expr { + ![file isdirectory $f] && + ([file size $f] > $s) + } +} + +# ------------------------------------------------------------------------- diff --git a/tcllib/modules/fileutil/find.test b/tcllib/modules/fileutil/find.test new file mode 100644 index 0000000..e35b48f --- /dev/null +++ b/tcllib/modules/fileutil/find.test @@ -0,0 +1,367 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: find.test,v 1.7 2007/10/24 19:28:36 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +support { + useLocalFile find.setup +} +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +test find-1.1 {standard recursive find} {macOrUnix} { + f_setup + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file* 2}]] + +test find-1.2 {standard recursive find} {win} { + f_setup + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}]] + +test find-1.3 {find directories} { + f_setup + set res [fileutil::find [tempPath {find 1}] {file isdirectory}] + f_cleanup + set res +} [list [tempPath {find 1/find 2}]] + +test find-1.4 {find files bigger than a given size} {macOrUnix} { + f_setup + set res [fileutil::find [tempPath {find 1}] {fileIsBiggerThan 1}] + f_cleanup + set res +} [list [tempPath {find 1/find 2/file* 2}]] + +# Find has to skip '{file 3}', in the sense that the path should be in +# the output, but it must not be expanded further. Two tests, one for +# all versions of Tcl (8.2+), but only unix, and one for windows, +# restricted to Tcl 8.4+. + +test find-1.5.0 {handling of circular links} {unix} { + f_setupcircle + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test find-1.5.1 {handling of circular links} {win tcl8.4plus} { + f_setupcircle + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}]] + +test find-1.6 {find file} { + f_setup + set res [::fileutil::find [tempPath {find 1/file [1]}]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}]] + +test find-1.7 {find file with filter} { + f_setup + set res [::fileutil::find [tempPath {find 1/file [1]}] {file isfile}] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}]] + +test find-1.8 {find file with filter - negative} { + f_setup + set res [::fileutil::find [tempPath {find 1/file [1]}] {file isdirectory}] + f_cleanup + set res +} {} + +# Behaviour of find with regard to dot-files. + +test find-1.9 {find file dot-files} { + f_setupdot + set res [lsort [::fileutil::find [tempPath dotfiles]]] + f_cleanupdot + set res +} [list [tempPath dotfiles/.foo] \ + [tempPath dotfiles/foo]] + + +# Find has to skip '{file 3}', in the sense that the path should be in +# the output, but it cannot be expanded further, being a broken +# link. Two tests, one for all versions of Tcl (8.2+), but only unix, +# and one for windows, restricted to Tcl 8.4+. + +test find-1.10.0 {handling of broken links} {unix} { + f_setupbroken + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test find-1.10.1 {handling of broken links} {win tcl8.4plus} { + f_setupbroken + set res [lsort [fileutil::find [tempPath {find 1}]]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}]] + + +test find-1.11.0 {find result, circular links, unix} -setup { + f_setupcircle3 +} -constraints unix -body { + join [lsort [fileutil::find [tempPath z]]] \n +} -cleanup { + f_cleanup3 +} -result [join [pathmap \ + z/a z/a/c z/a/c/g z/a/c/h z/a/c/h/e z/a/c/h/f \ + z/a/c/i z/a/d z/b z/b/e z/b/e/g z/b/e/g/c z/b/e/g/d \ + z/b/e/h z/b/e/i z/b/f] \n] + +test find-1.11.1 {find result, circular links, windows, 8.4+} -setup { + f_setupcircle3 +} -constraints {win tcl8.4plus} -body { + join [lsort [fileutil::find [tempPath z]]] \n +} -cleanup { + f_cleanup3 +} -result [join [pathmap \ + z/a z/a/c z/a/c/g z/a/c/h z/a/c/h/e z/a/c/h/f \ + z/a/c/i z/a/d z/b z/b/e z/b/e/g z/b/e/g/c z/b/e/g/d \ + z/b/e/h z/b/e/i z/b/f] \n] + +# ------------------------------------------------------------------------- + +test find-1.12.0 {Traverse pathological circularity, unix} -setup { + f_setup_crossindex +} -constraints unix -body { + join [lsort [fileutil::find [tempPath s]]] \n +} -cleanup { + f_cleanup_crossindex +} -result [join [pathmap \ + s/c \ + s/c/t \ + s/c/t/t0 \ + s/c/t/t0/b \ + s/c/t/t0/s \ + s/c/t/t1 \ + s/c/t/t1/b \ + s/c/t/t1/s \ + s/c/t/t2 \ + s/c/t/t2/b \ + s/c/t/t2/s \ + s/d \ + s/d/t0 \ + s/d/t0/b \ + s/d/t0/s \ + s/d/t0/s/t0 \ + s/d/t0/s/t1 \ + s/d/t0/s/t1/b \ + s/d/t0/s/t1/s \ + s/d/t0/s/t2 \ + s/d/t0/s/t2/b \ + s/d/t0/s/t2/s \ + s/d/t1 \ + s/d/t1/b \ + s/d/t1/s \ + s/d/t1/s/t0 \ + s/d/t1/s/t0/b \ + s/d/t1/s/t0/s \ + s/d/t1/s/t1 \ + s/d/t1/s/t2 \ + s/d/t1/s/t2/b \ + s/d/t1/s/t2/s \ + s/d/t2 \ + s/d/t2/b \ + s/d/t2/s \ + s/d/t2/s/t0 \ + s/d/t2/s/t0/b \ + s/d/t2/s/t0/s \ + s/d/t2/s/t1 \ + s/d/t2/s/t1/b \ + s/d/t2/s/t1/s \ + s/d/t2/s/t2 \ + ] \n] + +test find-1.12.1 {Traverse pathological circularity, windows, 8.4+} -setup { + f_setup_crossindex +} -constraints {win tcl8.4plus} -body { + join [lsort [fileutil::find [tempPath s]]] \n +} -cleanup { + f_cleanup_crossindex +} -result [join [pathmap \ + s/c \ + s/c/t \ + s/c/t/t0 \ + s/c/t/t0/b \ + s/c/t/t0/s \ + s/c/t/t1 \ + s/c/t/t1/b \ + s/c/t/t1/s \ + s/c/t/t2 \ + s/c/t/t2/b \ + s/c/t/t2/s \ + s/d \ + s/d/t0 \ + s/d/t0/b \ + s/d/t0/s \ + s/d/t0/s/t0 \ + s/d/t0/s/t1 \ + s/d/t0/s/t1/b \ + s/d/t0/s/t1/s \ + s/d/t0/s/t2 \ + s/d/t0/s/t2/b \ + s/d/t0/s/t2/s \ + s/d/t1 \ + s/d/t1/b \ + s/d/t1/s \ + s/d/t1/s/t0 \ + s/d/t1/s/t0/b \ + s/d/t1/s/t0/s \ + s/d/t1/s/t1 \ + s/d/t1/s/t2 \ + s/d/t1/s/t2/b \ + s/d/t1/s/t2/s \ + s/d/t2 \ + s/d/t2/b \ + s/d/t2/s \ + s/d/t2/s/t0 \ + s/d/t2/s/t0/b \ + s/d/t2/s/t0/s \ + s/d/t2/s/t1 \ + s/d/t2/s/t1/b \ + s/d/t2/s/t1/s \ + s/d/t2/s/t2 \ + ] \n] + +# ------------------------------------------------------------------------- + +test find-2.0 {find by pattern} { + list [catch { + ::fileutil::findByPattern [tempPath {}] -glob {fil*} foo + } err] $err +} {1 {wrong#args for "::fileutil::findByPattern", should be "::fileutil::findByPattern basedir ?-regexp|-glob? ?--? patterns"}} + +test find-2.1 {find by pattern} { + list [catch { + ::fileutil::findByPattern [tempPath {}] -glob + } err] $err +} {1 {wrong#args for "::fileutil::findByPattern", should be "::fileutil::findByPattern basedir ?-regexp|-glob? ?--? patterns"}} + +test find-2.2 {find by pattern} {macOrUnix} { + f_setupcircle + set res [lsort [::fileutil::findByPattern [tempPath {find 1}] -glob {fil*}]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test find-2.3 {find by pattern} {win} { + f_setup + set res [lsort [::fileutil::findByPattern [tempPath {find 1}] -glob {fil*}]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}]] + +test find-2.4 {find by pattern} { + f_setup + set res [lsort [::fileutil::findByPattern [tempPath {find 1}] -regexp {.*\\[1\\]$}]] + f_cleanup + set res +} [list [tempPath {find 1/file [1]}]] + +# ------------------------------------------------------------------------- + +test find-3.0 {inaccessible directory} {unix notRoot} { + f_setupnostat + set res [lsort [fileutil::find [tempPath find3]]] + f_cleanupnostat + set res +} [list [tempPath find3/find4]] + +test find-3.1 {inaccessible directory} {unix notRoot} { + f_setupnostat + set res [lsort [fileutil::find [tempPath find3/find4]]] + f_cleanupnostat + set res +} {} + +# ------------------------------------------------------------------------- + +test find-sf-3147481-0 {unreadable directory} {notRoot} { + f_setupnoread + set res [lsort [fileutil::find [tempPath find3]]] + f_cleanupnoread + set res +} [list [tempPath find3/find4]] + +test find-sf-3147481-1 {unreadable directory} {notRoot} { + f_setupnoread + set res [lsort [fileutil::find [tempPath find3/find4]]] + f_cleanupnoread + set res +} {} + +# ------------------------------------------------------------------------- + +proc rec {f} { + # Documented filter API: + # f = unqualified filename, + # pwd = directory the file is in. + global res + lappend res [list [pwd] $f] + return 1 +} + +test find-4.0 {find file with filter, filter API} { + f_setup + set res {} + ::fileutil::find [tempPath {find 1}] rec + f_cleanup + lsort $res +} [list \ + [list [tempPath {find 1/find 2}] {file* 2}] \ + [list [tempPath {find 1}] {file [1]}] \ + [list [tempPath {find 1}] {find 2}] \ + ] +# pwd fname + +# ------------------------------------------------------------------------- + +f_cleanall +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/include/cross-index-trav.inc b/tcllib/modules/fileutil/include/cross-index-trav.inc new file mode 100644 index 0000000..a51b823 --- /dev/null +++ b/tcllib/modules/fileutil/include/cross-index-trav.inc @@ -0,0 +1,16 @@ +[example { + package require fileutil::traverse + + proc NoLinks {fileName} { + if {[string equal [file type $fileName] link]} { + return 0 + } + return 1 + } + + fileutil::traverse T /sys/devices -prefilter NoLinks + T foreach p { + puts $p + } + T destroy +}] diff --git a/tcllib/modules/fileutil/include/cross-index.inc b/tcllib/modules/fileutil/include/cross-index.inc new file mode 100644 index 0000000..5abce12 --- /dev/null +++ b/tcllib/modules/fileutil/include/cross-index.inc @@ -0,0 +1,12 @@ +[example { + /sys/class/tty/tty0 --> ../../dev/tty0 + /sys/class/tty/tty1 --> ../../dev/tty1 + /sys/class/tty/tty2 --> ../../dev/tty1 + + /sys/dev/tty0/bus + /sys/dev/tty0/subsystem --> ../../class/tty + /sys/dev/tty1/bus + /sys/dev/tty1/subsystem --> ../../class/tty + /sys/dev/tty2/bus + /sys/dev/tty2/subsystem --> ../../class/tty +}] diff --git a/tcllib/modules/fileutil/inplace.test b/tcllib/modules/fileutil/inplace.test new file mode 100644 index 0000000..b30a593 --- /dev/null +++ b/tcllib/modules/fileutil/inplace.test @@ -0,0 +1,1129 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: inplace.test,v 1.3 2009/10/06 20:07:18 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +test cat-1.1 {cat} { + set path [makeFile "foo\nbar\nbaz\n" {cat [1]}] + + set data [fileutil::cat $path] + + removeFile {cat [1]} + set data +} "foo\nbar\nbaz\n" + +test cat-1.2 {cat multiple files} {macOrUnix} { + set pathA [makeFile "foo\nbar\nbaz\n" {cat [1]}] + set pathB [makeFile "bebop" {cat* 2}] + + set data [fileutil::cat $pathA $pathB] + + removeFile {cat [1]} + removeFile {cat* 2} + set data +} "foo\nbar\nbaz\nbebop\n" + + +test cat-1.3.0 {cat, option processing} { + set path [makeFile "foo\r\nbar\r\nbaz\r\n" {cat [1]}] + + set data [fileutil::cat -translation binary $path] + + removeFile {cat [1]} + set data +} "foo\r\nbar\r\nbaz\r\n" + +test cat-1.3.1 {cat, option processing} { + set path [makeFile "foo\r\nbar\r\nbaz\r\n" {cat [1]}] + + set data [fileutil::cat $path] + + removeFile {cat [1]} + set data +} "foo\nbar\nbaz\n" + +test cat-1.4 {cat multiple files} {macOrUnix} { + set pathA [makeFile "foo\r\nbar\r\nbaz\r\n" {cat [1]}] + set pathB [makeFile "bebop\r\nsnoof" {cat* 2}] + + set data [fileutil::cat $pathA -translation binary $pathB] + + removeFile {cat [1]} + removeFile {cat* 2} + set data +} "foo\nbar\nbaz\nbebop\r\nsnoof\n" + +test cat-1.5.0 {cat, bad arguments} { + catch {fileutil::cat} msg + set msg +} {wrong#args: should be fileutil::cat ?-eofchar|-translation|-encoding arg?+ file ...} + +test cat-1.5.1 {cat, bad arguments} { + catch {fileutil::cat -translation} msg + set msg +} {wrong#args: should be fileutil::cat ?-eofchar|-translation|-encoding arg?+ file ...} + +test cat-1.5.2 {cat, bad arguments} { + catch {fileutil::cat -bogus foo} msg + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + +set xpath [makeFile {} {cat [2]}] +removeFile {cat [2]} + +test cat-1.5.3 {cat, bad arguments, unreadable file} {unixOnly notRoot} { + set path [makeFile {} {cat [2]}] + file attributes $path -permissions 0000 + + catch {fileutil::cat $path} msg + + file attributes $path -permissions 0600 + removeFile {cat [2]} + set msg +} "Cannot read file \"$xpath\", read access is denied" + +test cat-1.5.4 {cat, bad arguments, non-existing file} {unixOnly} { + set path [makeFile {} {cat [2]}] + removeFile {cat [2]} + + catch {fileutil::cat $path} msg + + set msg +} "Cannot read file \"$xpath\", does not exist" + +test cat-1.5.5 {cat, bad arguments, directory} {unixOnly} { + set path [makeDirectory {cat [2]}] + + catch {fileutil::cat $path} msg + + removeDirectory {cat [2]} + set msg +} "Cannot read file \"$xpath\", is not a file" + + +test writefile-1.0 {writeFile, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::writeFile $path {}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test writefile-1.1 {writeFile, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::writeFile $path {}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test writefile-1.2 {writeFile, wrong#args} { + catch {fileutil::writeFile irrelevant} msg + set msg +} {wrong#args: should be fileutil::writeFile ?-eofchar|-translation|-encoding arg? file data} + +test writefile-1.3 {writeFile, wrong#args} { + catch {fileutil::writeFile} msg + set msg +} {wrong#args: should be fileutil::writeFile ?-eofchar|-translation|-encoding arg? file data} + +test writefile-1.4 {writeFile, wrong#args} { + catch {fileutil::writeFile a b c} msg + set msg +} {wrong#args: should be fileutil::writeFile ?-eofchar|-translation|-encoding arg? file data} + +test writefile-1.5 {writeFile, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::writeFile -bogus $path {}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + + +test writefile-2.0 {writeFile, create file} { + set path [makeFile {} out] + removeFile out + + set res {} + lappend res [file exists $path] + + fileutil::writeFile $path dummy + + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + removeFile out + set res +} {0 1 dummy} + +test writefile-2.1 {writeFile, replace file} { + set path [makeFile {} out] + + set res {} + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + fileutil::writeFile $path dummy + + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + removeFile out + set res +} {1 { +} 1 dummy} + +test writefile-2.2 {writeFile, translation 1} { + set path [makeFile {} out] + + fileutil::writeFile -translation binary $path "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\nfoo" + +test writefile-2.3 {writeFile, translation 2} { + set path [makeFile {} out] + + fileutil::writeFile -translation crlf $path "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\r\nfoo" + +test appendtofile-1.0 {appendToFile, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::appendToFile $path {}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test appendtofile-1.1 {appendToFile, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::appendToFile $path {}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test appendtofile-1.2 {appendToFile, wrong#args} { + catch {fileutil::appendToFile irrelevant} msg + set msg +} {wrong#args: should be fileutil::appendToFile ?-eofchar|-translation|-encoding arg? file data} + +test appendtofile-1.3 {appendToFile, wrong#args} { + catch {fileutil::appendToFile} msg + set msg +} {wrong#args: should be fileutil::appendToFile ?-eofchar|-translation|-encoding arg? file data} + +test appendtofile-1.4 {appendToFile, wrong#args} { + catch {fileutil::appendToFile a b c} msg + set msg +} {wrong#args: should be fileutil::appendToFile ?-eofchar|-translation|-encoding arg? file data} + +test appendtofile-1.5 {appendToFile, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::appendToFile -bogus $path {}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + + +test appendtofile-2.0 {appendToFile, create file} { + set path [makeFile {} out] + removeFile out + + set res {} + lappend res [file exists $path] + + fileutil::appendToFile $path dummy + + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + removeFile out + set res +} {0 1 dummy} + +test appendtofile-2.1 {appendToFile, true append} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn| + + set res {} + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + fileutil::appendToFile $path dummy + + lappend res [file exists $path] + lappend res [fileutil::cat $path] + + removeFile out + set res +} {1 aragorn| 1 aragorn|dummy} + +test appendtofile-2.2 {appendToFile, translation 1} { + set path [makeFile {} out] + fileutil::writeFile $path {} + + fileutil::appendToFile -translation binary $path "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\nfoo" + +test appendtofile-2.3 {appendToFile, translation 2} { + set path [makeFile {} out] + fileutil::writeFile $path {} + + fileutil::appendToFile -translation crlf $path "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\r\nfoo" + + +test insertintofile-1.0 {insertIntoFile, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::insertIntoFile $path 0 {}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test insertintofile-1.1 {insertIntoFile, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::insertIntoFile $path 0 {}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test insertintofile-1.2 {insertIntoFile, missing file} { + set path [makeFile {} missing] + removeFile missing + + catch {fileutil::insertIntoFile $path 0 {}} msg + + string map [list $path @] $msg +} {Cannot use file "@", does not exist} + +test insertintofile-1.3 {insertIntoFile, wrong#args} { + catch {fileutil::insertIntoFile irrelevant} msg + set msg +} {wrong#args: should be fileutil::insertIntoFile ?-eofchar|-translation|-encoding arg? file at data} + +test insertintofile-1.4 {insertIntoFile, wrong#args} { + catch {fileutil::insertIntoFile} msg + set msg +} {wrong#args: should be fileutil::insertIntoFile ?-eofchar|-translation|-encoding arg? file at data} + +test insertintofile-1.5 {insertIntoFile, wrong#args} { + catch {fileutil::insertIntoFile a b c d} msg + set msg +} {wrong#args: should be fileutil::insertIntoFile ?-eofchar|-translation|-encoding arg? file at data} + +test insertintofile-1.6 {insertIntoFile, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::insertIntoFile -bogus $path {}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + +test insertintofile-1.7 {insertIntoFile, non-integer insertion point} { + set path [makeFile {} dummy] + + catch {fileutil::insertIntoFile $path foo {}} msg + + removeFile dummy + set msg +} {Expected integer but got "foo"} + +test insertintofile-1.8 {insertIntoFile, negative insertion point} { + set path [makeFile {} dummy] + + catch {fileutil::insertIntoFile $path -1 {}} msg + + removeFile dummy + set msg +} {Bad insertion point -1, before start of data} + +test insertintofile-1.9 {insertIntoFile, insertion point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::insertIntoFile $path 4 {}} msg + + removeFile dummy + set msg +} {Bad insertion point 4, behind end of data} + +# Needed for 1.10 +set path [makeFile {} dummy] +set beyond [file size $path] +incr beyond +removeFile dummy + +test insertintofile-1.10 {insertIntoFile, insertion point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::insertIntoFile $path $beyond {}} msg + + removeFile dummy + set msg +} "Bad insertion point $beyond, behind end of data" + + +test insertintofile-2.0 {insertIntoFile, insert at front} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::insertIntoFile $path 0 dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn dummy|aragorn} + +test insertintofile-2.1 {insertIntoFile, insert degenerated to append} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::insertIntoFile $path \ + [file size $path] dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aragorndummy|} + +test insertintofile-2.2 {insertIntoFile, insert in the middle} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::insertIntoFile $path 3 dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aradummy|gorn} + +test insertintofile-2.3 {insertIntoFile, insert nothing} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::insertIntoFile $path 3 {} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aragorn} + +test insertintofile-2.4 {insertIntoFile, translation 1} { + set path [makeFile {} out] + fileutil::writeFile $path {} + + fileutil::insertIntoFile -translation binary $path 0 "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\nfoo" + +test insertintofile-2.5 {insertIntoFile, translation 2} { + set path [makeFile {} out] + fileutil::writeFile $path {} + + fileutil::insertIntoFile -translation crlf $path 0 "dummy\nfoo" + set res [fileutil::cat -translation binary $path] + + removeFile out + set res +} "dummy\r\nfoo" + + +test removefromfile-1.0 {removeFromFile, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::removeFromFile $path 0 {}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test removefromfile-1.1 {removeFromFile, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::removeFromFile $path 0 {}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test removefromfile-1.2 {removeFromFile, missing file} { + set path [makeFile {} missing] + removeFile missing + + catch {fileutil::removeFromFile $path 0 {}} msg + + string map [list $path @] $msg +} {Cannot use file "@", does not exist} + +test removefromfile-1.3 {removeFromFile, wrong#args} { + catch {fileutil::removeFromFile irrelevant} msg + set msg +} {wrong#args: should be fileutil::removeFromFile ?-eofchar|-translation|-encoding arg? file at n} + +test removefromfile-1.4 {removeFromFile, wrong#args} { + catch {fileutil::removeFromFile} msg + set msg +} {wrong#args: should be fileutil::removeFromFile ?-eofchar|-translation|-encoding arg? file at n} + +test removefromfile-1.5 {removeFromFile, wrong#args} { + catch {fileutil::removeFromFile a b c d} msg + set msg +} {wrong#args: should be fileutil::removeFromFile ?-eofchar|-translation|-encoding arg? file at n} + +test removefromfile-1.6 {removeFromFile, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile -bogus $path {}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + +test removefromfile-1.7 {removeFromFile, non-integer removal point} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path foo 0} msg + + removeFile dummy + set msg +} {Expected integer but got "foo"} + +test removefromfile-1.8 {removeFromFile, negative removal point} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path -1 0} msg + + removeFile dummy + set msg +} {Bad removal point -1, before start of data} + +test removefromfile-1.9 {removeFromFile, removal point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path 4 0} msg + + removeFile dummy + set msg +} {Bad removal point 4, behind end of data} + +set path [makeFile {} dummy] +set beyond [file size $path] +incr beyond +removeFile dummy + +test removefromfile-1.10 {removeFromFile, removal point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path $beyond 0} msg + + removeFile dummy + set msg +} "Bad removal point $beyond, behind end of data" + +test removefromfile-1.11 {removeFromFile, non-integer removal size} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path 0 foo} msg + + removeFile dummy + set msg +} {Expected integer but got "foo"} + +test removefromfile-1.12 {removeFromFile, negative removal size} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path 0 -1} msg + + removeFile dummy + set msg +} {Bad removal size -1} + +test removefromfile-1.13 {removeFromFile, removal size beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path 0 4} msg + + removeFile dummy + set msg +} {Bad removal size 4, going behind end of data} + +set path [makeFile {} dummy] +set beyond [file size $path] +incr beyond +removeFile dummy + +test removefromfile-1.14 {removeFromFile, removal point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::removeFromFile $path 0 $beyond} msg + + removeFile dummy + set msg +} "Bad removal size $beyond, going behind end of data" + + +test removefromfile-2.0 {removeFromFile, remove at front} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::removeFromFile $path 0 3 + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn gorn} + +test removefromfile-2.1 {removeFromFile, removal at end} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::removeFromFile $path 3 4 + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara} + +test removefromfile-2.2 {removeFromFile, removal in the middle} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::removeFromFile $path 3 1 + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn araorn} + +test removefromfile-2.3 {removeFromFile, remove nothing} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::removeFromFile $path 3 0 + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aragorn} + + +test replaceinfile-1.0 {replaceInFile, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::replaceInFile $path 0 0 {}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test replaceinfile-1.1 {replaceInFile, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::replaceInFile $path 0 0 {}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test replaceinfile-1.2 {replaceInFile, missing file} { + set path [makeFile {} missing] + removeFile missing + + catch {fileutil::replaceInFile $path 0 0 {}} msg + + string map [list $path @] $msg +} {Cannot use file "@", does not exist} + +test replaceinfile-1.3 {replaceInFile, wrong#args} { + catch {fileutil::replaceInFile irrelevant} msg + set msg +} {wrong#args: should be fileutil::replaceInFile ?-eofchar|-translation|-encoding arg? file at n data} + +test replaceinfile-1.4 {replaceInFile, wrong#args} { + catch {fileutil::replaceInFile} msg + set msg +} {wrong#args: should be fileutil::replaceInFile ?-eofchar|-translation|-encoding arg? file at n data} + +test replaceinfile-1.5 {replaceInFile, wrong#args} { + catch {fileutil::replaceInFile a b c d e} msg + set msg +} {wrong#args: should be fileutil::replaceInFile ?-eofchar|-translation|-encoding arg? file at n data} + +test replaceinfile-1.6 {replaceInFile, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile -bogus $path {}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + +test replaceinfile-1.7 {replaceInFile, non-integer replacement point} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path foo 0 x} msg + + removeFile dummy + set msg +} {Expected integer but got "foo"} + +test replaceinfile-1.8 {replaceInFile, negative replacement point} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path -1 0 x} msg + + removeFile dummy + set msg +} {Bad replacement point -1, before start of data} + +test replaceinfile-1.9 {replaceInFile, replacement point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path 4 0 x} msg + + removeFile dummy + set msg +} {Bad replacement point 4, behind end of data} + +set path [makeFile {} dummy] +set beyond [file size $path] +incr beyond +removeFile dummy + +test replaceinfile-1.10 {replaceInFile, replacement point beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path $beyond 0 x} msg + + removeFile dummy + set msg +} "Bad replacement point $beyond, behind end of data" + +test replaceinfile-1.11 {replaceInFile, non-integer replacement size} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path 0 foo x} msg + + removeFile dummy + set msg +} {Expected integer but got "foo"} + +test replaceinfile-1.12 {replaceInFile, negative replacement size} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path 0 -1 x} msg + + removeFile dummy + set msg +} {Bad replacement size -1} + +test replaceinfile-1.13 {replaceInFile, replacement size beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path 0 4 x} msg + + removeFile dummy + set msg +} {Bad replacement size 4, going behind end of data} + +set path [makeFile {} dummy] +set beyond [file size $path] +incr beyond +removeFile dummy + +test replaceinfile-1.14 {replaceInFile, replacement size beyond end-of-file} { + set path [makeFile {} dummy] + + catch {fileutil::replaceInFile $path 0 $beyond x} msg + + removeFile dummy + set msg +} "Bad replacement size $beyond, going behind end of data" + + +test replaceinfile-2.0 {replaceInFile, replace at front, remove} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 0 3 {} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn gorn} + +test replaceinfile-2.1 {replaceInFile, replacement at end, remove} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 4 {} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara} + +test replaceinfile-2.2 {replaceInFile, replacement in the middle, remove} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 1 {} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn araorn} + +test replaceinfile-2.3 {replaceInFile, replace nothing} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 0 {} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aragorn} + +test replaceinfile-2.4 {replaceInFile, replace at front, insert} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 0 0 dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn dummy|aragorn} + +test replaceinfile-2.5 {replaceInFile, replacement at end, append} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 7 0 |dummy + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn aragorn|dummy} + +test replaceinfile-2.6 {replaceInFile, replacement in the middle, insert} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 0 |dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara|dummy|gorn} + +test replaceinfile-2.7 {replaceInFile, replace at front, expand} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 0 3 dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn dummy|gorn} + +test replaceinfile-2.8 {replaceInFile, replacement at end, expand} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 4 |dummy + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara|dummy} + +test replaceinfile-2.9 {replaceInFile, replacement in the middle, expand} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 1 |dummy| + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara|dummy|orn} + +test replaceinfile-2.10 {replaceInFile, replace at front, shrink} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 0 3 | + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn |gorn} + +test replaceinfile-2.11 {replaceInFile, replacement at end, shrink} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 4 | + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara|} + +test replaceinfile-2.12 {replaceInFile, replacement in the middle, shrink} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::replaceInFile $path 3 3 | + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn ara|n} + + +test updateinplace-1.0 {updateInPlace, unwriteable file} {unixOnly notRoot} { + set path [makeFile {} unwritable] + file attributes $path -permissions 0000 + + catch {fileutil::updateInPlace $path {string map {}}} msg + + file attributes $path -permissions 0600 + removeFile unwritable + + string map [list $path @] $msg +} {Cannot use file "@", write access is denied} + +test updateinplace-1.1 {updateInPlace, not a file} {unixOnly} { + set path [makeDirectory notafile] + + catch {fileutil::updateInPlace $path {string map {}}} msg + + removeDirectory notafile + string map [list $path @] $msg +} {Cannot use file "@", is not a file} + +test updateinplace-1.2 {updateInPlace, missing file} { + set path [makeFile {} missing] + removeFile missing + + catch {fileutil::updateInPlace $path {string map {}}} msg + + string map [list $path @] $msg +} {Cannot use file "@", does not exist} + +test updateinplace-1.3 {updateInPlace, wrong#args} { + catch {fileutil::updateInPlace irrelevant} msg + set msg +} {wrong#args: should be fileutil::updateInPlace ?-eofchar|-translation|-encoding arg? file cmd} + +test updateinplace-1.4 {updateInPlace, wrong#args} { + catch {fileutil::updateInPlace} msg + set msg +} {wrong#args: should be fileutil::updateInPlace ?-eofchar|-translation|-encoding arg? file cmd} + +test updateinplace-1.5 {updateInPlace, wrong#args} { + catch {fileutil::updateInPlace a b c} msg + set msg +} {wrong#args: should be fileutil::updateInPlace ?-eofchar|-translation|-encoding arg? file cmd} + +test updateinplace-1.6 {updateInPlace, bad option} { + set path [makeFile {} dummy] + + catch {fileutil::updateInPlace -bogus $path {string map {}}} msg + + removeFile dummy + set msg +} {Bad option "-bogus", expected one of -encoding, -eofchar, or -translation} + +test updateinplace-1.7 {updateInPlace, bogus cmd} { + # Error leaves input file unchanged. + set path [makeFile {} dummy] + fileutil::writeFile $path aragorn + + catch {fileutil::updateInPlace $path bogus} msg + + set msg [list $msg [fileutil::cat $path]] + removeFile dummy + set msg +} {{invalid command name "bogus"} aragorn} + + +test updateinplace-2.0 {updateInPlace, string map} { + set path [makeFile {} out] + fileutil::writeFile $path aragorn + + set res {} + lappend res [fileutil::cat $path] + + fileutil::updateInPlace $path {string map {a | r =}} + + lappend res [fileutil::cat $path] + + removeFile out + set res +} {aragorn |=|go=n} + +# ------------------------------------------------------------------------- + +catch {unset path} +catch {unset res} +catch {unset msg} +catch {unset data} +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/multi.man b/tcllib/modules/fileutil/multi.man new file mode 100644 index 0000000..42703ce --- /dev/null +++ b/tcllib/modules/fileutil/multi.man @@ -0,0 +1,56 @@ +[comment {-*- tcl -*- doctools manpage}] +[manpage_begin fileutil::multi n 0.1] +[keywords copy] +[keywords {file utilities}] +[keywords move] +[keywords multi-file] +[keywords remove] +[moddesc {file utilities}] +[titledesc {Multi-file operation, scatter/gather, standard object}] +[category {Programming tools}] +[require Tcl 8.4] +[require fileutil::multi [opt 0.1]] +[require fileutil::multi::op [opt 0.1]] +[require wip [opt 1.0]] +[description] +[para] + +This package provides a single command to perform actions on multiple +files selected by glob patterns. It is a thin layer over the package +[package fileutil::multi::op] which provides objects for the +same. This package simply creates a single such object and directs all +file commands to it. + +[para] + +At the core is a domain specific language allowing the easy +specification of multi-file copy and/or move and/or deletion +operations. Alternate names would be scatter/gather processor, or +maybe even assembler. + +For the detailed specification of this language, and examples, please +see the documention for the package [package fileutil::multi::op]. + +[section {PUBLIC API}] + +The main command of the package is: + +[list_begin definitions] + +[call [cmd ::fileutil::multi] [opt [arg word]...]] + +This command interprets the specified words as file commands to +execute. See the section [sectref-external {FILE API}] of the +documentation for the package [package fileutil::multi::op] for +the set of acceptable commands, their syntax, and semantics. + +[para] + +The result of the command is the result generated by the last file +command it executed. + +[list_end] + +[vset CATEGORY fileutil] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/fileutil/multi.tcl b/tcllib/modules/fileutil/multi.tcl new file mode 100644 index 0000000..b95a728 --- /dev/null +++ b/tcllib/modules/fileutil/multi.tcl @@ -0,0 +1,28 @@ +# ### ### ### ######### ######### ######### +## +# (c) 2007 Andreas Kupries. + +# Multi file operations. Singleton based on the multiop processor. + +# ### ### ### ######### ######### ######### +## Requisites + +package require fileutil::multi::op + +# ### ### ### ######### ######### ######### +## API & Implementation + +namespace eval ::fileutil {} + +# Create the multiop processor object and make its do method the main +# command of this package. +::fileutil::multi::op ::fileutil::multi::obj + +proc ::fileutil::multi {args} { + return [uplevel 1 [linsert $args 0 ::fileutil::multi::obj do]] +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide fileutil::multi 0.1 diff --git a/tcllib/modules/fileutil/multi.test b/tcllib/modules/fileutil/multi.test new file mode 100644 index 0000000..01c4273 --- /dev/null +++ b/tcllib/modules/fileutil/multi.test @@ -0,0 +1,310 @@ +# -*- tcl -*- +# Tests for the multi-op system. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: multi.test,v 1.5 2008/10/11 05:42:37 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.4 +testsNeedTcltest 2.1 + +support { + use snit/snit.tcl snit + use struct/list.tcl struct::list + use wip/wip.tcl wip + useLocal fileutil.tcl fileutil + useLocal multiop.tcl fileutil::multi::op + + useLocalFile multiop.setup +} +testing { + useLocalKeep multi.tcl fileutil::multi +} + +# ------------------------------------------------------------------------- + +test multi-1.0 {multi-file operation, copying} -setup { + mo_setup +} -body { + fileutil::multi reset copy from $src to $dst the *e* except for *n* + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {bertram detlev}} + +test multi-1.1 {multi-file operation, moving} -setup { + mo_setup +} -body { + fileutil::multi reset move from $src into $dst the *e* except for *n* + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{anneliese connie egon egon/bettina egon/suse} {bertram detlev}} + +test multi-1.1 {multi-file operation, deletion} -setup { + mo_setup +} -body { + fileutil::multi reset copy from $src into $dst the *e* except for *n* + fileutil::multi reset remove in $dst the *a* + mo_scan destination +} -cleanup { + mo_cleanup +} -result {detlev} + +test multi-1.2 {multi-file operation, recursive copying} -setup { + mo_setup +} -body { + fileutil::multi reset recursively copy the * from $src to $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {anneliese bertram connie detlev egon egon/bettina egon/suse}} + +test multi-1.3 {multi-file operation, recursive move} -setup { + mo_setup +} -body { + fileutil::multi reset recursively move the * files from $src to $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {egon {anneliese bertram connie detlev egon egon/bettina egon/suse}} + +test multi-1.4 {multi-file operation, expand and save} -setup { + mo_setup +} -body { + fileutil::multi reset expand the *a* in $src -> v + lsort $v +} -cleanup { + mo_cleanup ; unset v +} -result {anneliese bertram} + +test multi-1.5 {multi-file operation, expand and save} -setup { + mo_setup +} -body { + set v {bertram egon} + fileutil::multi reset copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v +} -result {bertram egon egon/bettina egon/suse} + +# ------------------------------------------------------------------------- + +test multi-2.0 {multi-file operation, platform conditionals, not matching, win on unix} -setup { + mo_setup +} -constraints unix -body { + set v {bertram egon} + fileutil::multi reset for-win copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v +} -result {} + +test multi-2.1 {multi-file operation, platform conditionals, not matching, unix on win} -setup { + mo_setup +} -constraints win -body { + set v {bertram egon} + fileutil::multi reset for-unix copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v +} -result {} + +test multi-2.2 {multi-file operation, platform conditionals, matching, unix} -setup { + mo_setup +} -constraints unix -body { + set v {bertram} + fileutil::multi reset for-unix copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v +} -result {bertram} + +test multi-2.3 {multi-file operation, platform conditionals, matching, windows} -setup { + mo_setup +} -constraints win -body { + set v {bertram} + fileutil::multi reset for-win copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v +} -result {bertram} + +# ------------------------------------------------------------------------- + +proc rec {args} { + global res + lappend res $args + return +} + +test multi-3.0 {multi-file operation, invoke user operation} -setup { + mo_setup +} -constraints unix -body { + set v {bertram egon} + set res {} + fileutil::multi reset invoke rec the-set v from $src to $dst as X + set res +} -cleanup { + mo_cleanup ; unset v res +} -result [list [list $src $dst {bertram X egon egon}]] + +# ------------------------------------------------------------------------- + +test multi-4.0 {multi-file operation, moving, files} -setup { + mo_setup +} -body { + fileutil::multi reset move the * files from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{egon egon/bettina egon/suse} {anneliese bertram connie detlev}} + +test multi-4.1 {multi-file operation, moving, directories} -setup { + mo_setup +} -body { + fileutil::multi reset move the * directories from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{anneliese bertram connie detlev} {egon egon/bettina egon/suse}} + +test multi-4.2 {multi-file operation, moving, links} -setup { + mo_setup +} -body { + fileutil::multi reset move the * links from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {}} + +# ------------------------------------------------------------------------- + +test multi-5.0 {multi-file operation, strict destination} -setup { + mo_setup +} -body { + fileutil::multi reset strict into ${dst}x +} -cleanup { + mo_cleanup +} -returnCodes error -result "Destination directory \"${dst}x\": Does not exist" + +test multi-5.1 {multi-file operation, non-strict destination} -setup { + mo_setup +} -body { + fileutil::multi reset !strict into ${dst}x +} -cleanup { + mo_cleanup +} -result {} + +test multi-5.2 {multi-file operation, strict expansion} -setup { + mo_setup +} -body { + fileutil::multi reset strict expand the A* in $src +} -cleanup { + mo_cleanup +} -returnCodes error -result "No files matching pattern \"A*\" in directory \"$src\"" + +test multi-5.3 {multi-file operation, non-strict expansion} -setup { + mo_setup +} -body { + fileutil::multi reset !strict expand the A* in $src +} -cleanup { + mo_cleanup +} -result {} + +# ------------------------------------------------------------------------- + +test multi-6.0 {multi-file operation, query state, defaults} -setup { + mo_setup + fileutil::multi reset +} -body { + list \ + [dictsort [fileutil::multi state?]] \ + [fileutil::multi as?] \ + [fileutil::multi excluded?] \ + [fileutil::multi from?] \ + [fileutil::multi into?] \ + [fileutil::multi operation?] \ + [fileutil::multi recursive?] \ + [fileutil::multi strict?] \ + [fileutil::multi type?] + +} -cleanup { + mo_cleanup +} -result {{as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} {} {} {} {} {} 0 0 {}} + +test multi-6.1 {multi-file operation, query state, settings} -setup { + mo_setup +} -body { + fileutil::multi reset from $src to B not C* as D links recursive strict move + string map [list $src @] [list \ + [dictsort [fileutil::multi state?]] \ + [fileutil::multi as?] \ + [fileutil::multi excluded?] \ + [fileutil::multi from?] \ + [fileutil::multi into?] \ + [fileutil::multi operation?] \ + [fileutil::multi recursive?] \ + [fileutil::multi strict?] \ + [fileutil::multi type?]] +} -cleanup { + mo_cleanup +} -result {{as D excluded C* from @ into B op move recursive 1 strict 1 type links} D C* @ B move 1 1 links} + +# ------------------------------------------------------------------------- + +test multi-7.0 {multi-file operation, change destination dir, subdir} -setup { + mo_setup +} -body { + fileutil::multi reset in A cd B into? +} -cleanup { + mo_cleanup +} -result A/B + +test multi-7.1 {multi-file operation, change destination dir, up} -setup { + mo_setup +} -body { + fileutil::multi reset in A cd B up into? +} -cleanup { + mo_cleanup +} -result A + +# ------------------------------------------------------------------------- + +test multi-8.0 {multi-file operation, stack handling} -setup { + mo_setup +} -body { + list \ + [dictsort [fileutil::multi reset state?]] \ + [dictsort [fileutil::multi \( into B as A not C* state?]] \ + [dictsort [fileutil::multi \) state?]] +} -cleanup { + mo_cleanup +} -result [list \ + {as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} \ + {as A excluded C* from {} into B op {} recursive 0 strict 0 type {}} \ + {as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} \ + ] + +test multi-8.1 {multi-file operation, stack handling, underflow} -setup { + mo_setup +} -body { + fileutil::multi reset \) +} -cleanup { + mo_cleanup +} -returnCodes error -result {Stack underflow} + +# ------------------------------------------------------------------------- +mo_cleanup_all +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/multiop.man b/tcllib/modules/fileutil/multiop.man new file mode 100644 index 0000000..9227be4 --- /dev/null +++ b/tcllib/modules/fileutil/multiop.man @@ -0,0 +1,402 @@ +[comment {-*- tcl -*- doctools manpage}] +[manpage_begin fileutil::multi::op n 0.5.3] +[keywords copy] +[keywords {file utilities}] +[keywords move] +[keywords multi-file] +[keywords remove] +[moddesc {file utilities}] +[titledesc {Multi-file operation, scatter/gather}] +[category {Programming tools}] +[require Tcl 8.4] +[require fileutil::multi::op [opt 0.5.3]] +[require wip [opt 1.0]] +[description] +[para] + +This package provides objects which are able to perform actions on +multiple files selected by glob patterns. + +[para] + +At the core is a domain specific language allowing the easy +specification of multi-file copy and/or move and/or deletion +operations. Alternate names would be scatter/gather processor, or +maybe even assembler. + +[section {CLASS API}] + +The main command of the package is: + +[list_begin definitions] + +[call [cmd ::fileutil::multi::op] [opt [arg opName]] [opt [arg word]...]] + +The command creates a new multi-file operation object with an +associated global Tcl command whose name is [arg opName]. This +command can be used to invoke the various possible file operations. +It has the following general form: + +[list_begin definitions] +[call [cmd opName] [arg option] [opt [arg "arg arg ..."]]] + +[arg Option] and the [arg arg]s determine the exact behavior of the +command. + +[list_end] +[para] + +If the string [const %AUTO%] is used as the [arg opName] then the +package will generate a unique name on its own. + +[para] + +If one or more [arg word]s are specified they are interpreted as an +initial set of file commands to execute. I.e. the method [method do] +of the newly constructed object is implicitly invoked using the words +as its arguments. + +[list_end] + +[para] + +[section {OBJECT API}] + +The following methods are possible for multi-file operation objects: + +[list_begin definitions] + +[call [cmd \$opName] [method do] [opt [arg word]...]] + +This method interprets the specified words as file commands to +execute. See the section [sectref {FILE API}] for the set of +acceptable commands, their syntax, and semantics. + +[para] + +The result of the method is the result generated by the last file +command it executed. + +[list_end] + +[section {FILE API}] + +Both object constructor and method [method do] take a list of words +and interpret them as file commands to execute. The names were chosen +to allow the construction of operations as sentences in near-natural +language. Most of the commands influence just the state of the object, +i.e. are simply providing the configuration used by the command +triggering the actual action. + +[list_begin definitions] +[call [cmd into] [arg directory]] + +Specifies the destination directory for operations. + +[call [cmd in] [arg directory]] + +Alias for [cmd into]. + +[call [cmd to] [arg directory]] + +Alias for [cmd into]. + +[call [cmd from] [arg directory]] + +Specifies the source directory for operations. + +[call [cmd not] [arg pattern]] + +Specifies a glob pattern for paths to be excluded from the operation. + +[call [cmd for] [arg pattern]] + +Alias for [cmd not]. + +[call [cmd exclude] [arg pattern]] + +Alias for [cmd not]. + +[call [cmd but]] + +Has no arguments of its own, but looks ahead in the list of words and +executes all [cmd not] commands immediately following it. This allows the +construction of "but not" and "but exclude" clauses for a more natural +sounding specification of excluded paths. + +[call [cmd except]] + +A semi-alias for [cmd but]. Has no arguments of its own, but looks +ahead in the list of words and executes all [cmd for] commands +immediately following it. This allows the construction of "except for" +clauses for a more natural sounding specification of excluded paths. + +[call [cmd as] [arg name]] + +Specifies a new name for the first file handled by the current +operation. I.e. for the renaming of a single file during the +operation. + +[call [cmd recursive]] + +Signals that file expansion should happen in the whole directory +hierarchy and not just the directory itself. + +[call [cmd recursively]] + +An alias for [cmd recursive]. + +[call [cmd copy]] + +Signals that the operation is the copying of files from source to +destination directory per the specified inclusion and exclusion +patterns. + +[call [cmd move]] + +Signals that the operation is the moving of files from source to +destination directory per the specified inclusion and exclusion +patterns. + +[call [cmd remove]] + +Signals that the operation is the removal of files in the destination +directory per the specified inclusion and exclusion patterns. + +[call [cmd expand]] + +Signals that there is no operation but the calculation of the set of +files from the include and exclude patterns. This operation is not +available if [cmd the-set] is used. + +[call [cmd invoke] [arg cmdprefix]] + +Signals that the user-specified command prefix [arg cmdprefix] is the +operation to perform. The command prefix is executed at the global +level and given the source directory, destination directory, and set +of files (as dictionary mapping from source to destination files), in +this order. + +[call [cmd reset]] + +Forces the object into the ground state where all parts of the +configuration have default values. + +[call [cmd "("]] + +Saves a copy of the current object state on a stack. + +[call [cmd ")"]] + +Takes the state at the top of the state stack and restores it, +i.e. makes it the new current object state. + +[call [cmd cd] [arg directory]] + +Changes the destination directory to the sub-directory [arg directory] +of the current destination. + +[call [cmd up]] + +Changes the destination directory to the parent directory of the +current destination. + +[call [cmd for-windows]] + +Checks that Windows is the current platform. Aborts processing if not. + +[call [cmd for-win]] + +An alias for [cmd for-windows]. + +[call [cmd for-unix]] + +Checks that Unix is the current platform. Aborts processing if not. + +[call [cmd the] [arg pattern]] + +This command specifies the files to operate on per a glob pattern, and +is also the active element, i.e. the command which actually performs +the specified operation. All the other commands only modified the +object state to set the operation up, but di nothing else. + +[para] + +To allow for a more natural sounding syntax this command also looks +ahead in the list of words looks and executes several commands +immediately following it before performing its own actions. + +These commands are [cmd as], [cmd but], [cmd exclude], [cmd except], +[cmd from], and [cmd into] (and aliases). That way these commands act +like qualifiers, and still take effect as if they had been written +before this command. + +[para] + +After the operation has been performed the object state the exclude +patterns and the alias name, if specified, are reset to their default +values (i.e. empty), but nothing else. + +[call [cmd the-set] [arg varname]] + +Like [cmd the], however the set of files to use is not specified +implicitly per a glob pattern, but contained and loaded from the +specified variable. The operation [cmd expand] is not available +if this command is used. + +[call [cmd ->] [arg varname]] + +Saves the set of files from the last expansion into the specified +variable. + +[call [cmd strict]] + +Make file expansion and definition of destination directory ([cmd in] +and aliases) strict, i.e. report errors for missing directories, and +empty expansion. + +[call [cmd !strict]] + +Complement of [cmd strict]. A missing destination directory or empty +expansion are not reported as errors. + +[call [cmd files]] + +Limit the search to files. Default is to accept every type of path. + +[call [cmd links]] + +Limit the search to symbolic links. Default is to accept every type of path. + +[call [cmd directories]] + +Limit the search to directories. Default is to accept every type of path. + +[call [cmd dirs]] + +An alias for [cmd directories]. + +[call [cmd all]] + +Accept all types of paths (default). + +[call [cmd state?]] + +Returns the current state of the object as dictionary. The dictionary keys and their meanings are: + +[list_begin definitions] +[def [const as]] +Last setting made by [cmd as]. +[def [const excluded]] +List of currently known exclusion patterns. +[def [const from]] +Current source directory, set by [cmd from]. +[def [const into]] +Current destination directory, set by [cmd into] (and aliases). +[def [const operation]] +Current operation to perform, set by [cmd copy], [cmd move], [cmd remove], [cmd expand], or [cmd invoke]. +[def [const recursive]] +Current recursion status. Set/unset by [cmd recursive] and [cmd !recursive]. +[def [const strict]] +Current strictness. Set/unset by [cmd strict] and [cmd !strict]. +[def [const type]] +Current path type limiter. Set by either [cmd files], [cmd directories], [cmd links], or [cmd all]. +[list_end] + +[call [cmd as?]] +Returns the current alias name. +[call [cmd excluded?]] +Returns the current set of exclusion patterns. +[call [cmd from?]] +Returns the current source directory. +[call [cmd into?]] +Returns the current destination directory. +[call [cmd operation?]] +Returns the current operation to perform. +[call [cmd recursive?]] +Returns the current recursion status. +[call [cmd strict?]] +Returns the current strictness. +[call [cmd type?]] +Returns the current path type limiter. +[list_end] + +[section EXAMPLES] + +The following examples assume that the variable [var F] contains a +reference to a multi-file operation object. + +[example { + $F do copy \\ + the *.dll \\ + from c:/TDK/PrivateOpenSSL/bin \\ + to [installdir_of tls] +}] + +[example { + $F do move \\ + the * \\ + from /sources \\ + into /scratch \\ + but not *.html + + # Alternatively use 'except for *.html'. +}] + +[example { + $F do \\ + move \\ + the index \\ + from /sources \\ + into /scratch \\ + as pkgIndex.tcl +}] + +[example { + $F do \\ + remove \\ + the *.txt \\ + in /scratch +}] + +Note that the fact that most commands just modify the object state +allows us to use more off forms as specifications instead of just +nearly-natural language sentences. + +For example the second example in this section can re-arranged into: + +[example { + $F do \\ + from /sources \\ + into /scratch \\ + but not *.html \\ + move \\ + the * +}] + +and the result is not only still a valid specification, but even stays +relatively readable. + +[para] + +Further note that the information collected by the commands [cmd but], +[cmd except], and [cmd as] is automatically reset after the associated +[cmd the] was executed. However no other state is reset in that +manner, allowing the user to avoid repetitions of unchanging +information. For example the second and third examples of this section +can be merged and rewritten into the equivalent: + +[example { +$F do \\ + move \\ + the * \\ + from /sources \\ + into /scratch \\ + but not *.html not index \\ + the index \\ + as pkgIndex.tcl +}] + +[vset CATEGORY fileutil] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/fileutil/multiop.setup b/tcllib/modules/fileutil/multiop.setup new file mode 100644 index 0000000..1e31c8b --- /dev/null +++ b/tcllib/modules/fileutil/multiop.setup @@ -0,0 +1,49 @@ +# -*- tcl -*- +# Support code for the tests of the find command (and incremental find). +# +# Copyright (c) 2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: multiop.setup,v 1.1 2007/08/16 04:11:49 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +proc mo_setup {} { + makeDirectory origin + makeFile {} origin/anneliese + makeFile {} origin/bertram + makeFile {} origin/connie + makeFile {} origin/detlev + makeDirectory origin/egon + makeFile {} origin/egon/suse + makeFile {} origin/egon/bettina + makeDirectory destination + return +} + +proc mo_cleanup {} { + removeDirectory origin + removeDirectory destination + return +} + +proc mo_scan {d} { + set base [tempPath $d] + return [lsort -dict \ + [struct::list map [fileutil::find $base] \ + [list fileutil::stripPath $base]]] +} + +proc mo_cleanup_all {} { + rename mo_setup {} + rename mo_cleanup {} + rename mo_scan {} + rename mo_cleanup_all {} + unset ::src ::dst + return +} + +set src [tempPath origin] +set dst [tempPath destination] + +# ------------------------------------------------------------------------- diff --git a/tcllib/modules/fileutil/multiop.tcl b/tcllib/modules/fileutil/multiop.tcl new file mode 100644 index 0000000..4725daf --- /dev/null +++ b/tcllib/modules/fileutil/multiop.tcl @@ -0,0 +1,645 @@ +# ### ### ### ######### ######### ######### +## +# (c) 2007-2008 Andreas Kupries. + +# DSL allowing the easy specification of multi-file copy and/or move +# and/or deletion operations. Alternate names would be scatter/gather +# processor, or maybe even assembler. + +# Examples: +# (1) copy +# into [installdir_of tls] +# from c:/TDK/PrivateOpenSSL/bin +# the *.dll +# +# (2) move +# from /sources +# into /scratch +# the * +# but not *.html +# (Alternatively: except for *.html) +# +# (3) into /scratch +# from /sources +# move +# as pkgIndex.tcl +# the index +# +# (4) in /scratch +# remove +# the *.txt + +# The language is derived from the parts of TclApp's option language +# dealing with files and their locations, yet not identical. In parts +# simplified, in parts more capable, keyword names were changed +# throughout. + +# Language commands + +# From the examples +# +# into DIR : Specify destination directory. +# in DIR : See 'into'. +# from DIR : Specify source directory. +# the PATTERN (...) : Specify files to operate on. +# but not PATTERN : Specify exceptions to 'the'. +# but exclude PATTERN : Specify exceptions to 'the'. +# except for PATTERN : See 'but not'. +# as NAME : New name for file. +# move : Move files. +# copy : Copy files. +# remove : Delete files. +# +# Furthermore +# +# reset : Force to defaults. +# cd DIR : Change destination to subdirectory. +# up : Change destination to parent directory. +# ( : Save a copy of the current state. +# ) : Restore last saved state and make it current. + +# The main active element is the command 'the'. In other words, this +# command not only specifies the files to operate on, but also +# executes the operation as defined in the current state. All other +# commands modify the state to set the operation up, and nothing +# else. To allow for a more natural syntax the active command also +# looks ahead for the commands 'as', 'but', and 'except', and executes +# them, like qualifiers, so that they take effect as if they had been +# written before. The command 'but' and 'except use identical +# constructions to handle their qualifiers, i.e. 'not' and 'for'. + +# Note that the fact that most commands just modify the state allows +# us to use more off forms as specifications instead of just natural +# language sentences For example the example 2 can re-arranged into: +# +# (5) from /sources +# into /scratch +# but not *.html +# move +# the * +# +# and the result is still a valid specification. + +# Further note that the information collected by 'but', 'except', and +# 'as' is automatically reset after the associated 'the' was +# executed. However no other state is reset in that manner, allowing +# the user to avoid repetitions of unchanging information. Lets us for +# example merge the examples 2 and 3. The trivial merge is: + +# (6) move +# into /scratch +# from /sources +# the * +# but not *.html not index +# move +# into /scratch +# from /sources +# the index +# as pkgIndex.tcl +# +# With less repetitions +# +# (7) move +# into /scratch +# from /sources +# the * +# but not *.html not index +# the index +# as pkgIndex.tcl + +# I have not yet managed to find a suitable syntax to specify when to +# add a new extension to the moved/copied files, or have to strip all +# extensions, a specific extension, or even replace extensions. + +# Other possibilities to muse about: Load the patterns for 'not'/'for' +# from a file ... Actually, load the whole exceptions from a file, +# with its contents a proper interpretable word list. Which makes it +# general processing of include files. + +# ### ### ### ######### ######### ######### +## Requisites + +# This processor uses the 'wip' word list interpreter as its +# foundation. + +package require fileutil ; # File testing +package require snit ; # OO support +package require struct::stack ; # Context stack +package require wip ; # DSL execution core + +# ### ### ### ######### ######### ######### +## API & Implementation + +snit::type ::fileutil::multi::op { + # ### ### ### ######### ######### ######### + ## API + + constructor {args} {} ; # create processor + + # ### ### ### ######### ######### ######### + ## API - Implementation. + + constructor {args} { + install stack using struct::stack ${selfns}::stack + $self wip_setup + + # Mapping dsl commands to methods. + defdva \ + reset Reset ( Push ) Pop \ + into Into in Into from From \ + cd ChDir up ChUp as As \ + move Move copy Copy remove Remove \ + but But not Exclude the The \ + except Except for Exclude exclude Exclude \ + to Into -> Save the-set TheSet \ + recursive Recursive recursively Recursive \ + for-win ForWindows for-unix ForUnix \ + for-windows ForWindows expand Expand \ + invoke Invoke strict Strict !strict NotStrict \ + files Files links Links all Everything \ + dirs Directories directories Directories \ + state? QueryState from? QueryFrom into? QueryInto \ + excluded? QueryExcluded as? QueryAs type? QueryType \ + recursive? QueryRecursive operation? QueryOperation \ + strict? QueryStrict !recursive NotRecursive + + $self Reset + runl $args + return + } + + destructor { + $mywip destroy + return + } + + method do {args} { + return [runl $args] + } + + # ### ### ### ######### ######### ######### + ## DSL Implementation + wip::dsl + + # General reset of processor state + method Reset {} { + $stack clear + set base "" + set alias "" + set op "" + set recursive 0 + set src "" + set excl "" + set types {} + set strict 0 + return + } + + # Stack manipulation + method Push {} { + $stack push [list $base $alias $op $opcmd $recursive $src $excl $types $strict] + return + } + + method Pop {} { + if {![$stack size]} { + return -code error {Stack underflow} + } + foreach {base alias op opcmd recursive src excl types strict} [$stack pop] break + return + } + + # Destination directory + method Into {dir} { + if {$dir eq ""} {set dir [pwd]} + if {$strict && ![fileutil::test $dir edr msg {Destination directory}]} { + return -code error $msg + } + set base $dir + return + } + + method ChDir {dir} { $self Into [file join $base $dir] ; return } + method ChUp {} { $self Into [file dirname $base] ; return } + + # Detail + method As {fname} { + set alias [ForceRelative $fname] + return + } + + # Operations + method Move {} { set op move ; return } + method Copy {} { set op copy ; return } + method Remove {} { set op remove ; return } + method Expand {} { set op expand ; return } + + method Invoke {cmdprefix} { + set op invoke + set opcmd $cmdprefix + return + } + + # Operation qualifier + method Recursive {} { set recursive 1 ; return } + method NotRecursive {} { set recursive 0 ; return } + + # Source directory + method From {dir} { + if {$dir eq ""} {set dir [pwd]} + if {![fileutil::test $dir edr msg {Source directory}]} { + return -code error $msg + } + set src $dir + return + } + + # Exceptions + method But {} { run_next_while {not exclude} ; return } + method Except {} { run_next_while {for} ; return } + + method Exclude {pattern} { + lappend excl $pattern + return + } + + # Define the files to operate on, and perform the operation. + method The {pattern} { + run_next_while {as but except exclude from into in to files dirs directories links all} + + switch -exact -- $op { + invoke {Invoke [Resolve [Remember [Exclude [Expand $src $pattern]]]]} + move {Move [Resolve [Remember [Exclude [Expand $src $pattern]]]]} + copy {Copy [Resolve [Remember [Exclude [Expand $src $pattern]]]]} + remove {Remove [Remember [Exclude [Expand $base $pattern]]] } + expand { Remember [Exclude [Expand $base $pattern]] } + } + + # Reset the per-pattern flags of the resolution context back + # to their defaults, for the next pattern. + + set alias {} + set excl {} + set recursive 0 + return + } + + # Like 'The' above, except that the fileset is taken from the + # specified variable. Semi-complementary to 'Save' below. + # Exclusion data and recursion info do not apply for this, this is + # already implicitly covered by the set, when it was generated. + + method TheSet {varname} { + # See 'Save' for the levels we jump here. + upvar 5 $varname var + + run_next_while {as from into in to} + + switch -exact -- $op { + invoke {Invoke [Resolve $var]} + move {Move [Resolve $var]} + copy {Copy [Resolve $var]} + remove {Remove $var } + expand { + return -code error "Expansion does not make sense\ + when we already have a set of files." + } + } + + # Reset the per-pattern flags of the resolution context back + # to their defaults, for the next pattern. + + set alias {} + return + } + + # Save the last expansion result to a variable for use by future commands. + + method Save {varname} { + # Levels to jump. Brittle. + # 5: Caller + # 4: object do ... + # 3: runl + # 2: wip::runl + # 1: run_next + # 0: Here + upvar 5 $varname v + set v $lastexpansion + return + } + + # Platform conditionals ... + + method ForUnix {} { + global tcl_platform + if {$tcl_platform(platform) eq "unix"} return + # Kill the remaining code. This effectively aborts processing. + replacel {} + return + } + + method ForWindows {} { + global tcl_platform + if {$tcl_platform(platform) eq "windows"} return + # Kill the remaining code. This effectively aborts processing. + replacel {} + return + } + + # Strictness + + method Strict {} { + set strict 1 + return + } + + method NotStrict {} { + set strict 0 + return + } + + # Type qualifiers + + method Files {} { + set types files + return + } + + method Links {} { + set types links + return + } + + method Directories {} { + set types dirs + return + } + + method Everything {} { + set types {} + return + } + + # State interogation + + method QueryState {} { + return [list \ + from $src \ + into $base \ + as $alias \ + op $op \ + excluded $excl \ + recursive $recursive \ + type $types \ + strict $strict \ + ] + } + method QueryExcluded {} { + return $excl + } + method QueryFrom {} { + return $src + } + method QueryInto {} { + return $base + } + method QueryAs {} { + return $alias + } + method QueryOperation {} { + return $op + } + method QueryRecursive {} { + return $recursive + } + method QueryType {} { + return $types + } + method QueryStrict {} { + return $strict + } + + # ### ### ### ######### ######### ######### + ## DSL State + + component stack ; # State stack - ( ) + variable base "" ; # Destination dir - into, in, cd, up + variable alias "" ; # Detail - as + variable op "" ; # Operation - move, copy, remove, expand, invoke + variable opcmd "" ; # Command prefix for invoke. + variable recursive 0 ; # Op. qualifier: recursive expansion? + variable src "" ; # Source dir - from + variable excl "" ; # Excluded files - but not|exclude, except for + # incl ; # Included files - the (immediate use) + variable types {} ; # Limit glob/find to specific types (f, l, d). + variable strict 0 ; # Strictness of into/Expand + + variable lastexpansion "" ; # Area for last expansion result, for 'Save' to take from. + + # ### ### ### ######### ######### ######### + ## Internal -- Path manipulation helpers. + + proc ForceRelative {path} { + set pathtype [file pathtype $path] + switch -exact -- $pathtype { + relative { + return $path + } + absolute { + # Chop off the first element in the path, which is the + # root, either '/' or 'x:/'. If this was the only + # element assume an empty path. + + set path [lrange [file split $path] 1 end] + if {![llength $path]} {return {}} + return [eval [linsert $path 0 file join]] + } + volumerelative { + return -code error {Unable to handle volumerelative path, yet} + } + } + + return -code error \ + "file pathtype returned unknown type \"$pathtype\"" + } + + proc ForceAbsolute {path} { + return [file join [pwd] $path] + } + + # ### ### ### ######### ######### ######### + ## Internal - Operation execution helpers + + proc Invoke {files} { + upvar 1 base base src src opcmd opcmd + uplevel #0 [linsert $opcmd end $src $base $files] + return + } + + proc Move {files} { + upvar 1 base base src src + + foreach {s d} $files { + set s [file join $src $s] + set d [file join $base $d] + + file mkdir [file dirname $d] + file rename -force $s $d + } + return + } + + proc Copy {files} { + upvar 1 base base src src + + foreach {s d} $files { + set s [file join $src $s] + set d [file join $base $d] + + file mkdir [file dirname $d] + if { + [file isdirectory $s] && + [file exists $d] && + [file isdirectory $d] + } { + # Special case: source and destination are + # directories, and the latter exists. This puts the + # source under the destination, and may even prevent + # copying at all. The semantics of the operation is + # that the source is the destination. We avoid the + # trouble by copying the contents of the source, + # instead of the directory itself. + foreach path [glob -directory $s *] { + file copy -force $path $d + } + } else { + file copy -force $s $d + } + } + return + } + + proc Remove {files} { + upvar 1 base base + + foreach f $files { + file delete -force [file join $base $f] + } + return + } + + # ### ### ### ######### ######### ######### + ## Internal -- Resolution helper commands + + typevariable tmap -array { + files {f TFile} + links {l TLink} + dirs {d TDir} + {} {{} {}} + } + + proc Expand {dir pattern} { + upvar 1 recursive recursive strict strict types types tmap tmap + # FUTURE: struct::list filter ... + + set files {} + if {$recursive} { + # Recursion through the entire directory hierarchy, save + # all matching paths. + + set filter [lindex $tmap($types) 1] + if {$filter ne ""} { + set filter [myproc $filter] + } + + foreach f [fileutil::find $dir $filter] { + if {![string match $pattern [file tail $f]]} continue + lappend files [fileutil::stripPath $dir $f] + } + } else { + # No recursion, just scan the whole directory for matching paths. + # check for specific types integrated. + + set filter [lindex $tmap($types) 0] + if {$filter ne ""} { + foreach f [glob -nocomplain -directory $dir -types $filter -- $pattern] { + lappend files [fileutil::stripPath $dir $f] + } + } else { + foreach f [glob -nocomplain -directory $dir -- $pattern] { + lappend files [fileutil::stripPath $dir $f] + } + } + } + + if {[llength $files]} {return $files} + if {!$strict} {return {}} + + return -code error \ + "No files matching pattern \"$pattern\" in directory \"$dir\"" + } + + proc TFile {f} {file isfile $f} + proc TDir {f} {file isdirectory $f} + proc TLink {f} {expr {[file type $f] eq "link"}} + + proc Exclude {files} { + upvar 1 excl excl + + # FUTURE: struct::list filter ... + set res {} + foreach f $files { + if {[IsExcluded $f $excl]} continue + lappend res $f + } + return $res + } + + proc IsExcluded {f patterns} { + foreach p $patterns { + if {[string match $p $f]} {return 1} + } + return 0 + } + + proc Resolve {files} { + upvar 1 alias alias + set res {} + foreach f $files { + + # Remember alias for processing and auto-invalidate to + # prevent contamination of the next file. + + set thealias $alias + set alias "" + + if {$thealias eq ""} { + set d $f + } else { + set d [file dirname $f] + if {$d eq "."} { + set d $thealias + } else { + set d [file join $d $thealias] + } + } + + lappend res $f $d + } + return $res + } + + proc Remember {files} { + upvar 1 lastexpansion lastexpansion + set lastexpansion $files + return $files + } + + ## + # ### ### ### ######### ######### ######### +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide fileutil::multi::op 0.5.3 diff --git a/tcllib/modules/fileutil/multiop.test b/tcllib/modules/fileutil/multiop.test new file mode 100644 index 0000000..a94e1c6 --- /dev/null +++ b/tcllib/modules/fileutil/multiop.test @@ -0,0 +1,370 @@ +# -*- tcl -*- +# Tests for the multi-op system. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: multiop.test,v 1.7 2008/10/11 05:42:37 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.4 +testsNeedTcltest 2.1 + +support { + use snit/snit.tcl snit + use struct/list.tcl struct::list + + # Note: Order is important for the next two. First ::fileutil is + # wiped out, and through this all previously created multi::op + # objects, like from the 'multi.test'. This also kills the + # embedded wip objects, and wiping out ::wip after is ok. + + # However, if we were to wipe out ::wip before ::fileutil kills + # the wip objects, and leaves the multi::op objects with dangling + # references. Wiping them then out then causes snit to write error + # messages to stdout (RT.InstanceTrace) due to the already deleted + # namespaces for the wip objects. + + useLocal fileutil.tcl fileutil + use wip/wip.tcl wip + + useLocalFile multiop.setup +} +testing { + useLocal multiop.tcl fileutil::multi::op +} + +# ------------------------------------------------------------------------- + +test multiop-1.0 {multi-file operation, copying} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do copy the *e* from $src to $dst except for *n* + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {bertram detlev}} + +test multiop-1.1 {multi-file operation, moving} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do move the *e* from $src into $dst except for *n* + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{anneliese connie egon egon/bettina egon/suse} {bertram detlev}} + +test multiop-1.1 {multi-file operation, deletion} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do copy the *e* from $src into $dst except for *n* + X do remove the *a* in $dst + mo_scan destination +} -cleanup { + mo_cleanup + X destroy +} -result {detlev} + +test multiop-1.2 {multi-file operation, recursive copying} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do recursively copy the * from $src to $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {anneliese bertram connie detlev egon egon/bettina egon/suse}} + +test multiop-1.3 {multi-file operation, recursive move} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do recursively move the * files from $src to $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {egon {anneliese bertram connie detlev egon egon/bettina egon/suse}} + +test multiop-1.4 {multi-file operation, expand and save} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do expand the *a* in $src -> v + lsort $v +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {anneliese bertram} + +test multiop-1.5 {multi-file operation, expand and save} -setup { + mo_setup + fileutil::multi::op X +} -body { + set v {bertram egon} + X do copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {bertram egon egon/bettina egon/suse} + +# ------------------------------------------------------------------------- + +test multiop-2.0 {multi-file operation, platform conditionals, not matching, win on unix} -setup { + mo_setup + fileutil::multi::op X +} -constraints unix -body { + set v {bertram egon} + X do for-win copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {} + +test multiop-2.1 {multi-file operation, platform conditionals, not matching, unix on win} -setup { + mo_setup + fileutil::multi::op X +} -constraints win -body { + set v {bertram egon} + X do for-unix copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {} + +test multiop-2.2 {multi-file operation, platform conditionals, matching, unix} -setup { + mo_setup + fileutil::multi::op X +} -constraints unix -body { + set v {bertram} + X do for-unix copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {bertram} + +test multiop-2.3 {multi-file operation, platform conditionals, matching, windows} -setup { + mo_setup + fileutil::multi::op X +} -constraints win -body { + set v {bertram} + X do for-win copy the-set v from $src to $dst + mo_scan destination +} -cleanup { + mo_cleanup ; unset v + X destroy +} -result {bertram} + +# ------------------------------------------------------------------------- + +proc rec {args} { + global res + lappend res $args + return +} + +test multiop-3.0 {multi-file operation, invoke user operation} -setup { + mo_setup + fileutil::multi::op X +} -constraints unix -body { + set v {bertram egon} + set res {} + X do invoke rec the-set v from $src to $dst as X + set res +} -cleanup { + mo_cleanup ; unset v res + X destroy +} -result [list [list $src $dst {bertram X egon egon}]] + +# ------------------------------------------------------------------------- + +test multiop-4.0 {multi-file operation, moving, files} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do move the * files from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{egon egon/bettina egon/suse} {anneliese bertram connie detlev}} + +test multiop-4.1 {multi-file operation, moving, directories} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do move the * directories from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{anneliese bertram connie detlev} {egon egon/bettina egon/suse}} + +test multiop-4.2 {multi-file operation, moving, links} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do move the * links from $src into $dst + list [mo_scan origin] [mo_scan destination] +} -cleanup { + mo_cleanup + X destroy +} -result {{anneliese bertram connie detlev egon egon/bettina egon/suse} {}} + +# ------------------------------------------------------------------------- + +test multiop-5.0 {multi-file operation, strict destination} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do strict into ${dst}x +} -cleanup { + mo_cleanup + X destroy +} -returnCodes error -result "Destination directory \"${dst}x\": Does not exist" + +test multiop-5.1 {multi-file operation, non-strict destination} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do !strict into ${dst}x +} -cleanup { + mo_cleanup + X destroy +} -result {} + +test multiop-5.2 {multi-file operation, strict expansion} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do strict expand the A* in $src +} -cleanup { + mo_cleanup + X destroy +} -returnCodes error -result "No files matching pattern \"A*\" in directory \"$src\"" + +test multiop-5.3 {multi-file operation, non-strict expansion} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do !strict expand the A* in $src +} -cleanup { + mo_cleanup + X destroy +} -result {} + +# ------------------------------------------------------------------------- + +test multiop-6.0 {multi-file operation, query state, defaults} -setup { + mo_setup + fileutil::multi::op X +} -body { + list \ + [dictsort [X do state?]] \ + [X do as?] \ + [X do excluded?] \ + [X do from?] \ + [X do into?] \ + [X do operation?] \ + [X do recursive?] \ + [X do strict?] \ + [X do type?] + +} -cleanup { + mo_cleanup + X destroy +} -result {{as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} {} {} {} {} {} 0 0 {}} + +test multiop-6.1 {multi-file operation, query state, settings} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do from $src to B not C* as D links recursive strict move + string map [list $src @] [list \ + [dictsort [X do state?]] \ + [X do as?] \ + [X do excluded?] \ + [X do from?] \ + [X do into?] \ + [X do operation?] \ + [X do recursive?] \ + [X do strict?] \ + [X do type?]] +} -cleanup { + mo_cleanup + X destroy +} -result {{as D excluded C* from @ into B op move recursive 1 strict 1 type links} D C* @ B move 1 1 links} + +# ------------------------------------------------------------------------- + +test multiop-7.0 {multi-file operation, change destination dir, subdir} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do in A cd B into? +} -cleanup { + mo_cleanup + X destroy +} -result A/B + +test multiop-7.1 {multi-file operation, change destination dir, up} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do in A cd B up into? +} -cleanup { + mo_cleanup + X destroy +} -result A + +# ------------------------------------------------------------------------- + +test multiop-8.0 {multi-file operation, stack handling} -setup { + mo_setup + fileutil::multi::op X +} -body { + list \ + [dictsort [X do state?]] \ + [dictsort [X do \( into B as A not C* state?]] \ + [dictsort [X do \) state?]] +} -cleanup { + mo_cleanup + X destroy +} -result [list \ + {as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} \ + {as A excluded C* from {} into B op {} recursive 0 strict 0 type {}} \ + {as {} excluded {} from {} into {} op {} recursive 0 strict 0 type {}} \ + ] + +test multiop-8.1 {multi-file operation, stack handling, underflow} -setup { + mo_setup + fileutil::multi::op X +} -body { + X do \) +} -cleanup { + mo_cleanup + X destroy +} -returnCodes error -result {Stack underflow} + +# ------------------------------------------------------------------------- +mo_cleanup_all +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/pathops.test b/tcllib/modules/fileutil/pathops.test new file mode 100644 index 0000000..0ff6866 --- /dev/null +++ b/tcllib/modules/fileutil/pathops.test @@ -0,0 +1,515 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2009 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: pathops.test,v 1.2 2009/10/27 19:16:34 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +set dir $::tcltest::temporaryDirectory + +# ------------------------------------------------------------------------- + +test jail-1.0 {jail error} { + catch {::fileutil::jail} res + set res +} [tcltest::wrongNumArgs {::fileutil::jail} {jail filename} 0] + +test jail-1.2 {jail error} { + catch {::fileutil::jail a} res + set res +} [tcltest::wrongNumArgs {::fileutil::jail} {jail filename} 1] + +test jail-1.3 {jail error} { + catch {::fileutil::jail a b c} res + set res +} [tcltest::tooManyArgs {::fileutil::jail} {jail filename}] + +test jail-2.0 {jail relative} { + ::fileutil::jail /var/www a/b/c +} /var/www/a/b/c + +test jail-2.1 {jail absolute outside} { + ::fileutil::jail /var/www /a/b/c +} /var/www/a/b/c + +test jail-2.1.1 {jail absolute outside, spaces} { + ::fileutil::jail /var/www {/a/b/c d} +} {/var/www/a/b/c d} + +test jail-2.2 {jail absolute inside} { + ::fileutil::jail /var/www /var/www/a/b/c +} /var/www/a/b/c + +test jail-2.2.1 {jail absolute inside} { + ::fileutil::jail /var/www {/var/www/a/b/c d} +} {/var/www/a/b/c d} + +test jail-2.3 {try to escape from jail} { + ::fileutil::jail /var/www ../../etc/passwd +} /var/www/etc/passwd + +test jail-2.4 {jail is relative itself} { + ::fileutil::jail a b +} [file join $dir a b] + +test jail-2.4.1 {jail is relative itself, spaces in path} { + ::fileutil::jail a {b c} +} [file join $dir a {b c}] + +test jail-2.4.2 {jail is relative itself, spaces in path} { + ::fileutil::jail {a b} {c d} +} [file join $dir {a b} {c d}] + + +# Need tests using non-existing paths for sure. Similar tests for +# 'normalize' as well. + +# Tests for the internal 'Normalize' command. This is our forward +# compatibility wrapper and it should behave identical to the +# 8.4. builtin 'file normalize'. We pilfered the test cases from the +# test suite for 'file normalize' in the Tcl core. + +if {![string equal $::tcl_platform(platform) windows]} { + + set dirfile [makeDirectory dir.file] + set dirbfile [makeDirectory dir2.file] + set insidefile [makeFile "test file in directory" dir.file/inside.file] + set gorpfile [makeFile "test file" gorp.file] + + # Paths for the links. + + set linkfile [tempPath link.file] + set dirlink [tempPath dir.link] + set dirblink [tempPath dir2.link] + set linkinsidefile [tempPath $dirfile/linkinside.file] + set dirbblink [tempPath $dirbfile/dir2.link]] + + # Create the links. Unix specific. + + exec ln -s gorp.file $linkfile + exec ln -s inside.file $linkinsidefile + exec ln -s dir.file $dirlink + exec ln -s dir.link $dirblink + exec ln -s ../dir2.link $dirbblink + + # File/Directory structure created by the above. + # + # /FOO/dir2.link -> dir.link + # /FOO/dir.link -> dir.file + # /FOO/dir.file/ + # /FOO/dir.file/linkinside.file -> inside.file + # /FOO/dir.file/inside.file + # + # /FOO/link.file -> gorp.file + # /FOO/gorp.file + # + # /FOO/dir2.file/ + # /FOO/dir2.file/dir2.link -> ../dir2.link +} + + +test fu-normalize-1.0 {link normalisation} {unixOnly} { + # Symlink of last path element is not resolved. + string equal \ + [::fileutil::Normalize $gorpfile] \ + [::fileutil::Normalize $linkfile] +} {0} + +test fu-normalize-1.1 {link normalisation} {unixOnly} { + # Symlink of last path element is not resolved. + string equal \ + [::fileutil::Normalize $dirfile] \ + [::fileutil::Normalize $dirlink] +} {0} + +test fu-normalize-1.2 {link normalisation} {unixOnly} { + # Link higher in path is resolved (File!, non-existing last component). + string equal \ + [::fileutil::Normalize [file join $gorpfile foo]] \ + [::fileutil::Normalize [file join $linkfile foo]] +} {1} + +test fu-normalize-1.3 {link normalisation} {unixOnly} { + # Link higher in path is resolved (Directory, non-existing last component). + string equal \ + [::fileutil::Normalize [file join $dirfile foo]] \ + [::fileutil::Normalize [file join $dirlink foo]] +} {1} + +test fu-normalize-1.4 {link normalisation} {unixOnly} { + # Link higher in path is resolved (Directory, existing last component). + string equal \ + [::fileutil::Normalize $insidefile] \ + [::fileutil::Normalize [file join $dirlink inside.file]] +} {1} + +test fu-normalize-1.5 {link normalisation} {unixOnly} { + # Identical paths. + string equal \ + [::fileutil::Normalize $linkinsidefile] \ + [::fileutil::Normalize $linkinsidefile] +} {1} + +test fu-normalize-1.6 {link normalisation} {unixOnly} { + # Double link, one in last component, that one not resolved. + string equal \ + [::fileutil::Normalize $linkinsidefile] \ + [::fileutil::Normalize [file join $dirlink inside.file]] +} {0} + +test fu-normalize-1.7 {link normalisation} {unixOnly} { + # Double link, both higher up, second is file!, both resolved + string equal \ + [::fileutil::Normalize [file join $dirlink linkinside.file foo]] \ + [::fileutil::Normalize [file join $insidefile foo]] +} {1} + +test fu-normalize-1.8 {link normalisation} {unixOnly} { + # Directory link, and bad last component + string equal \ + [::fileutil::Normalize ${linkinsidefile}foo] \ + [::fileutil::Normalize [file join $dirlink inside.filefoo]] +} {0} + +if 0 { + test fu-normalize-1.9 {link normalisation} {unixOnly} { + file delete -force $dirlink + file link $dirlink [file nativename $dirfile] + string equal \ + [::fileutil::Normalize [file join $linkinsidefile foo]] \ + [::fileutil::Normalize [file join $dirlink inside.file foo]] + } {1} +} + +test fu-normalize-1.10 {link normalisation: double link} {unixOnly} { + # Double symlink in one component. + string equal \ + [::fileutil::Normalize [file join $linkinsidefile foo]] \ + [::fileutil::Normalize [file join $dirblink inside.file foo]] +} {1} + + +test fu-normalize-1.11 {link normalisation: double link, back in tree} {unixOnly} { + # Double link and back up in the tree. + + string equal \ + [::fileutil::Normalize [file join $linkinsidefile foo]] \ + [::fileutil::Normalize [file join $dirbblink inside.file foo]] +} {1} + + +test fu-normalize-2.0 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::Normalize /a/b/c +} /a/b/c + +test fu-normalize-2.1 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::Normalize /a/../b/c +} /b/c + +test fu-normalize-2.2 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::Normalize /a/./b/c +} /a/b/c + +test fu-normalize-2.3 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::Normalize /../b/c +} /b/c + +test fu-normalize-2.4 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::Normalize /a/../../b/c +} /b/c + + + +# Based on the internal Normalize, a fullnormalize (which resolves a +# link in the last element as well. + +test fu-fullnormalize-1.0 {link normalisation} {unixOnly} { + # Symlink of last path element _is_ resolved. + string equal \ + [::fileutil::fullnormalize $gorpfile] \ + [::fileutil::fullnormalize $linkfile] +} {1} + +test fu-fullnormalize-1.1 {link normalisation} {unixOnly} { + # Symlink of last path element _is_ resolved. + string equal \ + [::fileutil::fullnormalize $dirfile] \ + [::fileutil::fullnormalize $dirlink] +} {1} + +test fu-fullnormalize-1.2 {link normalisation} {unixOnly} { + # Link higher in path is resolved (File!, non-existing last component). + string equal \ + [::fileutil::fullnormalize [file join $gorpfile foo]] \ + [::fileutil::fullnormalize [file join $linkfile foo]] +} {1} + +test fu-fullnormalize-1.3 {link normalisation} {unixOnly} { + # Link higher in path is resolved (Directory, non-existing last component). + string equal \ + [::fileutil::fullnormalize [file join $dirfile foo]] \ + [::fileutil::fullnormalize [file join $dirlink foo]] +} {1} + +test fu-fullnormalize-1.4 {link normalisation} {unixOnly} { + # Link higher in path is resolved (Directory, existing last component). + string equal \ + [::fileutil::fullnormalize $insidefile] \ + [::fileutil::fullnormalize [file join $dirlink inside.file]] +} {1} + +test fu-fullnormalize-1.5 {link normalisation} {unixOnly} { + # Identical paths. + string equal \ + [::fileutil::fullnormalize $linkinsidefile] \ + [::fileutil::fullnormalize $linkinsidefile] +} {1} + +test fu-fullnormalize-1.6 {link normalisation} {unixOnly} { + # Double link, one in last component, this one is resolved. + string equal \ + [::fileutil::fullnormalize $linkinsidefile] \ + [::fileutil::fullnormalize [file join $dirlink inside.file]] +} {1} + +test fu-fullnormalize-1.7 {link normalisation} {unixOnly} { + # Double link, both higher up, second is file!, both resolved + string equal \ + [::fileutil::fullnormalize [file join $dirlink linkinside.file foo]] \ + [::fileutil::fullnormalize [file join $insidefile foo]] +} {1} + +test fu-fullnormalize-1.8 {link normalisation} {unixOnly} { + # Directory link, and bad last component + string equal \ + [::fileutil::fullnormalize ${linkinsidefile}foo] \ + [::fileutil::fullnormalize [file join $dirlink inside.filefoo]] +} {0} + +test fu-fullnormalize-1.10 {link normalisation: double link} {unixOnly} { + # Double symlink in one component. + string equal \ + [::fileutil::fullnormalize [file join $linkinsidefile foo]] \ + [::fileutil::fullnormalize [file join $dirblink inside.file foo]] +} {1} + + +test fu-fullnormalize-2.0 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::fullnormalize /a/b/c +} /a/b/c + +test fu-fullnormalize-2.1 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::fullnormalize /a/../b/c +} /b/c + +test fu-fullnormalize-2.2 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::fullnormalize /a/./b/c +} /a/b/c + +test fu-fullnormalize-2.3 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::fullnormalize /../b/c +} /b/c + +test fu-fullnormalize-2.4 {normalisation, non-existing paths} {unixOnly} { + ::fileutil::fullnormalize /a/../../b/c +} /b/c + +# Cleaning up after. + +removeFile find3/find4/file5 +removeDirectory find3/find4 +removeDirectory find3 +removeDirectory touchTest +removeDirectory installDst +removeDirectory installSrc +removeDirectory {find 1} +removeDirectory dotfiles +removeDirectory grepTest + +if {![string equal $::tcl_platform(platform) windows]} { + file delete -force $linkfile + file delete -force $dirlink + file delete -force $dirblink + file delete -force $linkinsidefile + file delete -force $dirbblink + + removeFile dir.file/inside.file + removeFile gorp.file + removeDirectory dir.file + removeDirectory dir2.file +} + +# ------------------------------------------------------------------------- +# Computation of paths relative to a base. + +test fu-relative-1.0 {fileutil::relative, wrong#args} { + catch {fileutil::relative} msg + set msg +} [tcltest::wrongNumArgs fileutil::relative {base dst} 0] + +test fu-relative-1.1 {fileutil::relative, wrong#args} { + catch {fileutil::relative a} msg + set msg +} [tcltest::wrongNumArgs fileutil::relative {base dst} 1] + +test fu-relative-1.2 {fileutil::relative, wrong#args} { + catch {fileutil::relative a b c} msg + set msg +} [tcltest::tooManyArgs fileutil::relative {base dst}] + +foreach {n base dst result} { + 0 /base /base/destination destination + 1 /base /destination ../destination + 2 base base/destination destination + 3 base destination ../destination + 4 /sub/base /sub/sub/destination ../sub/destination + 5 /sub/sub/base /sub/destination ../../destination + 6 sub/base sub/sub/destination ../sub/destination + 7 sub/sub/base sub/destination ../../destination + 8 /base /base . + 9 base base . + 10 /base/sub /base/sub . + 11 base/sub base/sub . + 12 /base/sub /base .. + 13 base/sub base .. + 14 base/sub destination ../../destination + 15 base/tcl base/common ../common + 16 base/tcl/x base/common ../../common + 17 /base/tcl /base/common ../common + 18 /base/tcl/x /base/common ../../common +} { + test fu-relative-2.$n {fileutil::relative} { + fileutil::relative $base $dst + } $result +} + +foreach {n base dst ra rb} { + 0 /base base/destination absolute relative + 1 base /destination relative absolute +} { + test fu-relative-3.$n {fileutil::relative, bad mix} unixOnly { + catch {fileutil::relative $base $dst} msg + set msg + } "Unable to compute relation for paths of different pathtypes: $ra vs. $rb, ($base vs. $dst)" +} + +foreach {n base dst ra rb} { + 0 /base base/destination volumerelative relative + 1 base /destination relative volumerelative +} { + test fu-relative-4.$n {fileutil::relative, bad mix} winOnly { + catch {fileutil::relative $base $dst} msg + set msg + } "Unable to compute relation for paths of different pathtypes: $ra vs. $rb, ($base vs. $dst)" +} + +test fu-relativeurl-1.0 {fileutil::relativeUrl, wrong#args} { + catch {fileutil::relativeUrl} msg + set msg +} [tcltest::wrongNumArgs fileutil::relativeUrl {base dst} 0] + +test fu-relativeurl-1.1 {fileutil::relativeUrl, wrong#args} { + catch {fileutil::relativeUrl a} msg + set msg +} [tcltest::wrongNumArgs fileutil::relativeUrl {base dst} 1] + +test fu-relativeurl-1.2 {fileutil::relativeUrl, wrong#args} { + catch {fileutil::relativeUrl a b c} msg + set msg +} [tcltest::tooManyArgs fileutil::relativeUrl {base dst}] + +foreach {n base dst result} { + 0 /base/file.html /base/destination/xx.html destination/xx.html + 1 /base/file.html /destination/xx.html ../destination/xx.html + 2 base/file.html base/destination/xx.html destination/xx.html + 3 base/file.html destination/xx.html ../destination/xx.html + 4 /sub/base/file.html /sub/sub/destination/xx.html ../sub/destination/xx.html + 5 /sub/sub/base/file.html /sub/destination/xx.html ../../destination/xx.html + 6 sub/base/file.html sub/sub/destination/xx.html ../sub/destination/xx.html + 7 sub/sub/base/file.html sub/destination/xx.html ../../destination/xx.html + 8 /base/file.html /base/xx.html xx.html + 9 base/file.html base/xx.html xx.html + 10 /base/sub/file.html /base/sub/xx.html xx.html + 11 base/sub/file.html base/sub/xx.html xx.html + 12 /base/sub/file.html /base/xx.html ../xx.html + 13 base/sub/file.html base/xx.html ../xx.html + 14 base/sub/file.html xx.html ../../xx.html + 15 base/tcl/a.html base/common/../common/./style.css ../common/style.css + 16 base/tcl/x/a.html base/common/../common/./style.css ../../common/style.css + 17 /base/tcl/a.html /base/common/../common/./style.css ../common/style.css + 18 /base/tcl/x/a.html /base/common/../common/./style.css ../../common/style.css +} { + test fu-relativeurl-2.$n {fileutil::relativeUrl} { + fileutil::relativeUrl $base $dst + } $result +} + +foreach {n base dst ra rb} { + 0 /base/file.html base/destination/xx.html absolute relative + 1 base/file.html /destination/xx.html relative absolute +} { + test fu-relativeurl-3.$n {fileutil::relativeUrl, bad mix} unixOnly { + catch {fileutil::relativeUrl $base $dst} msg + set msg + } "Unable to compute relation for paths of different pathtypes: $ra vs. $rb, ($base vs. $dst)" +} + +foreach {n base dst ra rb} { + 0 /base/file.html base/destination/xx.html volumerelative relative + 1 base/file.html /destination/xx.html relative volumerelative +} { + test fu-relativeurl-4.$n {fileutil::relativeUrl, bad mix} winOnly { + catch {fileutil::relativeUrl $base $dst} msg + set msg + } "Unable to compute relation for paths of different pathtypes: $ra vs. $rb, ($base vs. $dst)" +} + +if {[llength [info commands ::fileutil::LexNormalize]]} { + + # Check an internal command. May not exist (i.e. an accelerator + # may not define it). + + foreach {n base dst} { + 0 a/../b b + 1 a/./b a/b + 2 a a + 3 a/b a/b + 4 ./a a + 5 ../a a + 6 /../a /a + 7 /./a /a + 8 /a/../b /b + 9 /foo/bar/../snafu/../gobble /foo/gobble + } { + test fu-lexnormalize-1.$n "fileutil::LexNormalize $base" { + fileutil::LexNormalize $base + } $dst + } +} + +# ------------------------------------------------------------------------- + +unset dir +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/pkgIndex.tcl b/tcllib/modules/fileutil/pkgIndex.tcl new file mode 100644 index 0000000..8f6c392 --- /dev/null +++ b/tcllib/modules/fileutil/pkgIndex.tcl @@ -0,0 +1,10 @@ +if {![package vsatisfies [package provide Tcl] 8.2]} {return} +package ifneeded fileutil 1.15 [list source [file join $dir fileutil.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.3]} {return} +package ifneeded fileutil::traverse 0.6 [list source [file join $dir traverse.tcl]] + +if {![package vsatisfies [package provide Tcl] 8.4]} {return} +package ifneeded fileutil::multi 0.1 [list source [file join $dir multi.tcl]] +package ifneeded fileutil::multi::op 0.5.3 [list source [file join $dir multiop.tcl]] +package ifneeded fileutil::decode 0.2 [list source [file join $dir decode.tcl]] diff --git a/tcllib/modules/fileutil/strip.test b/tcllib/modules/fileutil/strip.test new file mode 100644 index 0000000..756d304 --- /dev/null +++ b/tcllib/modules/fileutil/strip.test @@ -0,0 +1,118 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2009 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: strip.test,v 1.3 2009/10/27 19:16:34 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- + +set dir $::tcltest::temporaryDirectory + +# ------------------------------------------------------------------------- + +# stripPwd/N/Prefix ----------------------------------------------------- +# dir = $::tcltest::temporaryDirectory = current working directory + +test stripPwd-1.0 {unrelated path} { + fileutil::stripPwd {find 1} +} {find 1} + +test stripPwd-1.1 {pwd-relative path} { + fileutil::stripPwd [file join [pwd] $dir {find 1}] +} {find 1} + +test stripPwd-1.2 {pwd-relative path} { + fileutil::stripPwd [file join [pwd] $dir {find 1} {find 2}] +} [file join {find 1} {find 2}] + +test stripPwd-1.3 {pwd itself} { + fileutil::stripPwd [pwd] +} . + + +test stripPath-1.0 {unrelated path} { + fileutil::stripPath [pwd] {find 1} +} {find 1} + +test stripPath-1.1 {prefix-relative path} { + fileutil::stripPath [pwd] [file join [pwd] $dir {find 1}] +} {find 1} + +test stripPath-1.2 {prefix-relative path} { + fileutil::stripPath [pwd] [file join [pwd] $dir {find 1} {find 2}] +} [file join {find 1} {find 2}] + +test stripPath-1.3 {prefix itself} { + fileutil::stripPath [pwd] [pwd] +} . + + +test stripPath-2.0 {SF Tcllib Bug 2499641, handle mixed case properly on windows} win { + fileutil::stripPath C:/temp C:/Temp/foo +} foo + +test stripPath-2.1.0 {SF Tcllib Bug 2872536, partial paths} unix { + fileutil::stripPath /temp /tempx/foo +} /tempx/foo + +test stripPath-2.1.1 {SF Tcllib Bug 2872536, partial paths} win { + fileutil::stripPath C:/temp C:/Tempx/foo +} C:/Tempx/foo + +test stripPath-2.2 {SF Tcllib Bug 2872536, different separators} win { + fileutil::stripPath c:/temp/foo/bar c:/temp/foo\\bar +} . + + +test stripN-1.0 {remove nothing} { + fileutil::stripN {find 1} 0 +} {find 1} + +test stripN-1.1 {remove all} { + fileutil::stripN {find 1} 1 +} {} + +test stripN-1.2 {remove more than existing} { + fileutil::stripN {find 1} 2 +} {} + +test stripN-2.0 {remove nothing} { + fileutil::stripN [file join {find 1} {find 2}] 0 +} [file join {find 1} {find 2}] + +test stripN-2.1 {remove part} { + fileutil::stripN [file join {find 1} {find 2}] 1 +} {find 2} + +test stripN-2.2 {remove all} { + fileutil::stripN [file join {find 1} {find 2}] 2 +} {} + +test stripN-2.3 {remove more than existing} { + fileutil::stripN [file join {find 1} {find 2}] 3 +} {} + +# ------------------------------------------------------------------------- + +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/test-data/pdf4tcl_01.pdf b/tcllib/modules/fileutil/test-data/pdf4tcl_01.pdf new file mode 100644 index 0000000..8289779 --- /dev/null +++ b/tcllib/modules/fileutil/test-data/pdf4tcl_01.pdf @@ -0,0 +1,83 @@ +%PDF-1.4 +%åäö +5 0 obj +<< +/Length 6 0 R +>> +stream +/Courier 12 Tf +0 Tr +12 TL +BT +(Hello) Tj + +ET + +endstream +endobj + +6 0 obj +45 +endobj + +4 0 obj +<</Type /Page +/Parent 2 0 R +/Resources 3 0 R +/MediaBox [0 0 595 842] +/Contents [5 0 R] +>> +endobj + +7 0 obj +<< +/Type /Font +/Subtype /Type1 +/Encoding /WinAnsiEncoding +/Name /Courier +/BaseFont /Courier +>> +endobj +1 0 obj +<< +/Type /Catalog +/Pages 2 0 R +>> +endobj + +2 0 obj +<< +/Type /Pages +/Count 1 +/Kids [4 0 R ] +>> +endobj + +3 0 obj +<< +/ProcSet[/PDF /Text /ImageC] +/Font << +/Courier 7 0 R +>> +>> +endobj + +xref +0 8 +0000000000 65535 f +0000000347 00000 n +0000000397 00000 n +0000000456 00000 n +0000000131 00000 n +0000000014 00000 n +0000000112 00000 n +0000000237 00000 n +trailer +<< +/Size 8 +/Root 1 0 R +>> + +startxref +534 +%%EOF diff --git a/tcllib/modules/fileutil/test.test b/tcllib/modules/fileutil/test.test new file mode 100644 index 0000000..9807dbe --- /dev/null +++ b/tcllib/modules/fileutil/test.test @@ -0,0 +1,665 @@ +# -*- tcl -*- +# Tests for fileutil commands. 'test'. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 1998-2000 by Ajuba Solutions. +# Copyright (c) 2001 by ActiveState Tool Corp. +# Copyright (c) 2005-2007 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: test.test,v 1.2 2009/10/06 20:07:18 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.2 +testsNeedTcltest 1.0 + +testing { + useLocal fileutil.tcl fileutil +} + +# ------------------------------------------------------------------------- +# In 8.3+ we can use symbolic permissions, i.e. strings like u+r, or +# ugo-r when invoking 'file attributes'. This feature is however not +# available in Tcl 8.2, the lowest revision supported by the +# package. So we make do without them and use absolute permissions +# instead. + +# 644 = -rw-r--r-- +# 700 = -rwx------ +# 600 = -rw------- +# 500 = -r-x------ +# 300 = --wx------ + +# ------------------------------------------------------------------------- + +set xpath [makeFile {} x] ; removeFile x + +# ------------------------------------------------------------------------- + +proc makewritable {path enable} { + global tcl_platform + if {[string equal $tcl_platform(platform) windows]} { + set ro [expr {!$enable}] + file attributes $path -readonly $ro + } else { + set mode [expr {$enable ? "700" : "500"}] + file attributes $path -permissions 00$mode + } + return +} + +proc makereadable {path enable} { + global tcl_platform + if {[string equal $tcl_platform(platform) windows]} { + return -code error "Can't do that on Windows" + } else { + set mode [expr {$enable ? "700" : "300"}] + file attributes $path -permissions 00$mode + } + return +} + +proc makeexecutable {path enable} { + global tcl_platform + if {[string equal $tcl_platform(platform) windows]} { + return -code error "Can't do that on Windows" + } else { + set mode [expr {$enable ? "700" : "600"}] + file attributes $path -permissions 00$mode + } + return +} + +# ------------------------------------------------------------------------- + +test test-1.0.0 {test read} {unixOnly} { + set path [makeFile {} x] + makereadable $path 1 + + set x PRE + set res [list [fileutil::test $path read x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-1.0.1 {test read, no variable} {unixOnly} { + set path [makeFile {} x] + makereadable $path 1 + + set x PRE + set res [list [fileutil::test $path read] $x] + removeFile x + set res +} {1 PRE} + +test test-1.0.2 {test !read} {unixOnly notRoot} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path read x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Read access is denied"] + +test test-1.0.3 {test !read, no variable} {unixOnly notRoot} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path read] $x] + removeFile x + set res +} {0 PRE} + +test test-1.0.4 {test !read, no label} {unixOnly notRead} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path read x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Read access is denied"] + +test test-2.0.0 {test write} { + set path [makeFile {} x] + makewritable $path 1 + + set x PRE + set res [list [fileutil::test $path write x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-2.0.1 {test write, no variable} { + set path [makeFile {} x] + makewritable $path 1 + + set x PRE + set res [list [fileutil::test $path write] $x] + removeFile x + set res +} {1 PRE} + +test test-2.0.2 {test !write} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path write x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Write access is denied"] + +test test-2.0.3 {test !write, no variable} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path write] $x] + removeFile x + set res +} {0 PRE} + +test test-2.0.4 {test !write, no label} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path write x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Write access is denied"] + +test test-3.0.0 {test exists} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path exists x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-3.0.1 {test exists, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path exists] $x] + removeFile x + set res +} {1 PRE} + +test test-3.0.2 {test !exists} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path exists x TEST] $x] + set res +} [list 0 "TEST \"$xpath\": Does not exist"] + +test test-3.0.3 {test !exists, no variable} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path exists] $x] + set res +} {0 PRE} + +test test-3.0.4 {test !exists, no label} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path exists x] $x] + set res +} [list 0 "\"$xpath\": Does not exist"] + +test test-4.0.0 {test file} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path file x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-4.0.1 {test file, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path file] $x] + removeFile x + set res +} {1 PRE} + +test test-4.0.2 {test !file} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path file x TEST] $x] + removeDirectory x + set res +} [list 0 "TEST \"$xpath\": Is not a file"] + +test test-4.0.3 {test !file, no variable} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path file] $x] + removeDirectory x + set res +} {0 PRE} + +test test-4.0.4 {test !file, no label} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path file x] $x] + removeDirectory x + set res +} [list 0 "\"$xpath\": Is not a file"] + +test test-5.0.0 {test dir} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path dir x TEST] $x] + removeDirectory x + set res +} {1 PRE} + +test test-5.0.1 {test dir, no variable} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path dir] $x] + removeDirectory x + set res +} {1 PRE} + +test test-5.0.2 {test !dir} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path dir x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Is not a directory"] + +test test-5.0.3 {test !dir, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path dir] $x] + removeFile x + set res +} {0 PRE} + +test test-5.0.4 {test !dir, no label} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path dir x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Is not a directory"] + +test test-6.0.0 {test exec} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 1 + + set x PRE + set res [list [fileutil::test $path exec x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-6.0.1 {test exec, no variable} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 1 + + set x PRE + set res [list [fileutil::test $path exec] $x] + removeFile x + set res +} {1 PRE} + +test test-6.0.2 {test !exec} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path exec x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Is not executable"] + +test test-6.0.3 {test !exec, no variable} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path exec] $x] + removeFile x + set res +} {0 PRE} + +test test-6.0.4 {test !exec, no label} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path exec x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Is not executable"] + + + +test test-1.1.0 {test read} {unixOnly} { + set path [makeFile {} x] + makereadable $path 1 + + set x PRE + set res [list [fileutil::test $path r x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-1.1.1 {test read, no variable} {unixOnly} { + set path [makeFile {} x] + makereadable $path 1 + + set x PRE + set res [list [fileutil::test $path r] $x] + removeFile x + set res +} {1 PRE} + +test test-1.1.2 {test !read} {unixOnly notRoot} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path r x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Read access is denied"] + +test test-1.1.3 {test !read, no variable} {unixOnly notRoot} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path r] $x] + removeFile x + set res +} {0 PRE} + +test test-1.1.4 {test !read, no label} {unixOnly notRoot} { + set path [makeFile {} x] + makereadable $path 0 + + set x PRE + set res [list [fileutil::test $path r x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Read access is denied"] + +test test-2.1.0 {test write} { + set path [makeFile {} x] + makewritable $path 1 + + set x PRE + set res [list [fileutil::test $path w x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-2.1.1 {test write, no variable} { + set path [makeFile {} x] + makewritable $path 1 + + set x PRE + set res [list [fileutil::test $path w] $x] + removeFile x + set res +} {1 PRE} + +test test-2.1.2 {test !write} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path w x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Write access is denied"] + +test test-2.1.3 {test !write, no variable} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path w] $x] + removeFile x + set res +} {0 PRE} + +test test-2.1.4 {test !write, no label} notRoot { + set path [makeFile {} x] + makewritable $path 0 + + set x PRE + set res [list [fileutil::test $path w x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Write access is denied"] + +test test-3.1.0 {test exists} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path e x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-3.1.1 {test exists, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path e] $x] + removeFile x + set res +} {1 PRE} + +test test-3.1.2 {test !exists} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path e x TEST] $x] + set res +} [list 0 "TEST \"$xpath\": Does not exist"] + +test test-3.1.3 {test !exists, no variable} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path e] $x] + set res +} {0 PRE} + +test test-3.1.4 {test !exists, no label} { + set path [makeFile {} x] + removeFile x + + set x PRE + set res [list [fileutil::test $path e x] $x] + set res +} [list 0 "\"$xpath\": Does not exist"] + +test test-4.1.0 {test file} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path f x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-4.1.1 {test file, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path f] $x] + removeFile x + set res +} {1 PRE} + +test test-4.1.2 {test !file} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path f x TEST] $x] + removeDirectory x + set res +} [list 0 "TEST \"$xpath\": Is not a file"] + +test test-4.1.3 {test !file, no variable} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path f] $x] + removeDirectory x + set res +} {0 PRE} + +test test-4.1.4 {test !file, no label} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path f x] $x] + removeDirectory x + set res +} [list 0 "\"$xpath\": Is not a file"] + +test test-5.1.0 {test dir} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path d x TEST] $x] + removeDirectory x + set res +} {1 PRE} + +test test-5.1.1 {test dir, no variable} { + set path [makeDirectory x] + + set x PRE + set res [list [fileutil::test $path d] $x] + removeDirectory x + set res +} {1 PRE} + +test test-5.1.2 {test !dir} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path d x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Is not a directory"] + +test test-5.1.3 {test !dir, no variable} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path d] $x] + removeFile x + set res +} {0 PRE} + +test test-5.1.4 {test !dir, no label} { + set path [makeFile {} x] + + set x PRE + set res [list [fileutil::test $path d x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Is not a directory"] + +test test-6.1.0 {test exec} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 1 + + set x PRE + set res [list [fileutil::test $path x x TEST] $x] + removeFile x + set res +} {1 PRE} + +test test-6.1.1 {test exec, no variable} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 1 + + set x PRE + set res [list [fileutil::test $path x] $x] + removeFile x + set res +} {1 PRE} + +test test-6.1.2 {test !exec} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path x x TEST] $x] + removeFile x + set res +} [list 0 "TEST \"$xpath\": Is not executable"] + +test test-6.1.3 {test !exec, no variable} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path x] $x] + removeFile x + set res +} {0 PRE} + +test test-6.1.4 {test !exec, no label} {unixOnly} { + set path [makeFile {} x] + makeexecutable $path 0 + + set x PRE + set res [list [fileutil::test $path x x] $x] + removeFile x + set res +} [list 0 "\"$xpath\": Is not executable"] + +# ------------------------------------------------------------------------- + +rename makewritable {} +rename makereadable {} +rename makeexecutable {} +catch {unset xpath} +catch {unset path} +catch {unset res} +catch {unset x} + +testsuiteCleanup +return diff --git a/tcllib/modules/fileutil/traverse.man b/tcllib/modules/fileutil/traverse.man new file mode 100644 index 0000000..971b635 --- /dev/null +++ b/tcllib/modules/fileutil/traverse.man @@ -0,0 +1,165 @@ +[comment {-*- text -*- doctools manpage}] +[vset VERSION 0.6] +[manpage_begin fileutil_traverse n [vset VERSION]] +[keywords {directory traversal}] +[keywords traversal] +[moddesc {file utilities}] +[titledesc {Iterative directory traversal}] +[category {Programming tools}] +[require Tcl 8.3] +[require fileutil::traverse [opt [vset VERSION]]] +[require fileutil] +[require control] +[description] +[para] + +This package provides objects for the programmable traversal of +directory hierarchies. + +The main command exported by the package is: + +[list_begin definitions] + +[call [cmd ::fileutil::traverse] [opt [arg objectName]] \ + [arg path] [opt "[arg option] [arg value]..."]] + +The command creates a new traversal object with an associated global +Tcl command whose name is [arg objectName]. This command may be used +to invoke various operations on the traverser. + +If the string [const %AUTO%] is used as the [arg objectName] then a +unique name will be generated by the package itself. + +[para] + +Regarding the recognized options see section [sectref OPTIONS]. Note +that all these options can be set only during the creation of the +traversal object. Changing them later is not possible and causes +errors to be thrown if attempted. + +[para] + +The object command has the following general form: + +[list_begin definitions] +[call [cmd \$traverser] [method command] [opt [arg "arg arg ..."]]] + +[arg Command] and its [arg arg]uments determine the exact behavior of +the object. + +[list_end] +[list_end] + +The following commands are possible for traversal objects: + +[list_begin definitions] + +[call [cmd \$traverser] [method files]] + +This method is the most highlevel one provided by traversal +objects. When invoked it returns a list containing the names of all +files and directories matching the current configuration of the +traverser. + +[call [cmd \$traverser] [method foreach] [arg filevar] [arg script]] + +The highlevel [method files] method (see above) is based on this +mid-level method. When invoked it finds all files and directories +matching per the current configuration and executes the [arg script] +for each path. The current path under consideration is stored in the +variable named by [arg filevar]. Both variable and script live / are +executed in the context of the caller of the method. In the method +[method files] the script simply saves the found paths into the list +to return. + +[call [cmd \$traverser] [method next] [arg filevar]] + +This is the lowest possible interface to the traverser, the core all +higher methods are built on. When invoked it returns a boolean value +indicating whether it found a path matching the current configuration +([const True]), or not ([const False]). If a path was found it is +stored into the variable named by [arg filevar], in the context of the +caller. + +[para] The [method foreach] method simply calls this method in a loop +until it returned [const False]. This method is exposed so that we are +also able to incrementally traverse a directory hierarchy in an +event-based manner. + +[para] Note that the traverser does follow symbolic links, except when +doing so would cause it to enter a link-cycle. In other words, the +command takes care to [emph not] lose itself in infinite loops upon +encountering circular link structures. Note that even links which are +not followed will still appear in the result. + +[list_end] + +[section OPTIONS] + +[list_begin options] +[opt_def -prefilter command_prefix] + +This callback is executed for directories. Its result determines if +the traverser recurses into the directory or not. The default is to +always recurse into all directories. The callback is invoked with a +single argument, the [emph absolute] path of the directory, and has to +return a boolean value, [const True] when the directory passes the +filter, and [const False] if not. + +[opt_def -filter command_prefix] + +This callback is executed for all paths. Its result determines if the +current path is a valid result, and returned by [method next]. The +default is to accept all paths as valid. The callback is invoked with +a single argument, the [emph absolute] path to check, and has to +return a boolean value, [const True] when the path passes the filter, +and [const False] if not. + +[opt_def -errorcmd command_prefix] + +This callback is executed for all paths the traverser has trouble +with. Like being unable to change into them, get their status, +etc. The default is to ignore any such problems. The callback is +invoked with a two arguments, the [emph absolute] path for which the +error occured, and the error message. Errors thrown by the filter +callbacks are handled through this callback too. Errors thrown by the +error callback itself are not caught and ignored, but allowed to pass +to the caller, i.e. however invoked the [method next]. Any other +results from the callback are ignored. + +[list_end] + + +[section {Warnings and Incompatibilities}] + +[list_begin definitions] + +[def [const 0.4.4]] +In this version the traverser's broken system for handling symlinks +was replaced with one working correctly and properly enumerating all +the legal non-cyclic paths under a base directory. + +[para] While correct this means that certain pathological directory +hierarchies with cross-linked sym-links will now take about O(n**2) +time to enumerate whereas the original broken code managed O(n) due to +its brokenness. + +[para] A concrete example and extreme case is the [file /sys] +hierarchy under Linux where some hundred devices exist under both +[file /sys/devices] and [file /sys/class] with the two sub-hierarchies +linking to the other, generating millions of legal paths to enumerate. +The structure, reduced to three devices, roughly looks like + +[include include/cross-index.inc] + +[para] When having to handle such a pathological hierarchy it is +recommended to use the [option -prefilter] option to prevent the +traverser from following symbolic links, like so: + +[include include/cross-index-trav.inc] + +[list_end] + +[vset CATEGORY fileutil] +[include ../doctools2base/include/feedback.inc] +[manpage_end] diff --git a/tcllib/modules/fileutil/traverse.tcl b/tcllib/modules/fileutil/traverse.tcl new file mode 100644 index 0000000..c8d981f --- /dev/null +++ b/tcllib/modules/fileutil/traverse.tcl @@ -0,0 +1,506 @@ +# traverse.tcl -- +# +# Directory traversal. +# +# Copyright (c) 2006-2015 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# +# See the file "license.terms" for information on usage and redistribution +# of this file, and for a DISCLAIMER OF ALL WARRANTIES. +# +# RCS: @(#) $Id: traverse.tcl,v 1.9 2012/08/29 20:42:19 andreas_kupries Exp $ + +package require Tcl 8.3 + +# OO core +if {[package vsatisfies [package present Tcl] 8.5]} { + # Use new Tcl 8.5a6+ features to specify the allowed packages. + # We can use anything above 1.3. This means v2 as well. + package require snit 1.3- +} else { + # For Tcl 8.{3,4} only snit1 of a suitable patchlevel is possible. + package require snit 1.3 +} +package require control ; # Helpers for control structures +package require fileutil ; # -> fullnormalize + +snit::type ::fileutil::traverse { + + # Incremental directory traversal. + + # API + # create %AUTO% basedirectory options... -> object + # next filevar -> boolean + # foreach filevar script + # files -> list (path ...) + + # Options + # -prefilter command-prefix + # -filter command-prefix + # -errorcmd command-prefix + + # Use cases + # + # (a) Basic incremental + # - Create and configure a traversal object. + # - Execute 'next' to retrieve one path at a time, + # until the command returns False, signaling that + # the iterator has exhausted the supply of paths. + # (The path is stored in the named variable). + # + # The execution of 'next' can be done in a loop, or via event + # processing. + + # (b) Basic loop + # - Create and configure a traversal object. + # - Run a script for each path, using 'foreach'. + # This is a convenient standard wrapper around 'next'. + # + # The loop properly handles all possible Tcl result codes. + + # (c) Non-incremental, non-looping. + # - Create and configure a traversal object. + # - Retrieve a list of all paths via 'files'. + + # The -prefilter callback is executed for directories. Its result + # determines if the traverser recurses into the directory or not. + # The default is to always recurse into all directories. The call- + # back is invoked with a single argument, the path of the + # directory. + # + # The -filter callback is executed for all paths. Its result + # determines if the current path is a valid result, and returned + # by 'next'. The default is to accept all paths as valid. The + # callback is invoked with a single argument, the path to check. + + # The -errorcmd callback is executed for all paths the traverser + # has trouble with. Like being unable to cd into them, get their + # status, etc. The default is to ignore any such problems. The + # callback is invoked with a two arguments, the path for which the + # error occured, and the error message. Errors thrown by the + # filter callbacks are handled through this callback too. Errors + # thrown by the error callback itself are not caught and ignored, + # but allowed to pass to the caller, usually of 'next'. + + # Note: Low-level functionality, version and platform dependent is + # implemented in procedures, and conditioally defined for optimal + # use of features, etc. ... + + # Note: Traversal is done in depth-first pre-order. + + # Note: The options are handled only during + # construction. Afterward they are read-only and attempts to + # modify them will cause the system to throw errors. + + # ### ### ### ######### ######### ######### + ## Implementation + + option -filter -default {} -readonly 1 + option -prefilter -default {} -readonly 1 + option -errorcmd -default {} -readonly 1 + + constructor {basedir args} { + set _base $basedir + $self configurelist $args + return + } + + method files {} { + set files {} + $self foreach f {lappend files $f} + return $files + } + + method foreach {fvar body} { + upvar 1 $fvar currentfile + + # (Re-)initialize the traversal state on every call. + $self Init + + while {[$self next currentfile]} { + set code [catch {uplevel 1 $body} result] + + # decide what to do upon the return code: + # + # 0 - the body executed successfully + # 1 - the body raised an error + # 2 - the body invoked [return] + # 3 - the body invoked [break] + # 4 - the body invoked [continue] + # everything else - return and pass on the results + # + switch -exact -- $code { + 0 {} + 1 { + return -errorinfo [::control::ErrorInfoAsCaller uplevel foreach] \ + -errorcode $::errorCode -code error $result + } + 3 { + # FRINK: nocheck + return + } + 4 {} + default { + return -code $code $result + } + } + } + return + } + + method next {fvar} { + upvar 1 $fvar currentfile + + # Initialize on first call. + if {!$_init} { + $self Init + } + + # We (still) have valid paths in the result stack, return the + # next one. + + if {[llength $_results]} { + set top [lindex $_results end] + set _results [lreplace $_results end end] + set currentfile $top + return 1 + } + + # Take the next directory waiting in the processing stack and + # fill the result stack with all valid files and sub- + # directories contained in it. Extend the processing queue + # with all sub-directories not yet seen already (!circular + # symlinks) and accepted by the prefilter. We stop iterating + # when we either have no directories to process anymore, or + # the result stack contains at least one path we can return. + + while {[llength $_pending]} { + set top [lindex $_pending end] + set _pending [lreplace $_pending end end] + + # Directory accessible? Skip if not. + if {![ACCESS $top]} { + Error $top "Inacessible directory" + continue + } + + # Expand the result stack with all files in the directory, + # modulo filtering. + + foreach f [GLOBF $top] { + if {![Valid $f]} continue + lappend _results $f + } + + # Expand the result stack with all sub-directories in the + # directory, modulo filtering. Further expand the + # processing stack with the same directories, if not seen + # yet and modulo pre-filtering. + + foreach f [GLOBD $top] { + if { + [string equal [file tail $f] "."] || + [string equal [file tail $f] ".."] + } continue + + if {[Valid $f]} { + lappend _results $f + } + + Enter $top $f + if {[Cycle $f]} continue + + if {[Recurse $f]} { + lappend _pending $f + } + } + + # Stop expanding if we have paths to return. + + if {[llength $_results]} { + set top [lindex $_results end] + set _results [lreplace $_results end end] + set currentfile $top + return 1 + } + } + + # Allow re-initialization with next call. + + set _init 0 + return 0 + } + + # ### ### ### ######### ######### ######### + ## Traversal state + + # * Initialization flag. Checked in 'next', reset by next when no + # more files are available. Set in 'Init'. + # * Base directory (or file) to start the traversal from. + # * Stack of prefiltered unknown directories waiting for + # processing, i.e. expansion (TOP at end). + # * Stack of valid paths waiting to be returned as results. + # * Set of directories already visited (normalized paths), for + # detection of circular symbolic links. + + variable _init 0 ; # Initialization flag. + variable _base {} ; # Base directory. + variable _pending {} ; # Processing stack. + variable _results {} ; # Result stack. + + # sym link handling (to break cycles, while allowing the following of non-cycle links). + # Notes + # - path parent tracking is lexical. + # - path identity tracking is based on the normalized path, i.e. the path with all + # symlinks resolved. + # Maps + # - path -> parent (easier to follow the list than doing dirname's) + # - path -> normalized (cache to avoid redundant calls of fullnormalize) + # cycle <=> A parent's normalized form (NF) is identical to the current path's NF + + variable _parent -array {} + variable _norm -array {} + + # ### ### ### ######### ######### ######### + ## Internal helpers. + + proc Enter {parent path} { + #puts ___E|$path + upvar 1 _parent _parent _norm _norm + set _parent($path) $parent + set _norm($path) [fileutil::fullnormalize $path] + } + + proc Cycle {path} { + upvar 1 _parent _parent _norm _norm + set nform $_norm($path) + set paren $_parent($path) + while {$paren ne {}} { + if {$_norm($paren) eq $nform} { return yes } + set paren $_parent($paren) + } + return no + } + + method Init {} { + array unset _parent * + array unset _norm * + + # Path ok as result? + if {[Valid $_base]} { + lappend _results $_base + } + + # Expansion allowed by prefilter? + if {[file isdirectory $_base] && [Recurse $_base]} { + Enter {} $_base + lappend _pending $_base + } + + # System is set up now. + set _init 1 + return + } + + proc Valid {path} { + #puts ___V|$path + upvar 1 options options + if {![llength $options(-filter)]} {return 1} + set path [file normalize $path] + set code [catch {uplevel \#0 [linsert $options(-filter) end $path]} valid] + if {!$code} {return $valid} + Error $path $valid + return 0 + } + + proc Recurse {path} { + #puts ___X|$path + upvar 1 options options _norm _norm + if {![llength $options(-prefilter)]} {return 1} + set path [file normalize $path] + set code [catch {uplevel \#0 [linsert $options(-prefilter) end $path]} valid] + if {!$code} {return $valid} + Error $path $valid + return 0 + } + + proc Error {path msg} { + upvar 1 options options + if {![llength $options(-errorcmd)]} return + set path [file normalize $path] + uplevel \#0 [linsert $options(-errorcmd) end $path $msg] + return + } + + ## + # ### ### ### ######### ######### ######### +} + +# ### ### ### ######### ######### ######### +## + +# The next three helper commands for the traverser depend strongly on +# the version of Tcl, and partially on the platform. + +# 1. In Tcl 8.3 using -types f will return only true files, but not +# links to files. This changed in 8.4+ where links to files are +# returned as well. So for 8.3 we have to handle the links +# separately (-types l) and also filter on our own. +# Note that Windows file links are hard links which are reported by +# -types f, but not -types l, so we can optimize that for the two +# platforms. +# +# 2. In Tcl 8.3 we also have a crashing bug in glob (SIGABRT, "stat on +# a known file") when trying to perform 'glob -types {hidden f}' on +# a directory without e'x'ecute permissions. We code around by +# testing if we can cd into the directory (stat might return enough +# information too (mode), but possibly also not portable). +# +# For Tcl 8.2 and 8.4+ glob simply delivers an empty result +# (-nocomplain), without crashing. For them this command is defined +# so that the bytecode compiler removes it from the bytecode. +# +# This bug made the ACCESS helper necessary. +# We code around the problem by testing if we can cd into the +# directory (stat might return enough information too (mode), but +# possibly also not portable). + +if {[package vsatisfies [package present Tcl] 8.5]} { + # Tcl 8.5+. + # We have to check readability of "current" on our own, glob + # changed to error out instead of returning nothing. + + proc ::fileutil::traverse::ACCESS {args} {return 1} + + proc ::fileutil::traverse::GLOBF {current} { + if {![file readable $current] || + [BadLink $current]} { + return {} + } + + set res [lsort -unique [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]]] + + # Look for broken links (They are reported as neither file nor directory). + foreach l [lsort -unique [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]]] { + if {[file isfile $l]} continue + if {[file isdirectory $l]} continue + lappend res $l + } + return [lsort -unique $res] + } + + proc ::fileutil::traverse::GLOBD {current} { + if {![file readable $current] || + [BadLink $current]} { + return {} + } + + lsort -unique [concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *]] + } + + proc ::fileutil::traverse::BadLink {current} { + if {[file type $current] ne "link"} { return no } + + set dst [file join [file dirname $current] [file readlink $current]] + + if {![file exists $dst] || + ![file readable $dst]} { + return yes + } + + return no + } + +} elseif {[package vsatisfies [package present Tcl] 8.4]} { + # Tcl 8.4+. + # (Ad 1) We have -directory, and -types, + # (Ad 2) Links are returned for -types f/d if they refer to files/dirs. + # (Ad 3) No bug to code around + + proc ::fileutil::traverse::ACCESS {args} {return 1} + + proc ::fileutil::traverse::GLOBF {current} { + set res [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]] + + # Look for broken links (They are reported as neither file nor directory). + foreach l [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *] ] { + if {[file isfile $l]} continue + if {[file isdirectory $l]} continue + lappend res $l + } + return $res + } + + proc ::fileutil::traverse::GLOBD {current} { + concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *] + } + +} else { + # 8.3. + # (Ad 1) We have -directory, and -types, + # (Ad 2) Links are NOT returned for -types f/d, collect separately. + # No symbolic file links on Windows. + # (Ad 3) Bug to code around. + + proc ::fileutil::traverse::ACCESS {current} { + if {[catch { + set h [pwd] ; cd $current ; cd $h + }]} {return 0} + return 1 + } + + if {[string equal $::tcl_platform(platform) windows]} { + proc ::fileutil::traverse::GLOBF {current} { + concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]] + } + } else { + proc ::fileutil::traverse::GLOBF {current} { + set l [concat \ + [glob -nocomplain -directory $current -types f -- *] \ + [glob -nocomplain -directory $current -types {hidden f} -- *]] + + foreach x [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]] { + if {[file isdirectory $x]} continue + # We have now accepted files, links to files, and broken links. + lappend l $x + } + + return $l + } + } + + proc ::fileutil::traverse::GLOBD {current} { + set l [concat \ + [glob -nocomplain -directory $current -types d -- *] \ + [glob -nocomplain -directory $current -types {hidden d} -- *]] + + foreach x [concat \ + [glob -nocomplain -directory $current -types l -- *] \ + [glob -nocomplain -directory $current -types {hidden l} -- *]] { + if {![file isdirectory $x]} continue + lappend l $x + } + + return $l + } +} + +# ### ### ### ######### ######### ######### +## Ready + +package provide fileutil::traverse 0.6 diff --git a/tcllib/modules/fileutil/traverse.test b/tcllib/modules/fileutil/traverse.test new file mode 100644 index 0000000..c7e5694 --- /dev/null +++ b/tcllib/modules/fileutil/traverse.test @@ -0,0 +1,499 @@ +# -*- tcl -*- +# Tests for the find function. +# +# Sourcing this file into Tcl runs the tests and generates output for errors. +# No output means no errors were found. +# +# Copyright (c) 2007-2015 by Andreas Kupries <andreas_kupries@users.sourceforge.net> +# All rights reserved. +# +# RCS: @(#) $Id: traverse.test,v 1.3 2012/08/29 20:42:19 andreas_kupries Exp $ + +# ------------------------------------------------------------------------- + +source [file join \ + [file dirname [file dirname [file join [pwd] [info script]]]] \ + devtools testutilities.tcl] + +testsNeedTcl 8.3 +testsNeedTcltest 2.1 + +support { + use control/control.tcl control + use snit/snit.tcl snit + + useLocalFile find.setup +} +testing { + useLocal traverse.tcl fileutil::traverse +} + +# ------------------------------------------------------------------------- +# Filters commands to record which callbacks were run. + +proc rec {x} { + lappend ::rec $x + return 1 +} + +proc recx {args} { + lappend ::rec $args + return 1 +} + +# ------------------------------------------------------------------------- + +test traverse-1.0.0 {Traverse result, circular links, unix} -setup { + f_setupcircle + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints unix -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.0.1 {Traverse result, circular links, windows, 8.4+} -setup { + f_setupcircle + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints {win tcl8.4plus} -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}]] + +test traverse-1.0.2 {Traverse result, unix} -setup { + f_setup + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints unix -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.0.3 {Traverse result, windows} -setup { + f_setup + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints win -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}]] + +# Find has to skip '{file 3}', in the sense that the path should be in +# the output, but it cannot be expanded further, being a broken +# link. Two tests, one for all versions of Tcl (8.2+), but only unix, +# and one for windows, restricted to Tcl 8.4+. + +test traverse-1.0.4 {Traverse result, broken links, unix} -setup { + f_setupbroken + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints unix -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.0.5 {Traverse result, broken links, windows, 8.4+} -setup { + f_setupbroken + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints {win tcl8.4plus} -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}]] + + +test traverse-1.0.6 {Traverse result, circular links to base, unix} -setup { + f_setupcircle2 + set t [fileutil::traverse %AUTO% [tempPath {find 1}]] +} -constraints unix -body { + lsort [$t files] +} -cleanup { + $t destroy + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +# ------------------------------------------------------------------------- + +test traverse-1.1.0 {Traverse filter execution, circular links, unix} -setup { + f_setupcircle + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -filter ::rec] +} -constraints unix -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.1.1 {Traverse filter execution, circular links, windows, 8.4+} -setup { + f_setupcircle + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -filter ::rec] +} -constraints {win tcl8.4plus} -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}]] + +test traverse-1.1.2 {Traverse filter execution, unix} -setup { + f_setup + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -filter ::rec] +} -constraints unix -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.1.3 {Traverse filter execution, windows} -setup { + f_setup + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -filter ::rec] +} -constraints win -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}]] + +# ------------------------------------------------------------------------- + +test traverse-1.2.0 {Traverse prefilter execution, unix} -setup { + f_setupcircle + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -prefilter ::rec] +} -constraints unix -body { + $t files + lsort $rec + # Note: The link 'file 3' is _not_ run through the pre-filter, + # because it is filtered out as already seen before it comes to + # the pre-filter stage. +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/find 2}]] + +test traverse-1.2.1 {Traverse prefilter execution, windows, 8.4+} -setup { + f_setupcircle + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -prefilter ::rec] +} -constraints {win tcl8.4plus} -body { + $t files + lsort $rec + # Note: The link 'file 3' is _not_ run through the pre-filter, + # because it is filtered out as already seen before it comes to + # the pre-filter stage. +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/find 2}]] + +test traverse-1.2.2 {Traverse prefilter execution, all platforms} -setup { + f_setup + set rec {} + set t [fileutil::traverse %AUTO% [tempPath {find 1}] -prefilter ::rec] +} -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/find 2}]] + +# ------------------------------------------------------------------------- + +test traverse-1.3.0 {Traverse error execution, unix, 8.4+} -setup { + f_setupnostat + set rec {} + set t [fileutil::traverse %AUTO% [tempPath find3] -errorcmd ::recx] +} -constraints {unix tcl8.4plus} -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanupnostat +} -result {} + +test traverse-1.3.1 {Traverse error execution, unix, 8.3} -setup { + f_setupnostat + set rec {} + set t [fileutil::traverse %AUTO% [tempPath find3] -errorcmd ::recx] +} -constraints {unix tcl8.3only} -body { + $t files + lsort $rec +} -cleanup { + $t destroy + unset rec + f_cleanupnostat +} -result [list [list [tempPath find3/find4] {Inacessible directory}]] + +# traverse 1.3.x - error callback, all platforms - Not possible. We have +# no win32 setup code for non-readable/non-accessible directories. + +# ------------------------------------------------------------------------- + +test traverse-1.4.0 {Traverse result, circular links, unix} -setup { + f_setupcircle3 + set t [fileutil::traverse %AUTO% [tempPath z]] +} -constraints unix -body { + join [lsort [$t files]] \n +} -cleanup { + $t destroy + f_cleanup3 +} -result [join [pathmap \ + z z/a z/a/c z/a/c/g z/a/c/h z/a/c/h/e z/a/c/h/f \ + z/a/c/i z/a/d z/b z/b/e z/b/e/g z/b/e/g/c z/b/e/g/d \ + z/b/e/h z/b/e/i z/b/f] \n] + +test traverse-1.4.1 {Traverse result, circular links, windows, 8.4+} -setup { + f_setupcircle3 + set t [fileutil::traverse %AUTO% [tempPath z]] +} -constraints {win tcl8.4plus} -body { + join [lsort [$t files]] \n +} -cleanup { + $t destroy + f_cleanup3 +} -result [join [pathmap \ + z z/a z/a/c z/a/c/g z/a/c/h z/a/c/h/e z/a/c/h/f \ + z/a/c/i z/a/d z/b z/b/e z/b/e/g z/b/e/g/c z/b/e/g/d \ + z/b/e/h z/b/e/i z/b/f] \n] + +# ------------------------------------------------------------------------- + +test traverse-1.5 {Traverse, relative base path, callback API} -setup { + f_setupcircle + set rec {} + + set base [tempPath {find 1}] + set bdir [file dirname $base] + set base [file tail $base] + set here [pwd] + cd $bdir + set t [fileutil::traverse %AUTO% $base -filter ::rec] +} -constraints unix -body { + $t files + lsort $rec +} -cleanup { + $t destroy + cd $here + unset rec bdir base here + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/file [1]}] \ + [tempPath {find 1/find 2}] \ + [tempPath {find 1/find 2/file 3}] \ + [tempPath {find 1/find 2/file* 2}]] + +test traverse-1.6 {Traverse, relative base path, callback API} -setup { + f_setupcircle + set rec {} + + set base [tempPath {find 1}] + set bdir [file dirname $base] + set base [file tail $base] + set here [pwd] + cd $bdir + set t [fileutil::traverse %AUTO% $base -prefilter ::rec] +} -constraints unix -body { + $t files + lsort $rec +} -cleanup { + $t destroy + cd $here + unset rec bdir base here + f_cleanup +} -result [list \ + [tempPath {find 1}] \ + [tempPath {find 1/find 2}]] + +# TODO: checking -error callback with ingoing relative basepath. + +# ------------------------------------------------------------------------- + +test traverse-2.0 {Traverse pathological circularity, unix} -setup { + f_setup_crossindex + set t [fileutil::traverse %AUTO% [tempPath s]] +} -constraints unix -body { + join [lsort -dict [$t files]] \n +} -cleanup { + $t destroy + f_cleanup_crossindex +} -result [join [pathmap \ + s \ + s/c \ + s/c/t \ + s/c/t/t0 \ + s/c/t/t0/b \ + s/c/t/t0/s \ + s/c/t/t1 \ + s/c/t/t1/b \ + s/c/t/t1/s \ + s/c/t/t2 \ + s/c/t/t2/b \ + s/c/t/t2/s \ + s/d \ + s/d/t0 \ + s/d/t0/b \ + s/d/t0/s \ + s/d/t0/s/t0 \ + s/d/t0/s/t1 \ + s/d/t0/s/t1/b \ + s/d/t0/s/t1/s \ + s/d/t0/s/t2 \ + s/d/t0/s/t2/b \ + s/d/t0/s/t2/s \ + s/d/t1 \ + s/d/t1/b \ + s/d/t1/s \ + s/d/t1/s/t0 \ + s/d/t1/s/t0/b \ + s/d/t1/s/t0/s \ + s/d/t1/s/t1 \ + s/d/t1/s/t2 \ + s/d/t1/s/t2/b \ + s/d/t1/s/t2/s \ + s/d/t2 \ + s/d/t2/b \ + s/d/t2/s \ + s/d/t2/s/t0 \ + s/d/t2/s/t0/b \ + s/d/t2/s/t0/s \ + s/d/t2/s/t1 \ + s/d/t2/s/t1/b \ + s/d/t2/s/t1/s \ + s/d/t2/s/t2 \ + ] \n] + +test traverse-2.1 {Traverse pathological circularity, windows, 8.4+} -setup { + f_setup_crossindex + set t [fileutil::traverse %AUTO% [tempPath s]] +} -constraints {win tcl8.4plus} -body { + join [lsort -dict [$t files]] \n +} -cleanup { + $t destroy + f_cleanup_crossindex +} -result [join [pathmap \ + s \ + s/c \ + s/c/t \ + s/c/t/t0 \ + s/c/t/t0/b \ + s/c/t/t0/s \ + s/c/t/t1 \ + s/c/t/t1/b \ + s/c/t/t1/s \ + s/c/t/t2 \ + s/c/t/t2/b \ + s/c/t/t2/s \ + s/d \ + s/d/t0 \ + s/d/t0/b \ + s/d/t0/s \ + s/d/t0/s/t0 \ + s/d/t0/s/t1 \ + s/d/t0/s/t1/b \ + s/d/t0/s/t1/s \ + s/d/t0/s/t2 \ + s/d/t0/s/t2/b \ + s/d/t0/s/t2/s \ + s/d/t1 \ + s/d/t1/b \ + s/d/t1/s \ + s/d/t1/s/t0 \ + s/d/t1/s/t0/b \ + s/d/t1/s/t0/s \ + s/d/t1/s/t1 \ + s/d/t1/s/t2 \ + s/d/t1/s/t2/b \ + s/d/t1/s/t2/s \ + s/d/t2 \ + s/d/t2/b \ + s/d/t2/s \ + s/d/t2/s/t0 \ + s/d/t2/s/t0/b \ + s/d/t2/s/t0/s \ + s/d/t2/s/t1 \ + s/d/t2/s/t1/b \ + s/d/t2/s/t1/s \ + s/d/t2/s/t2 \ + ] \n] + +# ------------------------------------------------------------------------- + +f_cleanall +testsuiteCleanup +return |