File operations

XSharper has built-in support for copy/move/delete/dir commands, which are useful for its batch file replacement functions. Support for .ZIP archives added in a similar fashion via #zipLib library.

The idea was to make group operations easy to use for simple cases, yet providing a capability to independently handle every individual file, confirm file overwrites and so on.

This is of the more complex and messier part of XSharper so a bit more explanation is due.

Algorithm

Implementation of the above functionality has a lot in common (and actually a common base class, ActionWithFilters) in its implementation.

There is a scanner of a directory tree, which filters files and directories matching a filter, and for every matching file contents of the block is executed. Hidden/System files are ignored unless hidden="1" is set.

<action filter="..." directoryFilter="..." hidden="0/1">
	user code executed FOR EVERY FILE 
	This is run BEFORE the copy/move/delete/etc. operation
	<try>
		This is also user code, but its exceptions will be caught
		This code may set skip variable to 1, if the operation should not be performed.
		...
		This is after the last user statement in try.
		If skip=0, here the actual Copy/Move/Delete etc will be executed with the provided file 
	</try>
	<catch>
		Executed if the operation fails for a particular file.

		Note that this catch block may be executed if an error occured
		in preparation for the action, even before user code is executed.
	</catch>
	<finally>
			Executed after the operation, whether it fails or succeedes
	</finally>
</action>

As this is technically a loop, action can break out of it.

What is special about the try/catch/finally block here is that <try> may be skipped, but <catch> will still catch copying exceptions.

A FileSystemInfo of the source file is passed to the block as "" and "from" variable (a prefix may may be added to variable via name attribute), which may be accessed as ${} or just $ inside XSharper expressions. If there is a destination location, it is set in "to" variable.

User-provided block decides whether to perform the command-specific operation (copy / move / delete / add to archive / extract) on the file by setting "skip" variable to true or false. If skip is false after completing the try block, the operation is executed.

Filter notation

There are two filters used for scanning directory tree, where both are optional. One filter is applied on found files, the other on found directories.

filter applies to found filenames without path. For example, for C:\Data\xxx.txt only xxx.txt will be evaluated against filter
directoryFilter applies to found directory names with full path. For example, for C:\Data\MyDir the whole string will be evaluated against filter.

Two different syntaxes may be used in the filter value:

wildcard A semicolon-separated list of masks. * = any number of any characters, ?=any single character. Mask may be prefixed with - to exclude, or optional + to include.

For example *.x??;-*.xls means all files with 3 letter extension that starts with x, except xls

pattern Normal regular expression
auto If filter starts with ^ it is considered to be pattern, otherwise wildcard.

List directory example

While I'm thinking of a better way to explain the above logic, here is an example that lists all *.XSH files in the current directory and its subdirectories, skipping ".svn" subdirectories. For every file its length is displayed:

<dir from="." filter="*.xsh" directoryFilter="-*\.svn" recursive="true">
	<print> ${=$.FullName}, Size=${=$.Length}</print>
</dir>

The following lists all files AND directories

<dir from="." sort="n" recursive="1" options="directories files">
	<print>${=$.fullname}</print>
	<noMatch>
		<print>Nothing found</print>
	</noMatch>
</dir>

sorted by name (sort="N"). Sorting order may be changed as in CMD.EXE, by specifying one or more of the letters:

N By name (alphabetic)
S By size (smallest first)
E By extension (alphabetic)
D By date/time (oldest first)
G Group directories first
A Access time
C Creation time
W Modification time (same as D)
- Prefix to reverse order

Please note that sort only applies to one directory, not to the whole tree, when listing directories recursively. This is consistent with dir behaviour in CMD.EXE

Copy or move files

Copy all files from current directory to r:\backup (as you see both try and catch may be skipped, to execute a piece of action after file copying completed)

<copy from="." to="r:\backup" recursive="true" hidden="true">
	<print>Copying ${from}=>${to}</print>
	<finally>
		<print>Done copying ${from}</print>
	</finally>
</copy>

Copy all files from current directory to r:\backup, ignoring all files with length>500

<copy from="." to="r:\backup" recursive="true" hidden="true">
	<set skip="${=$.length>500}" />
	<finally>
		<eval>
			$skip?null:c.Print($)
		</eval>
	</finally>
</copy>

Also support for overwriting existing file is available. In overwrite attribute it can be chosen whether to overwrite existing files always, only if newer, never, or ask user.

The piece below demonstrates copying procedure, confirming overwrite of the existing files:

<copy from="." to="r:\backup" recursive="true" hidden="true" overwrite="confirm">
    <!-- Skip will be set to true if destination file already exists -->
	<if condition="${skip}">
		<print newline='false'>File '${to}' already exists. Overwrite (Y/N)? </print>
		<while>
			<set key="${=char.ToLower(Console.Read())}" />
			<if condition="${=$key=='y'}">
				<set skip="false" />
				<break />
			</if>
			<if condition="${=$key=='n'}">
				<break />				
			</if>
		</while>
	</if>

	<try>
		<if isNotTrue="${skip}">
			<print newLine="false">Copying ${from} => ${to} .... </print>
			<sleep timeout="500" />
		</if>
	</try>
	
	<catch>
		<print outTo="^error">Ignoring ${=c.CurrentException.Message} when dealing with ${from}</print>
	</catch>
	<finally>
		<eval>
			$skip?null:c.writeline('done');
		</eval>
	</finally>
</copy>

Deleting files

This one just follows the pattern. There are two additional attributes. deleteReadOnly and deleteRoot, which control whether readonly files, and the root directory specified in from attribute, will be deleted.

Deleting a non-existing file or directory is not an error.

For example, the below deletes r:\backup.old, including hidden, system and read only files, printing the names of deleted files.

<delete from="r:\backup.old" 
			deleteRoot="false" deleteReadonly="true" hidden="true" recursive="true">
	<print>Deleting ${from}</print>
</delete>

Creating and unpacking ZIP archives

Again, same pattern. To ZIP r:\backup folder to a zip with default compression and password 'password' run

<zip from="r:\backup" to="x.zip" recursive="true" hidden="true" password='password'>
	<print>Archiving ${}</print>
</zip>

There are two inconvenient moments with an ancient format of ZIP, which should be kept in mind:

  • Unicode filenames (support may be enabled by setting unicode='true'). Not all ZIP implementations can correctly expand produced archives.
  • Time zone of the files. Most system expect it to be in local time, which is default for XSharper as well. However, it's problematic when distributing archives across different timezones, in which case XSharper may be instructed to use UTC time stamp (zipTime='utcFileTime').

Unzipping the archive previously created is easy too:

<delete from="r:\tmp" recursive="true" hidden="true" />
<unzip from="x.zip " to="r:\tmp" hidden="true" password='password'>
	<print>Extracting ${from} => ${to}</print>

	<!-- Ignore errors -->
	<catch />
</unzip>