1042 lines
40 KiB
C#
1042 lines
40 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
|
|
namespace Minimatch
|
|
{
|
|
///<summary>Contains options that control how Minimatch matches strings.</summary>
|
|
public class Options
|
|
{
|
|
///<summary>Suppresses the behavior of treating # at the start of a pattern as a comment.</summary>
|
|
public bool NoComment { get; set; }
|
|
///<summary>Suppresses the behavior of treating a leading ! character as negation.</summary>
|
|
public bool NoNegate { get; set; }
|
|
///<summary>Do not expand {a,b} and {1.3} brace sets.</summary>
|
|
public bool NoBrace { get; set; }
|
|
///<summary>Disable ** matching against multiple folder names.</summary>
|
|
public bool NoGlobStar { get; set; }
|
|
///<summary>Ignores case differences when matching.</summary>
|
|
public bool NoCase { get; set; }
|
|
///<summary>Disable "extglob" style patterns like <c>+(a|b)</c>.</summary>
|
|
public bool NoExt { get; set; }
|
|
///<summary>Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot.
|
|
///Note that by default, <c>a/**/b</c> will not match <c>a/.d/b</c>, unless dot is set.</summary>
|
|
public bool Dot { get; set; }
|
|
///<summary>When a match is not found by Match(), return a list containing the pattern itself. If not set, an empty list is returned if there are no matches.</summary>
|
|
public bool NoNull { get; set; }
|
|
///<summary>Returns from negate expressions the same as if they were not negated. (ie, true on a hit, false on a miss).</summary>
|
|
public bool FlipNegate { get; set; }
|
|
|
|
///<summary>If set, then patterns without slashes will be matched against the basename of the path if it contains slashes. For example, <c>a?b</c> would match the path <c>/xyz/123/acb</c>, but not <c>/xyz/acb/123</c>.</summary>
|
|
public bool MatchBase { get; set; }
|
|
|
|
internal RegexOptions RegexOptions { get { return NoCase ? RegexOptions.IgnoreCase : RegexOptions.None; } }
|
|
|
|
///<summary>If true, backslahes in patterns and paths will be treated as forward slashes. This disables escape characters.</summary>
|
|
public bool AllowWindowsPaths { get; set; }
|
|
|
|
// Aliases:
|
|
///<summary>Ignores case differences when matching. This is the same as NoCase.</summary>
|
|
public bool IgnoreCase
|
|
{
|
|
get { return NoCase; }
|
|
set { NoCase = value; }
|
|
}
|
|
}
|
|
|
|
|
|
///<summary>Parses a single glob pattern and tests strings against it.</summary>
|
|
public class Minimatcher
|
|
{
|
|
///<summary>Creates a filter function that tests input against a pattern.</summary>
|
|
public static Func<string, bool> CreateFilter(string pattern, Options options = null)
|
|
{
|
|
if (pattern == null) throw new ArgumentNullException("pattern");
|
|
// "" only matches ""
|
|
if (String.IsNullOrWhiteSpace(pattern)) return String.IsNullOrEmpty;
|
|
|
|
var m = new Minimatcher(pattern, options);
|
|
return m.IsMatch;
|
|
}
|
|
///<summary>Tests a single input against a pattern.</summary>
|
|
///<remarks>This function reparses this input on each invocation. For performance, avoid this function and reuse a Minimatcher instance instead.</remarks>
|
|
public static bool Check(string input, string pattern, Options options = null)
|
|
{
|
|
if (input == null) throw new ArgumentNullException("input");
|
|
if (pattern == null) throw new ArgumentNullException("pattern");
|
|
|
|
// shortcut: comments match nothing.
|
|
if (options != null && !options.NoComment && pattern[0] == '#')
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// "" only matches ""
|
|
if (String.IsNullOrWhiteSpace(pattern)) return input == "";
|
|
|
|
return new Minimatcher(pattern, options).IsMatch(input);
|
|
}
|
|
|
|
///<summary>Filters a list of inputs against a single pattern.</summary>
|
|
///<remarks>This function reparses this input on each invocation. For performance, avoid this function and reuse a Minimatcher instance instead.</remarks>
|
|
public static IEnumerable<string> Filter(IEnumerable<string> list, string pattern, Options options = null)
|
|
{
|
|
var mm = new Minimatcher(pattern, options);
|
|
list = list.Where(mm.IsMatch);
|
|
if (options != null && options.NoNull)
|
|
list = list.DefaultIfEmpty(pattern);
|
|
return list;
|
|
}
|
|
|
|
///<summary>Compiles a pattern into a single regular expression.</summary>
|
|
public static Regex CreateRegex(string pattern, Options options = null)
|
|
{
|
|
return new Minimatcher(pattern, options).MakeRegex();
|
|
}
|
|
|
|
|
|
readonly Options options;
|
|
|
|
string pattern;
|
|
bool negate = false;
|
|
bool comment = false;
|
|
bool empty = false;
|
|
|
|
///<summary>Creates a new Minimatcher instance, parsing the pattern into a regex.</summary>
|
|
public Minimatcher(string pattern, Options options = null)
|
|
{
|
|
if (pattern == null) throw new ArgumentNullException("pattern");
|
|
this.options = options ?? new Options();
|
|
this.pattern = pattern.Trim();
|
|
if (this.options.AllowWindowsPaths)
|
|
this.pattern = this.pattern.Replace('\\', '/');
|
|
|
|
this.Make();
|
|
}
|
|
|
|
///<summary>Checks whether a given string matches this pattern.</summary>
|
|
public bool IsMatch(string input) { return Match(input, false); }
|
|
|
|
///<summary>Filters a list of inputs against this pattern.</summary>
|
|
public IEnumerable<string> Filter(IEnumerable<string> list)
|
|
{
|
|
list = list.Where(IsMatch);
|
|
if (options.NoNull)
|
|
list = list.DefaultIfEmpty(pattern);
|
|
return list;
|
|
}
|
|
|
|
|
|
Regex regexp;
|
|
bool isError;
|
|
|
|
IEnumerable<string> globSet;
|
|
IEnumerable<IEnumerable<ParseItem>> set;
|
|
IEnumerable<IEnumerable<string>> globParts;
|
|
|
|
|
|
// any single thing other than /
|
|
// don't need to escape / when using new RegExp()
|
|
const string qmark = "[^/]"
|
|
|
|
// * => any number of characters
|
|
, star = qmark + "*?"
|
|
|
|
// ** when dots are allowed. Anything goes, except .. and .
|
|
// not (^ or / followed by one or two dots followed by $ or /),
|
|
// followed by anything, any number of times.
|
|
, twoStarDot = "(?:(?!(?:\\/|^)(?:\\.{1,2})($|\\/)).)*?"
|
|
|
|
// not a ^ or / followed by a dot,
|
|
// followed by anything, any number of times.
|
|
, twoStarNoDot = "(?:(?!(?:\\/|^)\\.).)*?";
|
|
|
|
// characters that need to be escaped in RegExp.
|
|
static readonly HashSet<Char> reSpecials = new HashSet<char>("().*{}+?[]^$\\!".ToCharArray());
|
|
static readonly Regex slashSplit = new Regex("/+");
|
|
|
|
void Make()
|
|
{
|
|
// empty patterns and comments match nothing.
|
|
if (!options.NoComment && !string.IsNullOrEmpty(pattern) && pattern[0] == '#')
|
|
{
|
|
this.comment = true;
|
|
return;
|
|
}
|
|
if (String.IsNullOrEmpty(pattern))
|
|
{
|
|
this.empty = true;
|
|
return;
|
|
}
|
|
|
|
// step 1: figure out negation, etc.
|
|
this.ParseNegate();
|
|
|
|
// step 2: expand braces
|
|
this.globSet = BraceExpand(pattern, options);
|
|
|
|
// step 3: now we have a set, so turn each one into a series of path-portion
|
|
// matching patterns.
|
|
// These will be regexps, except in the case of "**", which is
|
|
// set to the GLOBSTAR object for globstar behavior,
|
|
// and will not contain any / characters
|
|
this.globParts = globSet.Select(s => slashSplit.Split(s)).ToList();
|
|
|
|
// glob --> regexps
|
|
this.set = globParts.Select(g => g.Select(t => this.Parse(t, false)))
|
|
.Where(g => !g.Contains(null))
|
|
.Select(g => g.Select(t => t.Item1))
|
|
.ToList();
|
|
}
|
|
|
|
void ParseNegate()
|
|
{
|
|
var negateOffset = 0;
|
|
|
|
if (options.NoNegate) return;
|
|
|
|
for (var i = 0; i < pattern.Length && pattern[i] == '!'; i++)
|
|
{
|
|
negate = !negate;
|
|
negateOffset++;
|
|
}
|
|
|
|
if (negateOffset > 0) this.pattern = pattern.Substring(negateOffset);
|
|
}
|
|
|
|
static readonly Regex hasBraces = new Regex(@"\{.*\}");
|
|
static readonly Regex numericSet = new Regex(@"^\{(-?[0-9]+)\.\.(-?[0-9]+)\}");
|
|
// Brace expansion:
|
|
// a{b,c}d -> abd acd
|
|
// a{b,}c -> abc ac
|
|
// a{0..3}d -> a0d a1d a2d a3d
|
|
// a{b,c{d,e}f}g -> abg acdfg acefg
|
|
// a{b,c}d{e,f}g -> abdeg acdeg abdeg abdfg
|
|
//
|
|
// Invalid sets are not expanded.
|
|
// a{2..}b -> a{2..}b
|
|
// a{b}c -> a{b}c
|
|
///<summary>Expands all brace ranges in a pattern, returning a sequence containing every possible combination.</summary>
|
|
public static IEnumerable<string> BraceExpand(string pattern, Options options)
|
|
{
|
|
if (options.NoBrace || !hasBraces.IsMatch(pattern))
|
|
{
|
|
// shortcut. no need to expand.
|
|
return new[] { pattern };
|
|
}
|
|
bool escaping = false;
|
|
int i;
|
|
// examples and comments refer to this crazy pattern:
|
|
// a{b,c{d,e},{f,g}h}x{y,z}
|
|
// expected:
|
|
// abxy
|
|
// abxz
|
|
// acdxy
|
|
// acdxz
|
|
// acexy
|
|
// acexz
|
|
// afhxy
|
|
// afhxz
|
|
// aghxy
|
|
// aghxz
|
|
|
|
// everything before the first \{ is just a prefix.
|
|
// So, we pluck that off, and work with the rest,
|
|
// and then prepend it to everything we find.
|
|
if (pattern[0] != '{')
|
|
{
|
|
// console.error(pattern)
|
|
string prefix = null;
|
|
for (i = 0; i < pattern.Length; i++)
|
|
{
|
|
var c = pattern[i];
|
|
// console.error(i, c)
|
|
if (c == '\\')
|
|
{
|
|
escaping = !escaping;
|
|
}
|
|
else if (c == '{' && !escaping)
|
|
{
|
|
prefix = pattern.Substring(0, i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// actually no sets, all { were escaped.
|
|
if (prefix == null)
|
|
{
|
|
// console.error("no sets")
|
|
return new[] { pattern };
|
|
}
|
|
|
|
return BraceExpand(pattern.Substring(i), options).Select(t => prefix + t);
|
|
}
|
|
|
|
// now we have something like:
|
|
// {b,c{d,e},{f,g}h}x{y,z}
|
|
// walk through the set, expanding each part, until
|
|
// the set ends. then, we'll expand the suffix.
|
|
// If the set only has a single member, then'll put the {} back
|
|
|
|
// first, handle numeric sets, since they're easier
|
|
var numset = numericSet.Match(pattern);
|
|
if (numset.Success)
|
|
{
|
|
// console.error("numset", numset[1], numset[2])
|
|
var suf = BraceExpand(pattern.Substring(numset.Length), options).ToList();
|
|
int start = int.Parse(numset.Groups[1].Value),
|
|
end = int.Parse(numset.Groups[2].Value),
|
|
inc = start > end ? -1 : 1;
|
|
var retVal = new List<string>();
|
|
for (var w = start; w != (end + inc); w += inc)
|
|
{
|
|
// append all the suffixes
|
|
for (var ii = 0; ii < suf.Count; ii++)
|
|
{
|
|
retVal.Add(w.ToString() + suf[ii]);
|
|
}
|
|
}
|
|
return retVal;
|
|
}
|
|
|
|
// ok, walk through the set
|
|
// We hope, somewhat optimistically, that there
|
|
// will be a } at the end.
|
|
// If the closing brace isn't found, then the pattern is
|
|
// interpreted as braceExpand("\\" + pattern) so that
|
|
// the leading \{ will be interpreted literally.
|
|
i = 1; // skip the \{
|
|
int depth = 1;
|
|
var set = new List<string>();
|
|
string member = "";
|
|
|
|
for (i = 1; i < pattern.Length && depth > 0; i++)
|
|
{
|
|
var c = pattern[i];
|
|
// console.error("", i, c)
|
|
|
|
if (escaping)
|
|
{
|
|
escaping = false;
|
|
member += "\\" + c;
|
|
}
|
|
else
|
|
{
|
|
switch (c)
|
|
{
|
|
case '\\':
|
|
escaping = true;
|
|
continue;
|
|
|
|
case '{':
|
|
depth++;
|
|
member += "{";
|
|
continue;
|
|
|
|
case '}':
|
|
depth--;
|
|
// if this closes the actual set, then we're done
|
|
if (depth == 0)
|
|
{
|
|
set.Add(member);
|
|
member = "";
|
|
// pluck off the close-brace
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
member += c;
|
|
continue;
|
|
}
|
|
|
|
case ',':
|
|
if (depth == 1)
|
|
{
|
|
set.Add(member);
|
|
member = "";
|
|
}
|
|
else
|
|
{
|
|
member += c;
|
|
}
|
|
continue;
|
|
|
|
default:
|
|
member += c;
|
|
continue;
|
|
} // switch
|
|
} // else
|
|
} // for
|
|
|
|
// now we've either finished the set, and the suffix is
|
|
// pattern.substr(i), or we have *not* closed the set,
|
|
// and need to escape the leading brace
|
|
if (depth != 0)
|
|
{
|
|
// console.error("didn't close", pattern)
|
|
return BraceExpand("\\" + pattern, options);
|
|
}
|
|
|
|
// ["b", "c{d,e}","{f,g}h"] ->
|
|
// ["b", "cd", "ce", "fh", "gh"]
|
|
var addBraces = set.Count == 1;
|
|
|
|
set = set.SelectMany(p => BraceExpand(p, options)).ToList();
|
|
|
|
if (addBraces)
|
|
set = set.Select(s => "{" + s + "}").ToList();
|
|
// now attach the suffixes.
|
|
// x{y,z} -> ["xy", "xz"]
|
|
// console.error("set", set)
|
|
// console.error("suffix", pattern.substr(i))
|
|
return BraceExpand(pattern.Substring(i), options).SelectMany(s1 => set.Select(s2 => s2 + s1));
|
|
}
|
|
|
|
private class PatternListEntry
|
|
{
|
|
public char Type { get; set; }
|
|
public int Start { get; set; }
|
|
public int ReStart { get; set; }
|
|
}
|
|
|
|
abstract class ParseItem
|
|
{
|
|
public string Source { get; protected set; }
|
|
|
|
public static readonly ParseItem Empty = new LiteralItem("");
|
|
public static ParseItem Literal(string source) { return new LiteralItem(source); }
|
|
public abstract string RegexSource(Options options);
|
|
|
|
public abstract bool Match(string input, Options options);
|
|
}
|
|
class LiteralItem : ParseItem
|
|
{
|
|
public LiteralItem(string source) { Source = source; }
|
|
public override string RegexSource(Options options) { return Regex.Escape(Source); }
|
|
public override bool Match(string input, Options options)
|
|
{
|
|
return input.Equals(Source, options.NoCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
|
|
}
|
|
}
|
|
class MagicItem : ParseItem
|
|
{
|
|
public MagicItem(string source, Options options)
|
|
{
|
|
Source = source;
|
|
regex = new Lazy<Regex>(() => new Regex("^" + source + "$", options.RegexOptions));
|
|
}
|
|
readonly Lazy<Regex> regex;
|
|
|
|
public override string RegexSource(Options options) { return Source; }
|
|
public override bool Match(string input, Options options)
|
|
{
|
|
return regex.Value.IsMatch(input);
|
|
}
|
|
}
|
|
class GlobStar : ParseItem
|
|
{
|
|
private GlobStar() { }
|
|
public static readonly ParseItem Instance = new GlobStar();
|
|
|
|
public override string RegexSource(Options options)
|
|
{
|
|
return options.NoGlobStar ? star
|
|
: options.Dot ? twoStarDot
|
|
: twoStarNoDot;
|
|
}
|
|
|
|
public override bool Match(string input, Options options) { throw new NotSupportedException(); }
|
|
}
|
|
|
|
static readonly Regex escapeCheck = new Regex(@"((?:\\{2})*)(\\?)\|");
|
|
// parse a component of the expanded set.
|
|
// At this point, no pattern may contain "/" in it
|
|
// so we're going to return a 2d array, where each entry is the full
|
|
// pattern, split on '/', and then turned into a regular expression.
|
|
// A regexp is made at the end which joins each array with an
|
|
// escaped /, and another full one which joins each regexp with |.
|
|
//
|
|
// Following the lead of Bash 4.1, note that "**" only has special meaning
|
|
// when it is the *only* thing in a path portion. Otherwise, any series
|
|
// of * is equivalent to a single *. Globstar behavior is enabled by
|
|
// default, and can be disabled by setting options.noglobstar.
|
|
private Tuple<ParseItem, bool> Parse(string pattern, bool isSub)
|
|
{
|
|
// shortcuts
|
|
if (!options.NoGlobStar && pattern == "**") return Tuple.Create(GlobStar.Instance, false);
|
|
if (pattern == "") return Tuple.Create(ParseItem.Empty, false);
|
|
|
|
string re = "";
|
|
bool hasMagic = options.NoCase, escaping = false, inClass = false;
|
|
// ? => one single character
|
|
var patternListStack = new Stack<PatternListEntry>();
|
|
char plType;
|
|
char? stateChar = null;
|
|
|
|
int reClassStart = -1, classStart = -1;
|
|
// . and .. never match anything that doesn't start with .,
|
|
// even when options.dot is set.
|
|
string patternStart = pattern[0] == '.' ? "" // anything
|
|
// not (start or / followed by . or .. followed by / or end)
|
|
: options.Dot ? "(?!(?:^|\\/)\\.{1,2}(?:$|\\/))"
|
|
: "(?!\\.)";
|
|
|
|
Action clearStateChar = () =>
|
|
{
|
|
if (stateChar != null)
|
|
{
|
|
// we had some state-tracking character
|
|
// that wasn't consumed by this pass.
|
|
switch (stateChar)
|
|
{
|
|
case '*':
|
|
re += star;
|
|
hasMagic = true;
|
|
break;
|
|
case '?':
|
|
re += qmark;
|
|
hasMagic = true;
|
|
break;
|
|
default:
|
|
re += "\\" + stateChar;
|
|
break;
|
|
}
|
|
stateChar = null;
|
|
}
|
|
};
|
|
|
|
for (var i = 0; i < pattern.Length; i++)
|
|
{
|
|
var c = pattern[i];
|
|
//if (options.debug) {
|
|
// console.error("%s\t%s %s %j", pattern, i, re, c)
|
|
//}
|
|
|
|
// skip over any that are escaped.
|
|
if (escaping && reSpecials.Contains(c))
|
|
{
|
|
re += "\\" + c;
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
switch (c)
|
|
{
|
|
case '/':
|
|
// completely not allowed, even escaped.
|
|
// Should already be path-split by now.
|
|
return null;
|
|
|
|
case '\\':
|
|
clearStateChar();
|
|
escaping = true;
|
|
continue;
|
|
|
|
// the various stateChar values
|
|
// for the 'extglob' stuff.
|
|
case '?':
|
|
case '*':
|
|
case '+':
|
|
case '@':
|
|
case '!':
|
|
//if (options.debug) {
|
|
// console.error("%s\t%s %s %j <-- stateChar", pattern, i, re, c)
|
|
//}
|
|
|
|
// all of those are literals inside a class, except that
|
|
// the glob [!a] means [^a] in regexp
|
|
if (inClass)
|
|
{
|
|
if (c == '!' && i == classStart + 1) c = '^';
|
|
re += c;
|
|
continue;
|
|
}
|
|
|
|
// if we already have a stateChar, then it means
|
|
// that there was something like ** or +? in there.
|
|
// Handle the stateChar, then proceed with this one.
|
|
clearStateChar();
|
|
stateChar = c;
|
|
// if extglob is disabled, then +(asdf|foo) isn't a thing.
|
|
// just clear the statechar *now*, rather than even diving into
|
|
// the patternList stuff.
|
|
if (options.NoExt) clearStateChar();
|
|
continue;
|
|
|
|
case '(':
|
|
if (inClass)
|
|
{
|
|
re += "(";
|
|
continue;
|
|
}
|
|
|
|
if (stateChar == null)
|
|
{
|
|
re += "\\(";
|
|
continue;
|
|
}
|
|
|
|
plType = stateChar.Value;
|
|
patternListStack.Push(new PatternListEntry { Type = plType, Start = i - 1, ReStart = re.Length });
|
|
// negation is (?:(?!js)[^/]*)
|
|
re += stateChar == '!' ? "(?:(?!" : "(?:";
|
|
stateChar = null;
|
|
continue;
|
|
|
|
case ')':
|
|
if (inClass || !patternListStack.Any())
|
|
{
|
|
re += "\\)";
|
|
continue;
|
|
}
|
|
|
|
hasMagic = true;
|
|
re += ')';
|
|
plType = patternListStack.Pop().Type;
|
|
// negation is (?:(?!js)[^/]*)
|
|
// The others are (?:<pattern>)<type>
|
|
switch (plType)
|
|
{
|
|
case '!':
|
|
re += "[^/]*?)";
|
|
break;
|
|
case '?':
|
|
case '+':
|
|
case '*': re += plType; break;
|
|
case '@': break; // the default anyway
|
|
}
|
|
continue;
|
|
|
|
case '|':
|
|
if (inClass || !patternListStack.Any() || escaping)
|
|
{
|
|
re += "\\|";
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
re += "|";
|
|
continue;
|
|
|
|
// these are mostly the same in regexp and glob
|
|
case '[':
|
|
// swallow any state-tracking char before the [
|
|
clearStateChar();
|
|
|
|
if (inClass)
|
|
{
|
|
re += "\\" + c;
|
|
continue;
|
|
}
|
|
|
|
inClass = true;
|
|
classStart = i;
|
|
reClassStart = re.Length;
|
|
re += c;
|
|
continue;
|
|
|
|
case ']':
|
|
// a right bracket shall lose its special
|
|
// meaning and represent itself in
|
|
// a bracket expression if it occurs
|
|
// first in the list. -- POSIX.2 2.8.3.2
|
|
if (i == classStart + 1 || !inClass)
|
|
{
|
|
re += "\\" + c;
|
|
escaping = false;
|
|
continue;
|
|
}
|
|
|
|
// finish up the class.
|
|
hasMagic = true;
|
|
inClass = false;
|
|
re += c;
|
|
continue;
|
|
|
|
default:
|
|
// swallow any state char that wasn't consumed
|
|
clearStateChar();
|
|
|
|
if (escaping)
|
|
{
|
|
// no need
|
|
escaping = false;
|
|
}
|
|
else if (reSpecials.Contains(c) && !(c == '^' && inClass))
|
|
{
|
|
re += "\\";
|
|
}
|
|
|
|
re += c;
|
|
break;
|
|
} // switch
|
|
} // for
|
|
|
|
|
|
// handle the case where we left a class open.
|
|
// "[abc" is valid, equivalent to "\[abc"
|
|
if (inClass)
|
|
{
|
|
// split where the last [ was, and escape it
|
|
// this is a huge pita. We now have to re-walk
|
|
// the contents of the would-be class to re-translate
|
|
// any characters that were passed through as-is
|
|
string cs = pattern.Substring(classStart + 1);
|
|
var sp = this.Parse(cs, true);
|
|
re = re.Substring(0, reClassStart) + "\\[" + sp.Item1.Source;
|
|
hasMagic = hasMagic || sp.Item2;
|
|
}
|
|
|
|
// handle the case where we had a +( thing at the *end*
|
|
// of the pattern.
|
|
// each pattern list stack adds 3 chars, and we need to go through
|
|
// and escape any | chars that were passed through as-is for the regexp.
|
|
// Go through and escape them, taking care not to double-escape any
|
|
// | chars that were already escaped.
|
|
while (patternListStack.Any())
|
|
{
|
|
var pl = patternListStack.Pop();
|
|
var tail = re.Substring(pl.ReStart + 3);
|
|
// maybe some even number of \, then maybe 1 \, followed by a |
|
|
tail = escapeCheck.Replace(tail, m =>
|
|
{
|
|
string escape = m.Groups[2].Value;
|
|
// the | isn't already escaped, so escape it.
|
|
if (String.IsNullOrEmpty(escape)) escape = "\\";
|
|
|
|
// need to escape all those slashes *again*, without escaping the
|
|
// one that we need for escaping the | character. As it works out,
|
|
// escaping an even number of slashes can be done by simply repeating
|
|
// it exactly after itself. That's why this trick works.
|
|
//
|
|
// I am sorry that you have to see this.
|
|
return m.Groups[1].Value + m.Groups[1].Value + escape + "|";
|
|
});
|
|
|
|
// console.error("tail=%j\n %s", tail, tail)
|
|
var t = pl.Type == '*' ? star
|
|
: pl.Type == '?' ? qmark
|
|
: "\\" + pl.Type;
|
|
|
|
hasMagic = true;
|
|
re = re.Remove(pl.ReStart)
|
|
+ t + "\\("
|
|
+ tail;
|
|
}
|
|
|
|
// handle trailing things that only matter at the very end.
|
|
clearStateChar();
|
|
if (escaping)
|
|
{
|
|
// trailing \\
|
|
re += "\\\\";
|
|
}
|
|
|
|
// only need to apply the nodot start if the re starts with
|
|
// something that could conceivably capture a dot
|
|
var addPatternStart = false;
|
|
switch (re[0])
|
|
{
|
|
case '.':
|
|
case '[':
|
|
case '(': addPatternStart = true; break;
|
|
}
|
|
|
|
// if the re is not "" at this point, then we need to make sure
|
|
// it doesn't match against an empty path part.
|
|
// Otherwise a/* will match a/, which it should not.
|
|
if (re != "" && hasMagic) re = "(?=.)" + re;
|
|
|
|
if (addPatternStart) re = patternStart + re;
|
|
|
|
// parsing just a piece of a larger pattern.
|
|
if (isSub)
|
|
{
|
|
return Tuple.Create(ParseItem.Literal(re), hasMagic);
|
|
}
|
|
|
|
// skip the regexp for non-magical patterns
|
|
// unescape anything in it, though, so that it'll be
|
|
// an exact match against a file etc.
|
|
if (!hasMagic)
|
|
{
|
|
return Tuple.Create(ParseItem.Literal(GlobUnescape(pattern)), false);
|
|
}
|
|
return new Tuple<ParseItem, bool>(new MagicItem(re, options), false);
|
|
}
|
|
|
|
|
|
Regex MakeRegex()
|
|
{
|
|
if (this.regexp != null || isError) return this.regexp;
|
|
|
|
// at this point, this.set is a 2d array of partial
|
|
// pattern strings, or "**".
|
|
//
|
|
// It's better to use .match(). This function shouldn't
|
|
// be used, really, but it's pretty convenient sometimes,
|
|
// when you just want to work with a regex.
|
|
if (comment || empty || !set.Any())
|
|
{
|
|
this.isError = true;
|
|
return null;
|
|
}
|
|
var re = String.Join("|", set.Select(pattern =>
|
|
String.Join("\\/", pattern.Select(p => p.RegexSource(options))
|
|
)));
|
|
|
|
// must match entire pattern
|
|
// ending in a * or ** will make it less strict.
|
|
re = "^(?:" + re + ")$";
|
|
|
|
// can match anything, as long as it's not this.
|
|
if (this.negate) re = "^(?!" + re + ").*$";
|
|
|
|
try
|
|
{
|
|
return this.regexp = new Regex(re, options.RegexOptions);
|
|
}
|
|
catch
|
|
{
|
|
this.isError = true;
|
|
return null;
|
|
}
|
|
}
|
|
|
|
|
|
private bool Match(string input, bool partial)
|
|
{
|
|
// console.error("match", f, this.pattern)
|
|
// short-circuit in the case of busted things.
|
|
// comments, etc.
|
|
if (this.comment) return false;
|
|
if (this.empty) return input == "";
|
|
|
|
if (input == "/" && partial) return true;
|
|
|
|
// windows: need to use /, not \
|
|
// On other platforms, \ is a valid (albeit bad) filename char.
|
|
|
|
if (options.AllowWindowsPaths)
|
|
input = input.Replace("\\", "/");
|
|
|
|
// treat the test path as a set of pathparts.
|
|
var f = slashSplit.Split(input);
|
|
//if (options.debug) {
|
|
// console.error(this.pattern, "split", f)
|
|
//}
|
|
|
|
// just ONE of the pattern sets in this.set needs to match
|
|
// in order for it to be valid. If negating, then just one
|
|
// match means that we have failed.
|
|
// Either way, return on the first hit.
|
|
|
|
foreach (var pattern in set)
|
|
{
|
|
var hit = this.MatchOne(f, pattern.ToList(), partial);
|
|
if (hit)
|
|
{
|
|
if (options.FlipNegate) return true;
|
|
return !this.negate;
|
|
}
|
|
|
|
}
|
|
|
|
// didn't get any hits. this is success if it's a negative
|
|
// pattern, failure otherwise.
|
|
if (options.FlipNegate) return false;
|
|
return this.negate;
|
|
}
|
|
|
|
// set partial to true to test if, for example,
|
|
// "/a/b" matches the start of "/*/b/*/d"
|
|
// Partial means, if you run out of file before you run
|
|
// out of pattern, then that's fine, as long as all
|
|
// the parts match.
|
|
bool MatchOne(IList<string> file, IList<ParseItem> pattern, bool partial)
|
|
{
|
|
|
|
//if (options.debug) {
|
|
// console.error("matchOne",
|
|
// { "this": this
|
|
// , file: file
|
|
// , pattern: pattern })
|
|
//}
|
|
|
|
if (options.MatchBase && pattern.Count == 1)
|
|
{
|
|
file = new[] { file.Last(s => !String.IsNullOrEmpty(s)) };
|
|
}
|
|
|
|
//if (options.debug) {
|
|
// console.error("matchOne", file.length, pattern.length)
|
|
//}
|
|
int fi = 0, pi = 0;
|
|
for (; (fi < file.Count) && (pi < pattern.Count); fi++, pi++)
|
|
{
|
|
|
|
//if (options.debug) {
|
|
// console.error("matchOne loop")
|
|
//}
|
|
ParseItem p = pattern[pi];
|
|
string f = file[fi];
|
|
|
|
//if (options.debug) {
|
|
// console.error(pattern, p, f)
|
|
//}
|
|
|
|
// should be impossible.
|
|
// some invalid regexp stuff in the set.
|
|
if (p == null) return false;
|
|
|
|
if (p is GlobStar)
|
|
{
|
|
//if (options.debug)
|
|
// console.error('GLOBSTAR', [pattern, p, f])
|
|
|
|
// "**"
|
|
// a/**/b/**/c would match the following:
|
|
// a/b/x/y/z/c
|
|
// a/x/y/z/b/c
|
|
// a/b/x/b/x/c
|
|
// a/b/c
|
|
// To do this, take the rest of the pattern after
|
|
// the **, and see if it would match the file remainder.
|
|
// If so, return success.
|
|
// If not, the ** "swallows" a segment, and try again.
|
|
// This is recursively awful.
|
|
//
|
|
// a/**/b/**/c matching a/b/x/y/z/c
|
|
// - a matches a
|
|
// - doublestar
|
|
// - matchOne(b/x/y/z/c, b/**/c)
|
|
// - b matches b
|
|
// - doublestar
|
|
// - matchOne(x/y/z/c, c) -> no
|
|
// - matchOne(y/z/c, c) -> no
|
|
// - matchOne(z/c, c) -> no
|
|
// - matchOne(c, c) yes, hit
|
|
int fr = fi, pr = pi + 1;
|
|
if (pr == pattern.Count)
|
|
{
|
|
//if (options.debug)
|
|
// console.error('** at the end')
|
|
// a ** at the end will just swallow the rest.
|
|
// We have found a match.
|
|
// however, it will not swallow /.x, unless
|
|
// options.dot is set.
|
|
// . and .. are *never* matched by **, for explosively
|
|
// exponential reasons.
|
|
for (; fi < file.Count; fi++)
|
|
{
|
|
if (file[fi] == "." || file[fi] == ".." ||
|
|
(!options.Dot && !string.IsNullOrEmpty(file[fi]) && file[fi][0] == '.')) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
// ok, let's see if we can swallow whatever we can.
|
|
while (fr < file.Count)
|
|
{
|
|
var swallowee = file[fr];
|
|
|
|
//if (options.debug) {
|
|
// console.error('\nglobstar while',
|
|
// file, fr, pattern, pr, swallowee)
|
|
//}
|
|
|
|
// XXX remove this slice. Just pass the start index.
|
|
if (this.MatchOne(file.Skip(fr).ToList(), pattern.Skip(pr).ToList(), partial))
|
|
{
|
|
//if (options.debug)
|
|
// console.error('globstar found match!', fr, file.Count, swallowee)
|
|
// found a match.
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// can't swallow "." or ".." ever.
|
|
// can only swallow ".foo" when explicitly asked.
|
|
if (swallowee == "." || swallowee == ".." ||
|
|
(!options.Dot && swallowee[0] == '.'))
|
|
{
|
|
//if (options.debug)
|
|
// console.error("dot detected!", file, fr, pattern, pr)
|
|
break;
|
|
}
|
|
|
|
// ** swallows a segment, and continue.
|
|
//if (options.debug)
|
|
// console.error('globstar swallow a segment, and continue')
|
|
fr++;
|
|
}
|
|
}
|
|
// no match was found.
|
|
// However, in partial mode, we can't say this is necessarily over.
|
|
// If there's more *pattern* left, then
|
|
if (partial)
|
|
{
|
|
// ran out of file
|
|
// console.error("\n>>> no match, partial?", file, fr, pattern, pr)
|
|
if (fr == file.Count) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// something other than **
|
|
// non-magic patterns just have to match exactly
|
|
// patterns with magic have been turned into regexps.
|
|
if (!p.Match(f, options))
|
|
return false;
|
|
}
|
|
|
|
// Note: ending in / means that we'll get a final ""
|
|
// at the end of the pattern. This can only match a
|
|
// corresponding "" at the end of the file.
|
|
// If the file ends in /, then it can only match a
|
|
// a pattern that ends in /, unless the pattern just
|
|
// doesn't have any more for it. But, a/b/ should *not*
|
|
// match "a/b/*", even though "" matches against the
|
|
// [^/]*? pattern, except in partial mode, where it might
|
|
// simply not be reached yet.
|
|
// However, a/b/ should still satisfy a/*
|
|
|
|
// now either we fell off the end of the pattern, or we're done.
|
|
if (fi == file.Count && pi == pattern.Count)
|
|
{
|
|
// ran out of pattern and filename at the same time.
|
|
// an exact hit!
|
|
return true;
|
|
}
|
|
else if (fi == file.Count)
|
|
{
|
|
// ran out of file, but still had pattern left.
|
|
// this is ok if we're doing the match as part of
|
|
// a glob fs traversal.
|
|
return partial;
|
|
}
|
|
else if (pi == pattern.Count)
|
|
{
|
|
// ran out of pattern, still have file left.
|
|
// this is only acceptable if we're on the very last
|
|
// empty segment of a file with a trailing slash.
|
|
// a/* should match a/b/
|
|
var emptyFileEnd = (fi == file.Count - 1) && (file[fi] == "");
|
|
return emptyFileEnd;
|
|
}
|
|
|
|
// should be unreachable.
|
|
throw new InvalidOperationException("wtf?");
|
|
}
|
|
|
|
|
|
// replace stuff like \* with *
|
|
static readonly Regex globUnescaper = new Regex(@"\\(.)");
|
|
static string GlobUnescape(string s)
|
|
{
|
|
return globUnescaper.Replace(s, "$1");
|
|
}
|
|
}
|
|
}
|