summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/fileutil
diff options
context:
space:
mode:
authorWilliam Joye <wjoye@cfa.harvard.edu>2016-10-27 19:39:39 (GMT)
committerWilliam Joye <wjoye@cfa.harvard.edu>2016-10-27 19:39:39 (GMT)
commitea28451286d3ea4a772fa174483f9a7a66bb1ab3 (patch)
tree6ee9d8a7848333a7ceeee3b13d492e40225f8b86 /tcllib/modules/fileutil
parentb5ca09bae0d6a1edce939eea03594dd56383f2c8 (diff)
parent7c621da28f07e449ad90c387344f07a453927569 (diff)
downloadblt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.zip
blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.gz
blt-ea28451286d3ea4a772fa174483f9a7a66bb1ab3.tar.bz2
Merge commit '7c621da28f07e449ad90c387344f07a453927569' as 'tcllib'
Diffstat (limited to 'tcllib/modules/fileutil')
-rw-r--r--tcllib/modules/fileutil/ChangeLog1006
-rw-r--r--tcllib/modules/fileutil/cross-index-trav.inc16
-rw-r--r--tcllib/modules/fileutil/cross-index.inc12
-rw-r--r--tcllib/modules/fileutil/decode.tcl191
-rw-r--r--tcllib/modules/fileutil/filetype.test193
-rw-r--r--tcllib/modules/fileutil/fileutil.man522
-rw-r--r--tcllib/modules/fileutil/fileutil.tcl2295
-rw-r--r--tcllib/modules/fileutil/fileutil.test499
-rw-r--r--tcllib/modules/fileutil/find.setup432
-rw-r--r--tcllib/modules/fileutil/find.test367
-rw-r--r--tcllib/modules/fileutil/include/cross-index-trav.inc16
-rw-r--r--tcllib/modules/fileutil/include/cross-index.inc12
-rw-r--r--tcllib/modules/fileutil/inplace.test1129
-rw-r--r--tcllib/modules/fileutil/multi.man56
-rw-r--r--tcllib/modules/fileutil/multi.tcl28
-rw-r--r--tcllib/modules/fileutil/multi.test310
-rw-r--r--tcllib/modules/fileutil/multiop.man402
-rw-r--r--tcllib/modules/fileutil/multiop.setup49
-rw-r--r--tcllib/modules/fileutil/multiop.tcl645
-rw-r--r--tcllib/modules/fileutil/multiop.test370
-rw-r--r--tcllib/modules/fileutil/pathops.test515
-rw-r--r--tcllib/modules/fileutil/pkgIndex.tcl10
-rw-r--r--tcllib/modules/fileutil/strip.test118
-rw-r--r--tcllib/modules/fileutil/test-data/pdf4tcl_01.pdf83
-rw-r--r--tcllib/modules/fileutil/test.test665
-rw-r--r--tcllib/modules/fileutil/traverse.man165
-rw-r--r--tcllib/modules/fileutil/traverse.tcl506
-rw-r--r--tcllib/modules/fileutil/traverse.test499
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