//#define SelectorTrace // FileSelector.cs // ------------------------------------------------------------------ // // Copyright (c) 2008-2011 Dino Chiesa. // All rights reserved. // // This code module is part of DotNetZip, a zipfile class library. // // ------------------------------------------------------------------ // // This code is licensed under the Microsoft Public License. // See the file License.txt for the license details. // More info on: http://dotnetzip.codeplex.com // // ------------------------------------------------------------------ // // last saved: <2011-August-05 11:03:11> // // ------------------------------------------------------------------ // // This module implements a "file selector" that finds files based on a // set of inclusion criteria, including filename, size, file time, and // potentially file attributes. The criteria are given in a string with // a simple expression language. Examples: // // find all .txt files: // name = *.txt // // shorthand for the above // *.txt // // all files modified after January 1st, 2009 // mtime > 2009-01-01 // // All .txt files modified after the first of the year // name = *.txt AND mtime > 2009-01-01 // // All .txt files modified after the first of the year, or any file with the archive bit set // (name = *.txt AND mtime > 2009-01-01) or (attribtues = A) // // All .txt files or any file greater than 1mb in size // (name = *.txt or size > 1mb) // // and so on. // ------------------------------------------------------------------ using System; using System.Globalization; using System.IO; using System.Text; using System.Reflection; using System.ComponentModel; using System.Text.RegularExpressions; using System.Collections.Generic; #if SILVERLIGHT using System.Linq; #endif namespace OfficeOpenXml.Packaging.Ionic { /// /// Enumerates the options for a logical conjunction. This enum is intended for use /// internally by the FileSelector class. /// internal enum LogicalConjunction { NONE, AND, OR, XOR, } internal enum WhichTime { atime, mtime, ctime, } internal enum ComparisonOperator { [Description(">")] GreaterThan, [Description(">=")] GreaterThanOrEqualTo, [Description("<")] LesserThan, [Description("<=")] LesserThanOrEqualTo, [Description("=")] EqualTo, [Description("!=")] NotEqualTo } internal abstract partial class SelectionCriterion { internal virtual bool Verbose { get;set; } internal abstract bool Evaluate(string filename); [System.Diagnostics.Conditional("SelectorTrace")] protected static void CriterionTrace(string format, params object[] args) { //System.Console.WriteLine(" " + format, args); } } internal partial class SizeCriterion : SelectionCriterion { internal ComparisonOperator Operator; internal Int64 Size; public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append("size ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(Size.ToString()); return sb.ToString(); } internal override bool Evaluate(string filename) { System.IO.FileInfo fi = new System.IO.FileInfo(filename); CriterionTrace("SizeCriterion::Evaluate('{0}' [{1}])", filename, this.ToString()); return _Evaluate(fi.Length); } private bool _Evaluate(Int64 Length) { bool result = false; switch (Operator) { case ComparisonOperator.GreaterThanOrEqualTo: result = Length >= Size; break; case ComparisonOperator.GreaterThan: result = Length > Size; break; case ComparisonOperator.LesserThanOrEqualTo: result = Length <= Size; break; case ComparisonOperator.LesserThan: result = Length < Size; break; case ComparisonOperator.EqualTo: result = Length == Size; break; case ComparisonOperator.NotEqualTo: result = Length != Size; break; default: throw new ArgumentException("Operator"); } return result; } } internal partial class TimeCriterion : SelectionCriterion { internal ComparisonOperator Operator; internal WhichTime Which; internal DateTime Time; public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append(Which.ToString()).Append(" ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(Time.ToString("yyyy-MM-dd-HH:mm:ss")); return sb.ToString(); } internal override bool Evaluate(string filename) { DateTime x; switch (Which) { case WhichTime.atime: x = System.IO.File.GetLastAccessTime(filename).ToUniversalTime(); break; case WhichTime.mtime: x = System.IO.File.GetLastWriteTime(filename).ToUniversalTime(); break; case WhichTime.ctime: x = System.IO.File.GetCreationTime(filename).ToUniversalTime(); break; default: throw new ArgumentException("Operator"); } CriterionTrace("TimeCriterion({0},{1})= {2}", filename, Which.ToString(), x); return _Evaluate(x); } private bool _Evaluate(DateTime x) { bool result = false; switch (Operator) { case ComparisonOperator.GreaterThanOrEqualTo: result = (x >= Time); break; case ComparisonOperator.GreaterThan: result = (x > Time); break; case ComparisonOperator.LesserThanOrEqualTo: result = (x <= Time); break; case ComparisonOperator.LesserThan: result = (x < Time); break; case ComparisonOperator.EqualTo: result = (x == Time); break; case ComparisonOperator.NotEqualTo: result = (x != Time); break; default: throw new ArgumentException("Operator"); } CriterionTrace("TimeCriterion: {0}", result); return result; } } internal partial class NameCriterion : SelectionCriterion { private Regex _re; private String _regexString; internal ComparisonOperator Operator; private string _MatchingFileSpec; internal virtual string MatchingFileSpec { set { // workitem 8245 if (Directory.Exists(value)) { _MatchingFileSpec = ".\\" + value + "\\*.*"; } else { _MatchingFileSpec = value; } _regexString = "^" + Regex.Escape(_MatchingFileSpec) .Replace(@"\\\*\.\*", @"\\([^\.]+|.*\.[^\\\.]*)") .Replace(@"\.\*", @"\.[^\\\.]*") .Replace(@"\*", @".*") //.Replace(@"\*", @"[^\\\.]*") // ill-conceived .Replace(@"\?", @"[^\\\.]") + "$"; CriterionTrace("NameCriterion regexString({0})", _regexString); _re = new Regex(_regexString, RegexOptions.IgnoreCase); } } public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append("name ").Append(EnumUtil.GetDescription(Operator)) .Append(" '") .Append(_MatchingFileSpec) .Append("'"); return sb.ToString(); } internal override bool Evaluate(string filename) { CriterionTrace("NameCriterion::Evaluate('{0}' pattern[{1}])", filename, _MatchingFileSpec); return _Evaluate(filename); } private bool _Evaluate(string fullpath) { CriterionTrace("NameCriterion::Evaluate({0})", fullpath); // No slash in the pattern implicitly means recurse, which means compare to // filename only, not full path. String f = (_MatchingFileSpec.IndexOf('\\') == -1) ? System.IO.Path.GetFileName(fullpath) : fullpath; // compare to fullpath bool result = _re.IsMatch(f); if (Operator != ComparisonOperator.EqualTo) result = !result; return result; } } internal partial class TypeCriterion : SelectionCriterion { private char ObjectType; // 'D' = Directory, 'F' = File internal ComparisonOperator Operator; internal string AttributeString { get { return ObjectType.ToString(); } set { if (value.Length != 1 || (value[0]!='D' && value[0]!='F')) throw new ArgumentException("Specify a single character: either D or F"); ObjectType = value[0]; } } public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append("type ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(AttributeString); return sb.ToString(); } internal override bool Evaluate(string filename) { CriterionTrace("TypeCriterion::Evaluate({0})", filename); bool result = (ObjectType == 'D') ? Directory.Exists(filename) : File.Exists(filename); if (Operator != ComparisonOperator.EqualTo) result = !result; return result; } } #if !SILVERLIGHT internal partial class AttributesCriterion : SelectionCriterion { private FileAttributes _Attributes; internal ComparisonOperator Operator; internal string AttributeString { get { string result = ""; if ((_Attributes & FileAttributes.Hidden) != 0) result += "H"; if ((_Attributes & FileAttributes.System) != 0) result += "S"; if ((_Attributes & FileAttributes.ReadOnly) != 0) result += "R"; if ((_Attributes & FileAttributes.Archive) != 0) result += "A"; if ((_Attributes & FileAttributes.ReparsePoint) != 0) result += "L"; if ((_Attributes & FileAttributes.NotContentIndexed) != 0) result += "I"; return result; } set { _Attributes = FileAttributes.Normal; foreach (char c in value.ToUpper(CultureInfo.InvariantCulture)) { switch (c) { case 'H': if ((_Attributes & FileAttributes.Hidden) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.Hidden; break; case 'R': if ((_Attributes & FileAttributes.ReadOnly) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.ReadOnly; break; case 'S': if ((_Attributes & FileAttributes.System) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.System; break; case 'A': if ((_Attributes & FileAttributes.Archive) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.Archive; break; case 'I': if ((_Attributes & FileAttributes.NotContentIndexed) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.NotContentIndexed; break; case 'L': if ((_Attributes & FileAttributes.ReparsePoint) != 0) throw new ArgumentException(String.Format("Repeated flag. ({0})", c), "value"); _Attributes |= FileAttributes.ReparsePoint; break; default: throw new ArgumentException(value); } } } } public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append("attributes ").Append(EnumUtil.GetDescription(Operator)).Append(" ").Append(AttributeString); return sb.ToString(); } private bool _EvaluateOne(FileAttributes fileAttrs, FileAttributes criterionAttrs) { bool result = false; if ((_Attributes & criterionAttrs) == criterionAttrs) result = ((fileAttrs & criterionAttrs) == criterionAttrs); else result = true; return result; } internal override bool Evaluate(string filename) { // workitem 10191 if (Directory.Exists(filename)) { // Directories don't have file attributes, so the result // of an evaluation is always NO. This gets negated if // the operator is NotEqualTo. return (Operator != ComparisonOperator.EqualTo); } #if NETCF FileAttributes fileAttrs = NetCfFile.GetAttributes(filename); #else FileAttributes fileAttrs = System.IO.File.GetAttributes(filename); #endif return _Evaluate(fileAttrs); } private bool _Evaluate(FileAttributes fileAttrs) { bool result = _EvaluateOne(fileAttrs, FileAttributes.Hidden); if (result) result = _EvaluateOne(fileAttrs, FileAttributes.System); if (result) result = _EvaluateOne(fileAttrs, FileAttributes.ReadOnly); if (result) result = _EvaluateOne(fileAttrs, FileAttributes.Archive); if (result) result = _EvaluateOne(fileAttrs, FileAttributes.NotContentIndexed); if (result) result = _EvaluateOne(fileAttrs, FileAttributes.ReparsePoint); if (Operator != ComparisonOperator.EqualTo) result = !result; return result; } } #endif internal partial class CompoundCriterion : SelectionCriterion { internal LogicalConjunction Conjunction; internal SelectionCriterion Left; private SelectionCriterion _Right; internal SelectionCriterion Right { get { return _Right; } set { _Right = value; if (value == null) Conjunction = LogicalConjunction.NONE; else if (Conjunction == LogicalConjunction.NONE) Conjunction = LogicalConjunction.AND; } } internal override bool Evaluate(string filename) { bool result = Left.Evaluate(filename); switch (Conjunction) { case LogicalConjunction.AND: if (result) result = Right.Evaluate(filename); break; case LogicalConjunction.OR: if (!result) result = Right.Evaluate(filename); break; case LogicalConjunction.XOR: result ^= Right.Evaluate(filename); break; default: throw new ArgumentException("Conjunction"); } return result; } public override String ToString() { StringBuilder sb = new StringBuilder(); sb.Append("(") .Append((Left != null) ? Left.ToString() : "null") .Append(" ") .Append(Conjunction.ToString()) .Append(" ") .Append((Right != null) ? Right.ToString() : "null") .Append(")"); return sb.ToString(); } } /// /// FileSelector encapsulates logic that selects files from a source - a zip file /// or the filesystem - based on a set of criteria. This class is used internally /// by the DotNetZip library, in particular for the AddSelectedFiles() methods. /// This class can also be used independently of the zip capability in DotNetZip. /// /// /// /// /// /// The FileSelector class is used internally by the ZipFile class for selecting /// files for inclusion into the ZipFile, when the method, or one of /// its overloads, is called. It's also used for the methods. Typically, an /// application that creates or manipulates Zip archives will not directly /// interact with the FileSelector class. /// /// /// /// Some applications may wish to use the FileSelector class directly, to /// select files from disk volumes based on a set of criteria, without creating or /// querying Zip archives. The file selection criteria include: a pattern to /// match the filename; the last modified, created, or last accessed time of the /// file; the size of the file; and the attributes of the file. /// /// /// /// Consult the documentation for /// for more information on specifying the selection criteria. /// /// /// internal partial class FileSelector { internal SelectionCriterion _Criterion; #if NOTUSED /// /// The default constructor. /// /// /// Typically, applications won't use this constructor. Instead they'll /// call the constructor that accepts a selectionCriteria string. If you /// use this constructor, you'll want to set the SelectionCriteria /// property on the instance before calling SelectFiles(). /// protected FileSelector() { } #endif /// /// Constructor that allows the caller to specify file selection criteria. /// /// /// /// /// This constructor allows the caller to specify a set of criteria for /// selection of files. /// /// /// /// See for a description of /// the syntax of the selectionCriteria string. /// /// /// /// By default the FileSelector will traverse NTFS Reparse Points. To /// change this, use FileSelector(String, bool). /// /// /// /// The criteria for file selection. public FileSelector(String selectionCriteria) : this(selectionCriteria, true) { } /// /// Constructor that allows the caller to specify file selection criteria. /// /// /// /// /// This constructor allows the caller to specify a set of criteria for /// selection of files. /// /// /// /// See for a description of /// the syntax of the selectionCriteria string. /// /// /// /// The criteria for file selection. /// /// whether to traverse NTFS reparse points (junctions). /// public FileSelector(String selectionCriteria, bool traverseDirectoryReparsePoints) { if (!String.IsNullOrEmpty(selectionCriteria)) _Criterion = _ParseCriterion(selectionCriteria); TraverseReparsePoints = traverseDirectoryReparsePoints; } /// /// The string specifying which files to include when retrieving. /// /// /// /// /// Specify the criteria in statements of 3 elements: a noun, an operator, /// and a value. Consider the string "name != *.doc" . The noun is /// "name". The operator is "!=", implying "Not Equal". The value is /// "*.doc". That criterion, in English, says "all files with a name that /// does not end in the .doc extension." /// /// /// /// Supported nouns include "name" (or "filename") for the filename; /// "atime", "mtime", and "ctime" for last access time, last modfied time, /// and created time of the file, respectively; "attributes" (or "attrs") /// for the file attributes; "size" (or "length") for the file length /// (uncompressed); and "type" for the type of object, either a file or a /// directory. The "attributes", "type", and "name" nouns all support = /// and != as operators. The "size", "atime", "mtime", and "ctime" nouns /// support = and !=, and >, >=, <, <= as well. The times are /// taken to be expressed in local time. /// /// /// /// Specify values for the file attributes as a string with one or more of /// the characters H,R,S,A,I,L in any order, implying file attributes of /// Hidden, ReadOnly, System, Archive, NotContextIndexed, and ReparsePoint /// (symbolic link) respectively. /// /// /// /// To specify a time, use YYYY-MM-DD-HH:mm:ss or YYYY/MM/DD-HH:mm:ss as /// the format. If you omit the HH:mm:ss portion, it is assumed to be /// 00:00:00 (midnight). /// /// /// /// The value for a size criterion is expressed in integer quantities of /// bytes, kilobytes (use k or kb after the number), megabytes (m or mb), /// or gigabytes (g or gb). /// /// /// /// The value for a name is a pattern to match against the filename, /// potentially including wildcards. The pattern follows CMD.exe glob /// rules: * implies one or more of any character, while ? implies one /// character. If the name pattern contains any slashes, it is matched to /// the entire filename, including the path; otherwise, it is matched /// against only the filename without the path. This means a pattern of /// "*\*.*" matches all files one directory level deep, while a pattern of /// "*.*" matches all files in all directories. /// /// /// /// To specify a name pattern that includes spaces, use single quotes /// around the pattern. A pattern of "'* *.*'" will match all files that /// have spaces in the filename. The full criteria string for that would /// be "name = '* *.*'" . /// /// /// /// The value for a type criterion is either F (implying a file) or D /// (implying a directory). /// /// /// /// Some examples: /// /// /// /// /// criteria /// Files retrieved /// /// /// /// name != *.xls /// any file with an extension that is not .xls /// /// /// /// /// name = *.mp3 /// any file with a .mp3 extension. /// /// /// /// /// *.mp3 /// (same as above) any file with a .mp3 extension. /// /// /// /// /// attributes = A /// all files whose attributes include the Archive bit. /// /// /// /// /// attributes != H /// all files whose attributes do not include the Hidden bit. /// /// /// /// /// mtime > 2009-01-01 /// all files with a last modified time after January 1st, 2009. /// /// /// /// /// ctime > 2009/01/01-03:00:00 /// all files with a created time after 3am (local time), /// on January 1st, 2009. /// /// /// /// /// size > 2gb /// all files whose uncompressed size is greater than 2gb. /// /// /// /// /// type = D /// all directories in the filesystem. /// /// /// /// /// /// You can combine criteria with the conjunctions AND, OR, and XOR. Using /// a string like "name = *.txt AND size >= 100k" for the /// selectionCriteria retrieves entries whose names end in .txt, and whose /// uncompressed size is greater than or equal to 100 kilobytes. /// /// /// /// For more complex combinations of criteria, you can use parenthesis to /// group clauses in the boolean logic. Absent parenthesis, the /// precedence of the criterion atoms is determined by order of /// appearance. Unlike the C# language, the AND conjunction does not take /// precendence over the logical OR. This is important only in strings /// that contain 3 or more criterion atoms. In other words, "name = *.txt /// and size > 1000 or attributes = H" implies "((name = *.txt AND size /// > 1000) OR attributes = H)" while "attributes = H OR name = *.txt /// and size > 1000" evaluates to "((attributes = H OR name = *.txt) /// AND size > 1000)". When in doubt, use parenthesis. /// /// /// /// Using time properties requires some extra care. If you want to /// retrieve all entries that were last updated on 2009 February 14, /// specify "mtime >= 2009-02-14 AND mtime < 2009-02-15". Read this /// to say: all files updated after 12:00am on February 14th, until /// 12:00am on February 15th. You can use the same bracketing approach to /// specify any time period - a year, a month, a week, and so on. /// /// /// /// The syntax allows one special case: if you provide a string with no /// spaces, it is treated as a pattern to match for the filename. /// Therefore a string like "*.xls" will be equivalent to specifying "name /// = *.xls". This "shorthand" notation does not work with compound /// criteria. /// /// /// /// There is no logic in this class that insures that the inclusion /// criteria are internally consistent. For example, it's possible to /// specify criteria that says the file must have a size of less than 100 /// bytes, as well as a size that is greater than 1000 bytes. Obviously /// no file will ever satisfy such criteria, but this class does not check /// for or detect such inconsistencies. /// /// /// /// /// /// Thrown in the setter if the value has an invalid syntax. /// public String SelectionCriteria { get { if (_Criterion == null) return null; return _Criterion.ToString(); } set { if (value == null) _Criterion = null; else if (value.Trim() == "") _Criterion = null; else _Criterion = _ParseCriterion(value); } } /// /// Indicates whether searches will traverse NTFS reparse points, like Junctions. /// public bool TraverseReparsePoints { get; set; } private enum ParseState { Start, OpenParen, CriterionDone, ConjunctionPending, Whitespace, } private static class RegexAssertions { public static readonly String PrecededByOddNumberOfSingleQuotes = "(?<=(?:[^']*'[^']*')*'[^']*)"; public static readonly String FollowedByOddNumberOfSingleQuotesAndLineEnd = "(?=[^']*'(?:[^']*'[^']*')*[^']*$)"; public static readonly String PrecededByEvenNumberOfSingleQuotes = "(?<=(?:[^']*'[^']*')*[^']*)"; public static readonly String FollowedByEvenNumberOfSingleQuotesAndLineEnd = "(?=(?:[^']*'[^']*')*[^']*$)"; } private static string NormalizeCriteriaExpression(string source) { // The goal here is to normalize the criterion expression. At output, in // the transformed criterion string, every significant syntactic element // - a property element, grouping paren for the boolean logic, operator // ( = < > != ), conjunction, or property value - will be separated from // its neighbors by at least one space. Thus, // // before after // ------------------------------------------------------------------- // name=*.txt name = *.txt // (size>100)AND(name=*.txt) ( size > 100 ) AND ( name = *.txt ) // // This is relatively straightforward using regular expression // replacement. This method applies a distinct regex pattern and // corresponding replacement string for each one of a number of cases: // an open paren followed by a word; a word followed by a close-paren; a // pair of open parens; a close paren followed by a word (which should // then be followed by an open paren). And so on. These patterns and // replacements are all stored in prPairs. By applying each of these // regex replacements in turn, we get the transformed string. Easy. // // The resulting "normalized" criterion string, is then used as the // subject that gets parsed, by splitting the string into tokens that // are separated by spaces. Here, there's a twist. The spaces within // single-quote delimiters do not delimit distinct tokens. So, this // normalization method temporarily replaces those spaces with // ASCII 6 (0x06), a control character which is not a legal // character in a filename. The parsing logic that happens later will // revert that change, restoring the original value of the filename // specification. // // To illustrate, for a "before" string of [(size>100)AND(name='Name // (with Parens).txt')] , the "after" string is [( size > 100 ) AND // ( name = 'Name\u0006(with\u0006Parens).txt' )]. // string[][] prPairs = { // A. opening double parens - insert a space between them new string[] { @"([^']*)\(\(([^']+)", "$1( ($2" }, // B. closing double parens - insert a space between new string[] { @"(.)\)\)", "$1) )" }, // C. single open paren with a following word - insert a space between new string[] { @"\((\S)", "( $1" }, // D. single close paren with a preceding word - insert a space between the two new string[] { @"(\S)\)", "$1 )" }, // E. close paren at line start?, insert a space before the close paren // this seems like a degenerate case. I don't recall why it's here. new string[] { @"^\)", " )" }, // F. a word (likely a conjunction) followed by an open paren - insert a space between new string[] { @"(\S)\(", "$1 (" }, // G. single close paren followed by word - insert a paren after close paren new string[] { @"\)(\S)", ") $1" }, // H. insert space between = and a following single quote //new string[] { @"(=|!=)('[^']*')", "$1 $2" }, new string[] { @"(=)('[^']*')", "$1 $2" }, // I. insert space between property names and the following operator //new string[] { @"([^ ])([><(?:!=)=])", "$1 $2" }, new string[] { @"([^ !><])(>|<|!=|=)", "$1 $2" }, // J. insert spaces between operators and the following values //new string[] { @"([><(?:!=)=])([^ ])", "$1 $2" }, new string[] { @"(>|<|!=|=)([^ =])", "$1 $2" }, // K. replace fwd slash with backslash new string[] { @"/", "\\" }, }; string interim = source; for (int i=0; i < prPairs.Length; i++) { //char caseIdx = (char)('A' + i); string pattern = RegexAssertions.PrecededByEvenNumberOfSingleQuotes + prPairs[i][0] + RegexAssertions.FollowedByEvenNumberOfSingleQuotesAndLineEnd; interim = Regex.Replace(interim, pattern, prPairs[i][1]); } // match a fwd slash, followed by an odd number of single quotes. // This matches fwd slashes only inside a pair of single quote delimiters, // eg, a filename. This must be done as well as the case above, to handle // filenames specified inside quotes as well as filenames without quotes. var regexPattern = @"/" + RegexAssertions.FollowedByOddNumberOfSingleQuotesAndLineEnd; // replace with backslash interim = Regex.Replace(interim, regexPattern, "\\"); // match a space, followed by an odd number of single quotes. // This matches spaces only inside a pair of single quote delimiters. regexPattern = " " + RegexAssertions.FollowedByOddNumberOfSingleQuotesAndLineEnd; // Replace all spaces that appear inside single quotes, with // ascii 6. This allows a split on spaces to get tokens in // the expression. The split will not split any filename or // wildcard that appears within single quotes. After tokenizing, we // need to replace ascii 6 with ascii 32 to revert the // spaces within quotes. return Regex.Replace(interim, regexPattern, "\u0006"); } private static SelectionCriterion _ParseCriterion(String s) { if (s == null) return null; // inject spaces after open paren and before close paren, etc s = NormalizeCriteriaExpression(s); // no spaces in the criteria is shorthand for filename glob if (s.IndexOf(" ") == -1) s = "name = " + s; // split the expression into tokens string[] tokens = s.Trim().Split(' ', '\t'); if (tokens.Length < 3) throw new ArgumentException(s); SelectionCriterion current = null; LogicalConjunction pendingConjunction = LogicalConjunction.NONE; ParseState state; var stateStack = new System.Collections.Generic.Stack(); var critStack = new System.Collections.Generic.Stack(); stateStack.Push(ParseState.Start); for (int i = 0; i < tokens.Length; i++) { string tok1 = tokens[i].ToLower(CultureInfo.InvariantCulture); switch (tok1) { case "and": case "xor": case "or": state = stateStack.Peek(); if (state != ParseState.CriterionDone) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); if (tokens.Length <= i + 3) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); pendingConjunction = (LogicalConjunction)Enum.Parse(typeof(LogicalConjunction), tokens[i].ToUpper(CultureInfo.InvariantCulture), true); current = new CompoundCriterion { Left = current, Right = null, Conjunction = pendingConjunction }; stateStack.Push(state); stateStack.Push(ParseState.ConjunctionPending); critStack.Push(current); break; case "(": state = stateStack.Peek(); if (state != ParseState.Start && state != ParseState.ConjunctionPending && state != ParseState.OpenParen) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); if (tokens.Length <= i + 4) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); stateStack.Push(ParseState.OpenParen); break; case ")": state = stateStack.Pop(); if (stateStack.Peek() != ParseState.OpenParen) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); stateStack.Pop(); stateStack.Push(ParseState.CriterionDone); break; case "atime": case "ctime": case "mtime": if (tokens.Length <= i + 2) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); DateTime t; try { t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd-HH:mm:ss", null); } catch (FormatException) { try { t = DateTime.ParseExact(tokens[i + 2], "yyyy/MM/dd-HH:mm:ss", null); } catch (FormatException) { try { t = DateTime.ParseExact(tokens[i + 2], "yyyy/MM/dd", null); } catch (FormatException) { try { t = DateTime.ParseExact(tokens[i + 2], "MM/dd/yyyy", null); } catch (FormatException) { t = DateTime.ParseExact(tokens[i + 2], "yyyy-MM-dd", null); } } } } t= DateTime.SpecifyKind(t, DateTimeKind.Local).ToUniversalTime(); current = new TimeCriterion { Which = (WhichTime)Enum.Parse(typeof(WhichTime), tokens[i], true), Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]), Time = t }; i += 2; stateStack.Push(ParseState.CriterionDone); break; case "length": case "size": if (tokens.Length <= i + 2) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); Int64 sz = 0; string v = tokens[i + 2]; if (v.EndsWith("K", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024; else if (v.EndsWith("KB", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024; else if (v.EndsWith("M", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024; else if (v.EndsWith("MB", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024; else if (v.EndsWith("G", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 1)) * 1024 * 1024 * 1024; else if (v.EndsWith("GB", StringComparison.InvariantCultureIgnoreCase)) sz = Int64.Parse(v.Substring(0, v.Length - 2)) * 1024 * 1024 * 1024; else sz = Int64.Parse(tokens[i + 2]); current = new SizeCriterion { Size = sz, Operator = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]) }; i += 2; stateStack.Push(ParseState.CriterionDone); break; case "filename": case "name": { if (tokens.Length <= i + 2) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); ComparisonOperator c = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]); if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); string m = tokens[i + 2]; // handle single-quoted filespecs (used to include // spaces in filename patterns) if (m.StartsWith("'") && m.EndsWith("'")) { // trim off leading and trailing single quotes and // revert the control characters to spaces. m = m.Substring(1, m.Length - 2) .Replace("\u0006", " "); } // if (m.StartsWith("'")) // m = m.Replace("\u0006", " "); current = new NameCriterion { MatchingFileSpec = m, Operator = c }; i += 2; stateStack.Push(ParseState.CriterionDone); } break; #if !SILVERLIGHT case "attrs": case "attributes": #endif case "type": { if (tokens.Length <= i + 2) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); ComparisonOperator c = (ComparisonOperator)EnumUtil.Parse(typeof(ComparisonOperator), tokens[i + 1]); if (c != ComparisonOperator.NotEqualTo && c != ComparisonOperator.EqualTo) throw new ArgumentException(String.Join(" ", tokens, i, tokens.Length - i)); #if SILVERLIGHT current = (SelectionCriterion) new TypeCriterion { AttributeString = tokens[i + 2], Operator = c }; #else current = (tok1 == "type") ? (SelectionCriterion) new TypeCriterion { AttributeString = tokens[i + 2], Operator = c } : (SelectionCriterion) new AttributesCriterion { AttributeString = tokens[i + 2], Operator = c }; #endif i += 2; stateStack.Push(ParseState.CriterionDone); } break; case "": // NOP stateStack.Push(ParseState.Whitespace); break; default: throw new ArgumentException("'" + tokens[i] + "'"); } state = stateStack.Peek(); if (state == ParseState.CriterionDone) { stateStack.Pop(); if (stateStack.Peek() == ParseState.ConjunctionPending) { while (stateStack.Peek() == ParseState.ConjunctionPending) { var cc = critStack.Pop() as CompoundCriterion; cc.Right = current; current = cc; // mark the parent as current (walk up the tree) stateStack.Pop(); // the conjunction is no longer pending state = stateStack.Pop(); if (state != ParseState.CriterionDone) throw new ArgumentException("??"); } } else stateStack.Push(ParseState.CriterionDone); // not sure? } if (state == ParseState.Whitespace) stateStack.Pop(); } return current; } /// /// Returns a string representation of the FileSelector object. /// /// The string representation of the boolean logic statement of the file /// selection criteria for this instance. public override String ToString() { return "FileSelector("+_Criterion.ToString()+")"; } private bool Evaluate(string filename) { // dinoch - Thu, 11 Feb 2010 18:34 SelectorTrace("Evaluate({0})", filename); bool result = _Criterion.Evaluate(filename); return result; } [System.Diagnostics.Conditional("SelectorTrace")] private void SelectorTrace(string format, params object[] args) { if (_Criterion != null && _Criterion.Verbose) System.Console.WriteLine(format, args); } /// /// Returns the names of the files in the specified directory /// that fit the selection criteria specified in the FileSelector. /// /// /// /// This is equivalent to calling /// with recurseDirectories = false. /// /// /// /// The name of the directory over which to apply the FileSelector /// criteria. /// /// /// /// A collection of strings containing fully-qualified pathnames of files /// that match the criteria specified in the FileSelector instance. /// public System.Collections.Generic.ICollection SelectFiles(String directory) { return SelectFiles(directory, false); } /// /// Returns the names of the files in the specified directory that fit the /// selection criteria specified in the FileSelector, optionally recursing /// through subdirectories. /// /// /// /// This method applies the file selection criteria contained in the /// FileSelector to the files contained in the given directory, and /// returns the names of files that conform to the criteria. /// /// /// /// The name of the directory over which to apply the FileSelector /// criteria. /// /// /// /// Whether to recurse through subdirectories when applying the file /// selection criteria. /// /// /// /// A collection of strings containing fully-qualified pathnames of files /// that match the criteria specified in the FileSelector instance. /// public System.Collections.ObjectModel.ReadOnlyCollection SelectFiles(String directory, bool recurseDirectories) { if (_Criterion == null) throw new ArgumentException("SelectionCriteria has not been set"); var list = new List(); try { if (Directory.Exists(directory)) { String[] filenames = Directory.GetFiles(directory); // add the files: foreach (String filename in filenames) { if (Evaluate(filename)) list.Add(filename); } if (recurseDirectories) { // add the subdirectories: String[] dirnames = Directory.GetDirectories(directory); foreach (String dir in dirnames) { if (this.TraverseReparsePoints #if !SILVERLIGHT || ((File.GetAttributes(dir) & FileAttributes.ReparsePoint) == 0) #endif ) { // workitem 10191 if (Evaluate(dir)) list.Add(dir); list.AddRange(this.SelectFiles(dir, recurseDirectories)); } } } } } // can get System.UnauthorizedAccessException here catch (System.UnauthorizedAccessException) { } catch (System.IO.IOException) { } return list.AsReadOnly(); } } /// /// Summary description for EnumUtil. /// internal sealed class EnumUtil { private EnumUtil() { } /// /// Returns the value of the DescriptionAttribute if the specified Enum /// value has one. If not, returns the ToString() representation of the /// Enum value. /// /// The Enum to get the description for /// internal static string GetDescription(System.Enum value) { FieldInfo fi = value.GetType().GetField(value.ToString()); var attributes = (DescriptionAttribute[])fi.GetCustomAttributes(typeof(DescriptionAttribute), false); if (attributes.Length > 0) return attributes[0].Description; else return value.ToString(); } /// /// Converts the string representation of the name or numeric value of one /// or more enumerated constants to an equivalent enumerated object. /// Note: use the DescriptionAttribute on enum values to enable this. /// /// The System.Type of the enumeration. /// /// A string containing the name or value to convert. /// /// internal static object Parse(Type enumType, string stringRepresentation) { return Parse(enumType, stringRepresentation, false); } #if SILVERLIGHT public static System.Enum[] GetEnumValues(Type type) { if (!type.IsEnum) throw new ArgumentException("not an enum"); return ( from field in type.GetFields(BindingFlags.Public | BindingFlags.Static) where field.IsLiteral select (System.Enum)field.GetValue(null) ).ToArray(); } public static string[] GetEnumStrings() { var type = typeof(T); if (!type.IsEnum) throw new ArgumentException("not an enum"); return ( from field in type.GetFields(BindingFlags.Public | BindingFlags.Static) where field.IsLiteral select field.Name ).ToArray(); } #endif /// /// Converts the string representation of the name or numeric value of one /// or more enumerated constants to an equivalent enumerated object. A /// parameter specified whether the operation is case-sensitive. Note: /// use the DescriptionAttribute on enum values to enable this. /// /// The System.Type of the enumeration. /// /// A string containing the name or value to convert. /// /// /// Whether the operation is case-sensitive or not. /// internal static object Parse(Type enumType, string stringRepresentation, bool ignoreCase) { if (ignoreCase) stringRepresentation = stringRepresentation.ToLower(CultureInfo.InvariantCulture); #if SILVERLIGHT foreach (System.Enum enumVal in GetEnumValues(enumType)) #else foreach (System.Enum enumVal in System.Enum.GetValues(enumType)) #endif { string description = GetDescription(enumVal); if (ignoreCase) description = description.ToLower(CultureInfo.InvariantCulture); if (description == stringRepresentation) return enumVal; } return System.Enum.Parse(enumType, stringRepresentation, ignoreCase); } } #if DEMO internal class DemonstrateFileSelector { private string _directory; private bool _recurse; private bool _traverse; private bool _verbose; private string _selectionCriteria; private FileSelector f; public DemonstrateFileSelector() { this._directory = "."; this._recurse = true; } public DemonstrateFileSelector(string[] args) : this() { for (int i = 0; i < args.Length; i++) { switch(args[i]) { case"-?": Usage(); Environment.Exit(0); break; case "-d": i++; if (args.Length <= i) throw new ArgumentException("-directory"); this._directory = args[i]; break; case "-norecurse": this._recurse = false; break; case "-j-": this._traverse = false; break; case "-j+": this._traverse = true; break; case "-v": this._verbose = true; break; default: if (this._selectionCriteria != null) throw new ArgumentException(args[i]); this._selectionCriteria = args[i]; break; } if (this._selectionCriteria != null) this.f = new FileSelector(this._selectionCriteria); } } public static void Main(string[] args) { try { Console.WriteLine(); new DemonstrateFileSelector(args).Run(); } catch (Exception exc1) { Console.WriteLine("Exception: {0}", exc1.ToString()); Usage(); } } public void Run() { if (this.f == null) this.f = new FileSelector("name = *.jpg AND (size > 1000 OR atime < 2009-02-14-01:00:00)"); this.f.TraverseReparsePoints = _traverse; this.f.Verbose = this._verbose; Console.WriteLine(); Console.WriteLine(new String(':', 88)); Console.WriteLine("Selecting files:\n" + this.f.ToString()); var files = this.f.SelectFiles(this._directory, this._recurse); if (files.Count == 0) { Console.WriteLine("no files."); } else { Console.WriteLine("files: {0}", files.Count); foreach (string file in files) { Console.WriteLine(" " + file); } } } public static void Usage() { Console.WriteLine("FileSelector: select files based on selection criteria.\n"); Console.WriteLine("Usage:\n FileSelector [options]\n" + "\n" + " -d directory to select from (Default .)\n" + " -norecurse don't recurse into subdirs\n" + " -j- don't traverse junctions\n" + " -v verbose output\n"); } } #endif }