diff --git a/Mono.Addins.Setup/Mono.Addins.Setup/SetupService.cs b/Mono.Addins.Setup/Mono.Addins.Setup/SetupService.cs index 3c81f8c3..6b0e7858 100644 --- a/Mono.Addins.Setup/Mono.Addins.Setup/SetupService.cs +++ b/Mono.Addins.Setup/Mono.Addins.Setup/SetupService.cs @@ -361,6 +361,8 @@ string BuildPackageInternal (IProgressStatus monitor, string targetDirectory, st if (targetDirectory == null) targetDirectory = basePath; + conf.SetBasePath (basePath); + // Generate the file name string name; diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/FilePatternMatch.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/FilePatternMatch.cs new file mode 100644 index 00000000..595cd619 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/FilePatternMatch.cs @@ -0,0 +1,78 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.Internal; + +namespace Microsoft.Extensions.FileSystemGlobbing +{ + /// + /// Represents a file that was matched by searching using a globbing pattern + /// + struct FilePatternMatch : IEquatable + { + /// + /// The path to the file matched + /// + /// + /// If the matcher searched for "**/*.cs" using "src/Project" as the directory base and the pattern matcher found + /// "src/Project/Interfaces/IFile.cs", then Stem = "Interfaces/IFile.cs" and Path = "src/Project/Interfaces/IFile.cs". + /// + public string Path { get; } + + /// + /// The subpath to the matched file under the base directory searched + /// + /// + /// If the matcher searched for "**/*.cs" using "src/Project" as the directory base and the pattern matcher found + /// "src/Project/Interfaces/IFile.cs", + /// then Stem = "Interfaces/IFile.cs" and Path = "src/Project/Interfaces/IFile.cs". + /// + public string Stem { get; } + + /// + /// Initializes new instance of + /// + /// The path to the matched file + /// The stem + public FilePatternMatch(string path, string stem) + { + Path = path; + Stem = stem; + } + + /// + /// Determines if the specified match is equivalent to the current match using a case-insensitive comparison. + /// + /// The other match to be compared + /// True if and are equal using case-insensitive comparison + public bool Equals(FilePatternMatch other) + { + return string.Equals(other.Path, Path, StringComparison.OrdinalIgnoreCase) && + string.Equals(other.Stem, Stem, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Determines if the specified object is equivalent to the current match using a case-insensitive comparison. + /// + /// The object to be compared + /// True when + public override bool Equals(object obj) + { + return Equals((FilePatternMatch) obj); + } + + /// + /// Gets a hash for the file pattern match. + /// + /// Some number + public override int GetHashCode() + { + var hashCodeCombiner = HashCodeCombiner.Start(); + hashCodeCombiner.Add(Path, StringComparer.OrdinalIgnoreCase); + hashCodeCombiner.Add(Stem, StringComparer.OrdinalIgnoreCase); + + return hashCodeCombiner; + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/HashCodeCombiner.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/HashCodeCombiner.cs new file mode 100644 index 00000000..477da52f --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/HashCodeCombiner.cs @@ -0,0 +1,84 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections; +using System.Collections.Generic; +using System.Runtime.CompilerServices; + +namespace Microsoft.Extensions.Internal +{ + struct HashCodeCombiner + { + private long _combinedHash64; + + public int CombinedHash + { + [MethodImpl(MethodImplOptions.AggressiveInlining)] + get { return _combinedHash64.GetHashCode(); } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private HashCodeCombiner(long seed) + { + _combinedHash64 = seed; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(IEnumerable e) + { + if (e == null) + { + Add(0); + } + else + { + var count = 0; + foreach (object o in e) + { + Add(o); + count++; + } + Add(count); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static implicit operator int(HashCodeCombiner self) + { + return self.CombinedHash; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(int i) + { + _combinedHash64 = ((_combinedHash64 << 5) + _combinedHash64) ^ i; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(string s) + { + var hashCode = (s != null) ? s.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(object o) + { + var hashCode = (o != null) ? o.GetHashCode() : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void Add(TValue value, IEqualityComparer comparer) + { + var hashCode = value != null ? comparer.GetHashCode(value) : 0; + Add(hashCode); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static HashCodeCombiner Start() + { + return new HashCodeCombiner(0x1505L); + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/ILinearPattern.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/ILinearPattern.cs new file mode 100644 index 00000000..a110b1e5 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/ILinearPattern.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + interface ILinearPattern : IPattern + { + IList Segments { get; } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPathSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPathSegment.cs new file mode 100644 index 00000000..f80a5b4b --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPathSegment.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + interface IPathSegment + { + bool CanProduceStem { get; } + + bool Match(string value); + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPattern.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPattern.cs new file mode 100644 index 00000000..fb8b2878 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPattern.cs @@ -0,0 +1,12 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + interface IPattern + { + IPatternContext CreatePatternContextForInclude(); + + IPatternContext CreatePatternContextForExclude(); + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPatternContext.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPatternContext.cs new file mode 100644 index 00000000..8268bd1c --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IPatternContext.cs @@ -0,0 +1,22 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.Extensions.FileSystemGlobbing; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + interface IPatternContext + { + void Declare(Action onDeclare); + + bool Test(DirectoryInfo directory); + + PatternTestResult Test(FileInfo file); + + void PushDirectory(DirectoryInfo directory); + + void PopDirectory(); + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IRaggedPattern.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IRaggedPattern.cs new file mode 100644 index 00000000..ed443727 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/IRaggedPattern.cs @@ -0,0 +1,18 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + interface IRaggedPattern : IPattern + { + IList Segments { get; } + + IList StartsWith { get; } + + IList> Contains { get; } + + IList EndsWith { get; } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/MatcherContext.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/MatcherContext.cs new file mode 100644 index 00000000..6b610011 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/MatcherContext.cs @@ -0,0 +1,251 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; +using Microsoft.Extensions.FileSystemGlobbing.Util; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + class MatcherContext + { + private readonly DirectoryInfo _root; + private readonly List _includePatternContexts; + private readonly List _excludePatternContexts; + private readonly List _files; + + private readonly HashSet _declaredLiteralFolderSegmentInString; + private readonly HashSet _declaredLiteralFolderSegments = new HashSet(); + private readonly HashSet _declaredLiteralFileSegments = new HashSet(); + + private bool _declaredParentPathSegment; + private bool _declaredWildcardPathSegment; + + private readonly StringComparison _comparisonType; + + public MatcherContext( + IEnumerable includePatterns, + IEnumerable excludePatterns, + DirectoryInfo directoryInfo, + StringComparison comparison) + { + _root = directoryInfo; + _files = new List(); + _comparisonType = comparison; + + _includePatternContexts = includePatterns.Select(pattern => pattern.CreatePatternContextForInclude()).ToList(); + _excludePatternContexts = excludePatterns.Select(pattern => pattern.CreatePatternContextForExclude()).ToList(); + + _declaredLiteralFolderSegmentInString = new HashSet(StringComparisonHelper.GetStringComparer(comparison)); + } + + public PatternMatchingResult Execute() + { + _files.Clear(); + + Match(_root, parentRelativePath: null); + + return new PatternMatchingResult(_files, _files.Count > 0); + } + + private void Match(DirectoryInfo directory, string parentRelativePath) + { + // Request all the including and excluding patterns to push current directory onto their status stack. + PushDirectory(directory); + Declare(); + + var entities = new List(); + if (_declaredWildcardPathSegment || _declaredLiteralFileSegments.Any()) + { + entities.AddRange(directory.EnumerateFileSystemInfos()); + } + else + { + var candidates = directory.EnumerateFileSystemInfos().OfType(); + foreach (var candidate in candidates) + { + if (_declaredLiteralFolderSegmentInString.Contains(candidate.Name)) + { + entities.Add(candidate); + } + } + } + + if (_declaredParentPathSegment) + { + entities.Add(directory.Parent); + } + + // collect files and sub directories + var subDirectories = new List(); + foreach (var entity in entities) + { + var fileInfo = entity as FileInfo; + if (fileInfo != null) + { + var result = MatchPatternContexts(fileInfo, (pattern, file) => pattern.Test(file)); + if (result.IsSuccessful) + { + _files.Add(new FilePatternMatch( + path: CombinePath(parentRelativePath, fileInfo.Name), + stem: result.Stem)); + } + + continue; + } + + var directoryInfo = entity as DirectoryInfo; + if (directoryInfo != null) + { + if (MatchPatternContexts(directoryInfo, (pattern, dir) => pattern.Test(dir))) + { + subDirectories.Add(directoryInfo); + } + + continue; + } + } + + // Matches the sub directories recursively + foreach (var subDir in subDirectories) + { + var relativePath = CombinePath(parentRelativePath, subDir.Name); + + Match(subDir, relativePath); + } + + // Request all the including and excluding patterns to pop their status stack. + PopDirectory(); + } + + private void Declare() + { + _declaredLiteralFileSegments.Clear(); + _declaredLiteralFolderSegments.Clear(); + _declaredParentPathSegment = false; + _declaredWildcardPathSegment = false; + + foreach (var include in _includePatternContexts) + { + include.Declare(DeclareInclude); + } + } + + private void DeclareInclude(IPathSegment patternSegment, bool isLastSegment) + { + var literalSegment = patternSegment as LiteralPathSegment; + if (literalSegment != null) + { + if (isLastSegment) + { + _declaredLiteralFileSegments.Add(literalSegment); + } + else + { + _declaredLiteralFolderSegments.Add(literalSegment); + _declaredLiteralFolderSegmentInString.Add(literalSegment.Value); + } + } + else if (patternSegment is ParentPathSegment) + { + _declaredParentPathSegment = true; + } + else if (patternSegment is WildcardPathSegment) + { + _declaredWildcardPathSegment = true; + } + } + + internal static string CombinePath(string left, string right) + { + if (string.IsNullOrEmpty(left)) + { + return right; + } + else + { + return string.Format("{0}/{1}", left, right); + } + } + + // Used to adapt Test(DirectoryInfoBase) for the below overload + private bool MatchPatternContexts(TFileInfoBase fileinfo, Func test) + { + return MatchPatternContexts( + fileinfo, + (ctx, file) => + { + if (test(ctx, file)) + { + return PatternTestResult.Success(stem: string.Empty); + } + else + { + return PatternTestResult.Failed; + } + }).IsSuccessful; + } + + private PatternTestResult MatchPatternContexts(TFileInfoBase fileinfo, Func test) + { + var result = PatternTestResult.Failed; + + // If the given file/directory matches any including pattern, continues to next step. + foreach (var context in _includePatternContexts) + { + var localResult = test(context, fileinfo); + if (localResult.IsSuccessful) + { + result = localResult; + break; + } + } + + // If the given file/directory doesn't match any of the including pattern, returns false. + if (!result.IsSuccessful) + { + return PatternTestResult.Failed; + } + + // If the given file/directory matches any excluding pattern, returns false. + foreach (var context in _excludePatternContexts) + { + if (test(context, fileinfo).IsSuccessful) + { + return PatternTestResult.Failed; + } + } + + return result; + } + + private void PopDirectory() + { + foreach (var context in _excludePatternContexts) + { + context.PopDirectory(); + } + + foreach (var context in _includePatternContexts) + { + context.PopDirectory(); + } + } + + private void PushDirectory(DirectoryInfo directory) + { + foreach (var context in _includePatternContexts) + { + context.PushDirectory(directory); + } + + foreach (var context in _excludePatternContexts) + { + context.PushDirectory(directory); + } + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs new file mode 100644 index 00000000..d55537de --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/CurrentPathSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments +{ + class CurrentPathSegment : IPathSegment + { + public bool CanProduceStem { get { return false; } } + + public bool Match(string value) + { + return false; + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs new file mode 100644 index 00000000..644abd3c --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/LiteralPathSegment.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.FileSystemGlobbing.Util; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments +{ + class LiteralPathSegment : IPathSegment + { + private readonly StringComparison _comparisonType; + + public bool CanProduceStem { get { return false; } } + + public LiteralPathSegment(string value, StringComparison comparisonType) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + Value = value; + + _comparisonType = comparisonType; + } + + public string Value { get; } + + public bool Match(string value) + { + return string.Equals(Value, value, _comparisonType); + } + + public override bool Equals(object obj) + { + var other = obj as LiteralPathSegment; + + return other != null && + _comparisonType == other._comparisonType && + string.Equals(other.Value, Value, _comparisonType); + } + + public override int GetHashCode() + { + return StringComparisonHelper.GetStringComparer(_comparisonType).GetHashCode(Value); + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs new file mode 100644 index 00000000..4f7c85b0 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/ParentPathSegment.cs @@ -0,0 +1,19 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments +{ + class ParentPathSegment : IPathSegment + { + private static readonly string LiteralParent = ".."; + + public bool CanProduceStem { get { return false; } } + + public bool Match(string value) + { + return string.Equals(LiteralParent, value, StringComparison.Ordinal); + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs new file mode 100644 index 00000000..a8cb411f --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/RecursiveWildcardSegment.cs @@ -0,0 +1,17 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments +{ + class RecursiveWildcardSegment : IPathSegment + { + public bool CanProduceStem { get { return true; } } + + public bool Match(string value) + { + return false; + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs new file mode 100644 index 00000000..417a8474 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PathSegments/WildcardPathSegment.cs @@ -0,0 +1,74 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments +{ + class WildcardPathSegment : IPathSegment + { + // It doesn't matter which StringComparison type is used in this MatchAll segment because + // all comparing are skipped since there is no content in the segment. + public static readonly WildcardPathSegment MatchAll = new WildcardPathSegment( + string.Empty, new List(), string.Empty, StringComparison.OrdinalIgnoreCase); + + private readonly StringComparison _comparisonType; + + public WildcardPathSegment(string beginsWith, List contains, string endsWith, StringComparison comparisonType) + { + BeginsWith = beginsWith; + Contains = contains; + EndsWith = endsWith; + _comparisonType = comparisonType; + } + + public bool CanProduceStem { get { return true; } } + + public string BeginsWith { get; } + + public List Contains { get; } + + public string EndsWith { get; } + + public bool Match(string value) + { + var wildcard = this; + + if (value.Length < wildcard.BeginsWith.Length + wildcard.EndsWith.Length) + { + return false; + } + + if (!value.StartsWith(wildcard.BeginsWith, _comparisonType)) + { + return false; + } + + if (!value.EndsWith(wildcard.EndsWith, _comparisonType)) + { + return false; + } + + var beginRemaining = wildcard.BeginsWith.Length; + var endRemaining = value.Length - wildcard.EndsWith.Length; + for (var containsIndex = 0; containsIndex != wildcard.Contains.Count; ++containsIndex) + { + var containsValue = wildcard.Contains[containsIndex]; + var indexOf = value.IndexOf( + value: containsValue, + startIndex: beginRemaining, + count: endRemaining - beginRemaining, + comparisonType: _comparisonType); + if (indexOf == -1) + { + return false; + } + + beginRemaining = indexOf + containsValue.Length; + } + + return true; + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternBuilder.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternBuilder.cs new file mode 100644 index 00000000..067e9400 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternBuilder.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; +using Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.Patterns +{ + class PatternBuilder + { + private static readonly char[] _slashes = new[] { '/', '\\' }; + private static readonly char[] _star = new[] { '*' }; + + public PatternBuilder() + { + ComparisonType = StringComparison.OrdinalIgnoreCase; + } + + public PatternBuilder(StringComparison comparisonType) + { + ComparisonType = comparisonType; + } + + public StringComparison ComparisonType { get; } + + public IPattern Build(string pattern) + { + if (pattern == null) + { + throw new ArgumentNullException("pattern"); + } + + pattern = pattern.TrimStart(_slashes); + + if (pattern.TrimEnd(_slashes).Length < pattern.Length) + { + // If the pattern end with a slash, it is considered as + // a directory. + pattern = pattern.TrimEnd(_slashes) + "/**"; + } + + var allSegments = new List(); + var isParentSegmentLegal = true; + + IList segmentsPatternStartsWith = null; + IList> segmentsPatternContains = null; + IList segmentsPatternEndsWith = null; + + var endPattern = pattern.Length; + for (int scanPattern = 0; scanPattern < endPattern;) + { + var beginSegment = scanPattern; + var endSegment = NextIndex(pattern, _slashes, scanPattern, endPattern); + + IPathSegment segment = null; + + if (segment == null && endSegment - beginSegment == 3) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '.' && + pattern[beginSegment + 2] == '*') + { + // turn *.* into * + beginSegment += 2; + } + } + + if (segment == null && endSegment - beginSegment == 2) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '*') + { + // recognized ** + segment = new RecursiveWildcardSegment(); + } + else if (pattern[beginSegment] == '.' && + pattern[beginSegment + 1] == '.') + { + // recognized .. + + if (!isParentSegmentLegal) + { + throw new ArgumentException("\"..\" can be only added at the beginning of the pattern."); + } + segment = new ParentPathSegment(); + } + } + + if (segment == null && endSegment - beginSegment == 1) + { + if (pattern[beginSegment] == '.') + { + // recognized . + segment = new CurrentPathSegment(); + } + } + + if (segment == null && endSegment - beginSegment > 2) + { + if (pattern[beginSegment] == '*' && + pattern[beginSegment + 1] == '*' && + pattern[beginSegment + 2] == '.') + { + // recognize **. + // swallow the first *, add the recursive path segment and + // the remaining part will be treat as wild card in next loop. + segment = new RecursiveWildcardSegment(); + endSegment = beginSegment; + } + } + + if (segment == null) + { + var beginsWith = string.Empty; + var contains = new List(); + var endsWith = string.Empty; + + for (int scanSegment = beginSegment; scanSegment < endSegment;) + { + var beginLiteral = scanSegment; + var endLiteral = NextIndex(pattern, _star, scanSegment, endSegment); + + if (beginLiteral == beginSegment) + { + if (endLiteral == endSegment) + { + // and the only bit + segment = new LiteralPathSegment(Portion(pattern, beginLiteral, endLiteral), ComparisonType); + } + else + { + // this is the first bit + beginsWith = Portion(pattern, beginLiteral, endLiteral); + } + } + else if (endLiteral == endSegment) + { + // this is the last bit + endsWith = Portion(pattern, beginLiteral, endLiteral); + } + else + { + if (beginLiteral != endLiteral) + { + // this is a middle bit + contains.Add(Portion(pattern, beginLiteral, endLiteral)); + } + else + { + // note: NOOP here, adjacent *'s are collapsed when they + // are mixed with literal text in a path segment + } + } + + scanSegment = endLiteral + 1; + } + + if (segment == null) + { + segment = new WildcardPathSegment(beginsWith, contains, endsWith, ComparisonType); + } + } + + if (!(segment is ParentPathSegment)) + { + isParentSegmentLegal = false; + } + + if (segment is CurrentPathSegment) + { + // ignore ".\" + } + else + { + if (segment is RecursiveWildcardSegment) + { + if (segmentsPatternStartsWith == null) + { + segmentsPatternStartsWith = new List(allSegments); + segmentsPatternEndsWith = new List(); + segmentsPatternContains = new List>(); + } + else if (segmentsPatternEndsWith.Count != 0) + { + segmentsPatternContains.Add(segmentsPatternEndsWith); + segmentsPatternEndsWith = new List(); + } + } + else if (segmentsPatternEndsWith != null) + { + segmentsPatternEndsWith.Add(segment); + } + + allSegments.Add(segment); + } + + scanPattern = endSegment + 1; + } + + if (segmentsPatternStartsWith == null) + { + return new LinearPattern(allSegments); + } + else + { + return new RaggedPattern(allSegments, segmentsPatternStartsWith, segmentsPatternEndsWith, segmentsPatternContains); + } + } + + private static int NextIndex(string pattern, char[] anyOf, int beginIndex, int endIndex) + { + var index = pattern.IndexOfAny(anyOf, beginIndex, endIndex - beginIndex); + return index == -1 ? endIndex : index; + } + + private static string Portion(string pattern, int beginIndex, int endIndex) + { + return pattern.Substring(beginIndex, endIndex - beginIndex); + } + + private class LinearPattern : ILinearPattern + { + public LinearPattern(List allSegments) + { + Segments = allSegments; + } + + public IList Segments { get; } + + public IPatternContext CreatePatternContextForInclude() + { + return new PatternContextLinearInclude(this); + } + + public IPatternContext CreatePatternContextForExclude() + { + return new PatternContextLinearExclude(this); + } + } + + private class RaggedPattern : IRaggedPattern + { + public RaggedPattern(List allSegments, IList segmentsPatternStartsWith, IList segmentsPatternEndsWith, IList> segmentsPatternContains) + { + Segments = allSegments; + StartsWith = segmentsPatternStartsWith; + Contains = segmentsPatternContains; + EndsWith = segmentsPatternEndsWith; + } + + public IList> Contains { get; } + + public IList EndsWith { get; } + + public IList Segments { get; } + + public IList StartsWith { get; } + + public IPatternContext CreatePatternContextForInclude() + { + return new PatternContextRaggedInclude(this); + } + + public IPatternContext CreatePatternContextForExclude() + { + return new PatternContextRaggedExclude(this); + } + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs new file mode 100644 index 00000000..702fec30 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContext.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + abstract class PatternContext : IPatternContext + { + private Stack _stack = new Stack(); + protected TFrame Frame; + + public virtual void Declare(Action declare) { } + + public abstract PatternTestResult Test(FileInfo file); + + public abstract bool Test(DirectoryInfo directory); + + public abstract void PushDirectory(DirectoryInfo directory); + + public virtual void PopDirectory() + { + Frame = _stack.Pop(); + } + + protected void PushDataFrame(TFrame frame) + { + _stack.Push(Frame); + Frame = frame; + } + + protected bool IsStackEmpty() + { + return _stack.Count == 0; + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs new file mode 100644 index 00000000..a3c77810 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinear.cs @@ -0,0 +1,105 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + abstract class PatternContextLinear + : PatternContext + { + public PatternContextLinear(ILinearPattern pattern) + { + Pattern = pattern; + } + + public override PatternTestResult Test(FileInfo file) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test file before entering a directory."); + } + + if(!Frame.IsNotApplicable && IsLastSegment() && TestMatchingSegment(file.Name)) + { + return PatternTestResult.Success(CalculateStem(file)); + } + + return PatternTestResult.Failed; + } + + public override void PushDirectory(DirectoryInfo directory) + { + // copy the current frame + var frame = Frame; + + if (IsStackEmpty() || Frame.IsNotApplicable) + { + // when the stack is being initialized + // or no change is required. + } + else if (!TestMatchingSegment(directory.Name)) + { + // nothing down this path is affected by this pattern + frame.IsNotApplicable = true; + } + else + { + // Determine this frame's contribution to the stem (if any) + var segment = Pattern.Segments[Frame.SegmentIndex]; + if (frame.InStem || segment.CanProduceStem) + { + frame.InStem = true; + frame.StemItems.Add(directory.Name); + } + + // directory matches segment, advance position in pattern + frame.SegmentIndex = frame.SegmentIndex + 1; + } + + PushDataFrame(frame); + } + + public struct FrameData + { + public bool IsNotApplicable; + public int SegmentIndex; + public bool InStem; + private IList _stemItems; + + public IList StemItems + { + get { return _stemItems ?? (_stemItems = new List()); } + } + + public string Stem + { + get { return _stemItems == null ? null : string.Join("/", _stemItems); } + } + } + + protected ILinearPattern Pattern { get; } + + protected bool IsLastSegment() + { + return Frame.SegmentIndex == Pattern.Segments.Count - 1; + } + + protected bool TestMatchingSegment(string value) + { + if (Frame.SegmentIndex >= Pattern.Segments.Count) + { + return false; + } + + return Pattern.Segments[Frame.SegmentIndex].Match(value); + } + + protected string CalculateStem(FileInfo matchedFile) + { + return MatcherContext.CombinePath(Frame.Stem, matchedFile.Name); + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs new file mode 100644 index 00000000..7c58403a --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearExclude.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + class PatternContextLinearExclude : PatternContextLinear + { + public PatternContextLinearExclude(ILinearPattern pattern) + : base(pattern) + { + } + + public override bool Test(DirectoryInfo directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + return IsLastSegment() && TestMatchingSegment(directory.Name); + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs new file mode 100644 index 00000000..cde80265 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextLinearInclude.cs @@ -0,0 +1,49 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + class PatternContextLinearInclude : PatternContextLinear + { + public PatternContextLinearInclude(ILinearPattern pattern) + : base(pattern) + { + } + + public override void Declare(Action onDeclare) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't declare path segment before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return; + } + + if (Frame.SegmentIndex < Pattern.Segments.Count) + { + onDeclare(Pattern.Segments[Frame.SegmentIndex], IsLastSegment()); + } + } + + public override bool Test(DirectoryInfo directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + return !IsLastSegment() && TestMatchingSegment(directory.Name); + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs new file mode 100644 index 00000000..d96e4b01 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRagged.cs @@ -0,0 +1,197 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Collections.Generic; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + abstract class PatternContextRagged : PatternContext + { + public PatternContextRagged(IRaggedPattern pattern) + { + Pattern = pattern; + } + + public override PatternTestResult Test(FileInfo file) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test file before entering a directory."); + } + + if(!Frame.IsNotApplicable && IsEndingGroup() && TestMatchingGroup(file)) + { + return PatternTestResult.Success(CalculateStem(file)); + } + return PatternTestResult.Failed; + } + + public sealed override void PushDirectory(DirectoryInfo directory) + { + // copy the current frame + var frame = Frame; + + if (IsStackEmpty()) + { + // initializing + frame.SegmentGroupIndex = -1; + frame.SegmentGroup = Pattern.StartsWith; + } + else if (Frame.IsNotApplicable) + { + // no change + } + else if (IsStartingGroup()) + { + if (!TestMatchingSegment(directory.Name)) + { + // nothing down this path is affected by this pattern + frame.IsNotApplicable = true; + } + else + { + // starting path incrementally satisfied + frame.SegmentIndex += 1; + } + } + else if (!IsStartingGroup() && directory.Name == "..") + { + // any parent path segment is not applicable in ** + frame.IsNotApplicable = true; + } + else if (!IsStartingGroup() && !IsEndingGroup() && TestMatchingGroup(directory)) + { + frame.SegmentIndex = Frame.SegmentGroup.Count; + frame.BacktrackAvailable = 0; + } + else + { + // increase directory backtrack length + frame.BacktrackAvailable += 1; + } + + if (frame.InStem) + { + frame.StemItems.Add(directory.Name); + } + + while ( + frame.SegmentIndex == frame.SegmentGroup.Count && + frame.SegmentGroupIndex != Pattern.Contains.Count) + { + frame.SegmentGroupIndex += 1; + frame.SegmentIndex = 0; + if (frame.SegmentGroupIndex < Pattern.Contains.Count) + { + frame.SegmentGroup = Pattern.Contains[frame.SegmentGroupIndex]; + } + else + { + frame.SegmentGroup = Pattern.EndsWith; + } + + // We now care about the stem + frame.InStem = true; + } + + PushDataFrame(frame); + } + + public override void PopDirectory() + { + base.PopDirectory(); + if (Frame.StemItems.Count > 0) + { + Frame.StemItems.RemoveAt(Frame.StemItems.Count - 1); + } + } + + public struct FrameData + { + public bool IsNotApplicable; + + public int SegmentGroupIndex; + + public IList SegmentGroup; + + public int BacktrackAvailable; + + public int SegmentIndex; + + public bool InStem; + + private IList _stemItems; + + public IList StemItems + { + get { return _stemItems ?? (_stemItems = new List()); } + } + + public string Stem + { + get { return _stemItems == null ? null : string.Join("/", _stemItems); } + } + } + + protected IRaggedPattern Pattern { get; } + + protected bool IsStartingGroup() + { + return Frame.SegmentGroupIndex == -1; + } + + protected bool IsEndingGroup() + { + return Frame.SegmentGroupIndex == Pattern.Contains.Count; + } + + protected bool TestMatchingSegment(string value) + { + if (Frame.SegmentIndex >= Frame.SegmentGroup.Count) + { + return false; + } + return Frame.SegmentGroup[Frame.SegmentIndex].Match(value); + } + + protected bool TestMatchingGroup(FileSystemInfo value) + { + var groupLength = Frame.SegmentGroup.Count; + var backtrackLength = Frame.BacktrackAvailable + 1; + if (backtrackLength < groupLength) + { + return false; + } + + var scan = value; + for (int index = 0; index != groupLength; ++index) + { + var segment = Frame.SegmentGroup[groupLength - index - 1]; + if (!segment.Match(scan.Name)) + { + return false; + } + scan = GetParent (scan); + } + return true; + } + + FileSystemInfo GetParent (FileSystemInfo info) + { + switch (info) { + case FileInfo fi: + return fi.Directory; + case DirectoryInfo di: + return di.Parent; + } + return null; + } + + protected string CalculateStem(FileInfo matchedFile) + { + return MatcherContext.CombinePath(Frame.Stem, matchedFile.Name); + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs new file mode 100644 index 00000000..b656e68e --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedExclude.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + class PatternContextRaggedExclude : PatternContextRagged + { + public PatternContextRaggedExclude(IRaggedPattern pattern) + : base(pattern) + { + } + + public override bool Test(DirectoryInfo directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + if (IsEndingGroup() && TestMatchingGroup(directory)) + { + // directory excluded with file-like pattern + return true; + } + + if (Pattern.EndsWith.Count == 0 && + Frame.SegmentGroupIndex == Pattern.Contains.Count - 1 && + TestMatchingGroup(directory)) + { + // directory excluded by matching up to final '/**' + return true; + } + + return false; + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs new file mode 100644 index 00000000..0dd0cab9 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternContexts/PatternContextRaggedInclude.cs @@ -0,0 +1,60 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using Microsoft.Extensions.FileSystemGlobbing.Internal.PathSegments; + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal.PatternContexts +{ + class PatternContextRaggedInclude : PatternContextRagged + { + public PatternContextRaggedInclude(IRaggedPattern pattern) + : base(pattern) + { + } + + public override void Declare(Action onDeclare) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't declare path segment before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return; + } + + if (IsStartingGroup() && Frame.SegmentIndex < Frame.SegmentGroup.Count) + { + onDeclare(Frame.SegmentGroup[Frame.SegmentIndex], false); + } + else + { + onDeclare(WildcardPathSegment.MatchAll, false); + } + } + + public override bool Test(DirectoryInfo directory) + { + if (IsStackEmpty()) + { + throw new InvalidOperationException("Can't test directory before entering a directory."); + } + + if (Frame.IsNotApplicable) + { + return false; + } + + if (IsStartingGroup() && !TestMatchingSegment(directory.Name)) + { + // deterministic not-included + return false; + } + + return true; + } + } +} diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternTestResult.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternTestResult.cs new file mode 100644 index 00000000..301762ab --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Internal/PatternTestResult.cs @@ -0,0 +1,24 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.Extensions.FileSystemGlobbing.Internal +{ + struct PatternTestResult + { + public static readonly PatternTestResult Failed = new PatternTestResult(isSuccessful: false, stem: null); + + public bool IsSuccessful { get; } + public string Stem { get; } + + private PatternTestResult(bool isSuccessful, string stem) + { + IsSuccessful = isSuccessful; + Stem = stem; + } + + public static PatternTestResult Success(string stem) + { + return new PatternTestResult(isSuccessful: true, stem: stem); + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Matcher.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Matcher.cs new file mode 100644 index 00000000..b1947166 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/Matcher.cs @@ -0,0 +1,169 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Collections.Generic; +using Microsoft.Extensions.FileSystemGlobbing.Internal; +using Microsoft.Extensions.FileSystemGlobbing.Internal.Patterns; + +namespace Microsoft.Extensions.FileSystemGlobbing +{ + /// + /// Searches the file system for files with names that match specified patterns. + /// + /// + /// + /// Patterns specified in and can use + /// the following formats to match multiple files or directories. + /// + /// + /// + /// + /// exact directory and file name + /// + /// + /// + /// + /// "one.txt" + /// + /// + /// "dir/two.txt" + /// + /// + /// + /// + /// + /// + /// wildcards (*) in file and directory names that represent zero to many characters not including + /// directory separators characters + /// + /// + /// + /// + /// "*.txt"all files with .txt file extension + /// + /// + /// "*.*"all files with an extension + /// + /// + /// "*"all files in top level directory + /// + /// + /// ".*"filenames beginning with '.' + /// + /// - "*word* - all files with 'word' in the filename + /// + /// "readme.*" + /// all files named 'readme' with any file extension + /// + /// + /// "styles/*.css" + /// all files with extension '.css' in the directory 'styles/' + /// + /// + /// "scripts/*/*" + /// all files in 'scripts/' or one level of subdirectory under 'scripts/' + /// + /// + /// "images*/*" + /// all files in a folder with name that is or begins with 'images' + /// + /// + /// + /// + /// + /// arbitrary directory depth ("/**/") + /// + /// + /// + /// "**/*"all files in any subdirectory + /// + /// + /// "dir/**/*"all files in any subdirectory under 'dir/' + /// + /// + /// + /// + /// + /// relative paths + /// + /// '../shared/*' - all files in a diretory named 'shared' at the sibling level to the base directory given + /// to + /// + /// + /// + /// + class Matcher + { + private readonly IList _includePatterns = new List(); + private readonly IList _excludePatterns = new List(); + private readonly PatternBuilder _builder; + private readonly StringComparison _comparison; + + /// + /// Initializes an instance of using case-insensitive matching + /// + public Matcher() + : this(StringComparison.OrdinalIgnoreCase) + { + } + + /// + /// Initializes an instance of using the string comparsion method specified + /// + /// The to use + public Matcher(StringComparison comparisonType) + { + _comparison = comparisonType; + _builder = new PatternBuilder(comparisonType); + } + + /// + /// + /// Add a file name pattern that the matcher should use to discover files. Patterns are relative to the root + /// directory given when is called. + /// + /// + /// Use the forward slash '/' to represent directory separator. Use '*' to represent wildcards in file and + /// directory names. Use '**' to represent arbitrary directory depth. Use '..' to represent a parent directory. + /// + /// + /// The globbing pattern + /// The matcher + public virtual Matcher AddInclude(string pattern) + { + _includePatterns.Add(_builder.Build(pattern)); + return this; + } + + /// + /// + /// Add a file name pattern for files the matcher should exclude from the results. Patterns are relative to the + /// root directory given when is called. + /// + /// + /// Use the forward slash '/' to represent directory separator. Use '*' to represent wildcards in file and + /// directory names. Use '**' to represent arbitrary directory depth. Use '..' to represent a parent directory. + /// + /// + /// The globbing pattern + /// The matcher + public virtual Matcher AddExclude(string pattern) + { + _excludePatterns.Add(_builder.Build(pattern)); + return this; + } + + /// + /// Searches the directory specified for all files matching patterns added to this instance of + /// + /// The root directory for the search + /// Always returns instance of , even if not files were matched + public virtual PatternMatchingResult Execute(DirectoryInfo directoryInfo) + { + var context = new MatcherContext(_includePatterns, _excludePatterns, directoryInfo, _comparison); + return context.Execute(); + } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/PatternMatchingResult.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/PatternMatchingResult.cs new file mode 100644 index 00000000..5f463719 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/PatternMatchingResult.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; + +namespace Microsoft.Extensions.FileSystemGlobbing +{ + /// + /// Represents a collection of + /// + class PatternMatchingResult + { + /// + /// Initializes the result with a collection of + /// + /// A collection of + public PatternMatchingResult(IEnumerable files) + : this(files, hasMatches: files.Any()) + { + Files = files; + } + + /// + /// Initializes the result with a collection of + /// + /// A collection of + /// A value that determines if has any matches. + public PatternMatchingResult(IEnumerable files, bool hasMatches) + { + Files = files; + HasMatches = hasMatches; + } + + /// + /// A collection of + /// + public IEnumerable Files { get; set; } + + /// + /// Gets a value that determines if this instance of has any matches. + /// + public bool HasMatches { get; } + } +} \ No newline at end of file diff --git a/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/StringComparisonHelper.cs b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/StringComparisonHelper.cs new file mode 100644 index 00000000..d8ed3060 --- /dev/null +++ b/Mono.Addins/Microsoft.Extensions.FileSystemGlobbing/StringComparisonHelper.cs @@ -0,0 +1,31 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.FileSystemGlobbing.Util +{ + static class StringComparisonHelper + { + public static StringComparer GetStringComparer(StringComparison comparisonType) + { + switch (comparisonType) + { + case StringComparison.CurrentCulture: + return StringComparer.CurrentCulture; + case StringComparison.CurrentCultureIgnoreCase: + return StringComparer.CurrentCultureIgnoreCase; + case StringComparison.Ordinal: + return StringComparer.Ordinal; + case StringComparison.OrdinalIgnoreCase: + return StringComparer.OrdinalIgnoreCase; + case StringComparison.InvariantCulture: + return StringComparer.InvariantCulture; + case StringComparison.InvariantCultureIgnoreCase: + return StringComparer.InvariantCultureIgnoreCase; + default: + throw new InvalidOperationException($"Unexpected StringComparison type: {comparisonType}"); + } + } + } +} diff --git a/Mono.Addins/Mono.Addins.Description/AddinDescription.cs b/Mono.Addins/Mono.Addins.Description/AddinDescription.cs index 158dbec5..e7c40af5 100644 --- a/Mono.Addins/Mono.Addins.Description/AddinDescription.cs +++ b/Mono.Addins/Mono.Addins.Description/AddinDescription.cs @@ -36,6 +36,7 @@ using Mono.Addins.Serialization; using Mono.Addins.Database; using System.Text; +using Microsoft.Extensions.FileSystemGlobbing; namespace Mono.Addins.Description { @@ -381,11 +382,11 @@ public StringCollection AllFiles { get { StringCollection col = new StringCollection (); foreach (string s in MainModule.AllFiles) - col.Add (s); + AddFileToCollection (col, s); foreach (ModuleDescription mod in OptionalModules) { foreach (string s in mod.AllFiles) - col.Add (s); + AddFileToCollection (col, s); } return col; } @@ -922,6 +923,7 @@ public static AddinDescription Read (string configFile) config = Read (s, Path.GetDirectoryName (configFile)); } config.configFile = configFile; + config.SetBasePath (Path.GetDirectoryName (configFile)); return config; } @@ -951,6 +953,7 @@ public static AddinDescription Read (Stream stream, string basePath) public static AddinDescription Read (TextReader reader, string basePath) { AddinDescription config = new AddinDescription (); + config.SetBasePath (basePath); try { config.configDoc = new XmlDocument (); @@ -1072,6 +1075,21 @@ static bool GetBool (string s, bool defval) else return s == "true" || s == "yes"; } + + void AddFileToCollection (StringCollection collection, string file) + { + bool isSimpleFile = file.IndexOf ('*') == -1; + if (isSimpleFile) { + collection.Add (file); + return; + } + + var matcher = new Matcher (StringComparison.OrdinalIgnoreCase); + matcher.AddInclude (file); + var files = matcher.Execute (new DirectoryInfo (BasePath)).Files; + foreach (var globbedFile in files) + collection.Add (globbedFile.Path); + } internal static AddinDescription ReadBinary (FileDatabase fdb, string configFile) { diff --git a/Mono.Addins/Mono.Addins.csproj b/Mono.Addins/Mono.Addins.csproj index 88db0247..7df38590 100644 --- a/Mono.Addins/Mono.Addins.csproj +++ b/Mono.Addins/Mono.Addins.csproj @@ -157,6 +157,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Test/ImportGlobFileExtension/ImportGlobFileExtension.addin.xml b/Test/ImportGlobFileExtension/ImportGlobFileExtension.addin.xml new file mode 100644 index 00000000..a7f3a982 --- /dev/null +++ b/Test/ImportGlobFileExtension/ImportGlobFileExtension.addin.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + diff --git a/Test/ImportGlobFileExtension/dir1/bar.bin b/Test/ImportGlobFileExtension/dir1/bar.bin new file mode 100644 index 00000000..e69de29b diff --git a/Test/ImportGlobFileExtension/dir1/foo.bin b/Test/ImportGlobFileExtension/dir1/foo.bin new file mode 100644 index 00000000..e69de29b diff --git a/Test/ImportGlobFileExtension/dir1/foo.txt b/Test/ImportGlobFileExtension/dir1/foo.txt new file mode 100644 index 00000000..e69de29b diff --git a/Test/ImportGlobFileExtension/dir2/subdir/subfile.txt b/Test/ImportGlobFileExtension/dir2/subdir/subfile.txt new file mode 100644 index 00000000..e69de29b diff --git a/Test/ImportGlobFileExtension/dir2/subdir/subsubdir/subsubfile.txt b/Test/ImportGlobFileExtension/dir2/subdir/subsubdir/subsubfile.txt new file mode 100644 index 00000000..e69de29b diff --git a/Test/ImportGlobFileExtension/file1.txt b/Test/ImportGlobFileExtension/file1.txt new file mode 100644 index 00000000..e69de29b diff --git a/Test/UnitTests/TestAddinDescription.cs b/Test/UnitTests/TestAddinDescription.cs index 4da2fa98..eadb1f6b 100644 --- a/Test/UnitTests/TestAddinDescription.cs +++ b/Test/UnitTests/TestAddinDescription.cs @@ -186,6 +186,25 @@ public void WriteCorePropertiesAsProps () XmlDocument doc2 = desc.SaveToXml (); Assert.AreEqual (Util.Infoset (doc1), Util.Infoset (doc2)); } + + [Test] + public void FileGlobbingTest () + { + string pathToTestFolder = Path.Combine ("..", "..", "..", "ImportGlobFileExtension"); + AddinDescription desc = AddinDescription.Read (Path.Combine (pathToTestFolder, "ImportGlobFileExtension.addin.xml")); + + var allFiles = desc.AllFiles; + Assert.IsNotNull (allFiles); + CollectionAssert.IsNotEmpty (allFiles); + Assert.AreEqual (5, allFiles.Count); + CollectionAssert.AreEquivalent (new string [] { + "file1.txt", + Path.Combine ("dir1", "bar.bin"), + Path.Combine ("dir1", "foo.bin"), + Path.Combine ("dir2", "subdir", "subfile.txt"), + Path.Combine ("dir2", "subdir", "subsubdir", "subsubfile.txt") + }, allFiles); + } } }