diff --git a/.editorconfig b/.editorconfig index 0de42cbb..ac10945d 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,6 +191,7 @@ dotnet_diagnostic.IDE0079.severity = none dotnet_diagnostic.IDE0090.severity = none dotnet_diagnostic.IDE0220.severity = none dotnet_diagnostic.IDE1005.severity = suggestion +dotnet_diagnostic.IDE0220.severity = none dotnet_diagnostic.IDE1006.severity = suggestion dotnet_diagnostic.RS1024.severity = none # Compare symbols correctly diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7c93a9fa..e56f8e92 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,51 +8,56 @@ on: jobs: - build_packages: + build: runs-on: ubuntu-20.04 env: Configuration: Release TreatWarningsAsErrors: true WarningsNotAsErrors: 1591 + Deterministic: true defaults: run: working-directory: src - outputs: - version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - - name: Update path - run: echo ":/github/home/.dotnet/tools" >> $GITHUB_PATH - - run: dotnet tool install -g GitVersion.Tool --version 5.10.1 --ignore-failed-sources - - run: dotnet tool install -g dotnet-validate --version 0.0.1-preview.304 - - name: Resolve semantic version - id: semantic_version + - run: dotnet tool install -g GitVersion.Tool --version 5.10.0 + - name: Resolve version + id: version run: | - semantic_version=$(dotnet-gitversion /showvariable SemVer) - echo "semantic_version=$semantic_version" >> $GITHUB_ENV - working-directory: ./ + dotnet-gitversion > version.json + version="$(jq -r '.SemVer' version.json)" + pr_version="$(jq -r '.MajorMinorPatch' version.json)-$(jq -r '.PreReleaseLabel' version.json).${{ github.run_number }}.${{ github.run_attempt }}" + if [ "${{ github.event_name }}" = "pull_request" ]; then version=$pr_version; fi + echo "Resolved version: $version" + echo "Version=${version}" >> "$GITHUB_ENV" - run: dotnet restore --configfile nuget.config - run: dotnet format --no-restore --verify-no-changes --severity info --exclude-diagnostics IDE0270 - - run: dotnet build --no-restore /p:Version=${{ env.semantic_version }} + - run: dotnet build --no-restore - run: dotnet test --no-build - - run: dotnet pack --no-build /p:PackageVersion=${{ env.semantic_version }} + - run: dotnet pack --no-build - uses: actions/upload-artifact@v3 with: name: nuget_packages - path: src/CommandLine/bin/Release/*nupkg + path: src/CommandLine/bin/Release/*.*nupkg + - uses: actions/upload-artifact@v3 + with: + name: nuget_packages + path: src/Common/bin/Release/*nupkg + - uses: actions/upload-artifact@v3 + with: + name: nuget_packages + path: src/FileSystem/bin/Release/*.*nupkg - release_packages: + publish: runs-on: ubuntu-20.04 - needs: [build_packages] + needs: [build] if: github.ref_type == 'tag' - env: - NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} steps: - uses: actions/download-artifact@v3 with: name: nuget_packages path: nuget_packages - - run: dotnet nuget push "*.nupkg" -k NUGET_API_KEY -s "https://api.nuget.org/v3/index.json" + - run: dotnet nuget push "*.nupkg" -k ${{ secrets.NUGET_API_KEY }} -s "https://api.nuget.org/v3/index.json" --skip-duplicate working-directory: nuget_packages diff --git a/CHANGELOG.md b/CHANGELOG.md index 14fe838c..dec37d90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add [.NET API](https://josefpihrt.github.io/docs/orang/ref) ([#51](https://github.com/josefpihrt/roslynator/pull/51)). + ### Changed - Migrate documentation to [Docusaurus](https://josefpihrt.github.io/docs/orang) ([#48](https://github.com/josefpihrt/roslynator/pull/48)). diff --git a/src/CommandLine.Core/CommandLine.Core.csproj b/src/CommandLine.Core/CommandLine.Core.csproj index 3006c371..7ada90ee 100644 --- a/src/CommandLine.Core/CommandLine.Core.csproj +++ b/src/CommandLine.Core/CommandLine.Core.csproj @@ -8,7 +8,7 @@ - + @@ -16,4 +16,13 @@ + + + <_Parameter1>Orang, PublicKey=$(OrangPublicKey) + + + <_Parameter1>Orang.DocumentationGenerator, PublicKey=$(OrangPublicKey) + + + \ No newline at end of file diff --git a/src/CommandLine.Core/Help/CommandHelp.cs b/src/CommandLine.Core/Help/CommandHelp.cs index de4126ee..ca7f0a9a 100644 --- a/src/CommandLine.Core/Help/CommandHelp.cs +++ b/src/CommandLine.Core/Help/CommandHelp.cs @@ -42,18 +42,18 @@ public CommandHelp( public static CommandHelp Create( Command command, IEnumerable? providers = null, - Filter? filter = null) + Matcher? matcher = null) { ImmutableArray arguments = (command.Arguments.Any()) - ? HelpProvider.GetArgumentItems(command.Arguments, filter) + ? HelpProvider.GetArgumentItems(command.Arguments, matcher) : ImmutableArray.Empty; - ImmutableArray options = HelpProvider.GetOptionItems(command.Options, filter); + ImmutableArray options = HelpProvider.GetOptionItems(command.Options, matcher); ImmutableArray values = HelpProvider.GetOptionValues( command.Options, providers ?? ImmutableArray.Empty, - filter); + matcher); return new CommandHelp(command, arguments, options, values); } diff --git a/src/CommandLine.Core/Help/CommandsHelp.cs b/src/CommandLine.Core/Help/CommandsHelp.cs index 287b0cf5..8f555a4c 100644 --- a/src/CommandLine.Core/Help/CommandsHelp.cs +++ b/src/CommandLine.Core/Help/CommandsHelp.cs @@ -23,14 +23,14 @@ public CommandsHelp( public static CommandsHelp Create( IEnumerable commands, IEnumerable? providers = null, - Filter? filter = null) + Matcher? matcher = null) { - ImmutableArray commandsHelp = HelpProvider.GetCommandItems(commands, filter); + ImmutableArray commandsHelp = HelpProvider.GetCommandItems(commands, matcher); ImmutableArray values = HelpProvider.GetOptionValues( commands.SelectMany(f => f.Options), providers ?? ImmutableArray.Empty, - filter); + matcher); return new CommandsHelp(commandsHelp, values); } diff --git a/src/CommandLine.Core/Help/HelpProvider.cs b/src/CommandLine.Core/Help/HelpProvider.cs index cbeee953..f3a6640b 100644 --- a/src/CommandLine.Core/Help/HelpProvider.cs +++ b/src/CommandLine.Core/Help/HelpProvider.cs @@ -12,7 +12,7 @@ internal static class HelpProvider { public const int SeparatorWidth = 2; - public static ImmutableArray GetCommandItems(IEnumerable commands, Filter? filter = null) + public static ImmutableArray GetCommandItems(IEnumerable commands, Matcher? matcher = null) { if (!commands.Any()) return ImmutableArray.Empty; @@ -33,7 +33,7 @@ public static ImmutableArray GetCommandItems(IEnumerable c var commandItem = new CommandItem(command, syntax, description); - if (filter?.IsMatch(commandItem.Text) != false) + if (matcher?.IsMatch(commandItem.Text) != false) builder.Add(commandItem); } @@ -42,7 +42,7 @@ public static ImmutableArray GetCommandItems(IEnumerable c public static ImmutableArray GetArgumentItems( IEnumerable arguments, - Filter? filter = null) + Matcher? matcher = null) { int width = CalculateArgumentsWidths(arguments); @@ -66,7 +66,7 @@ public static ImmutableArray GetArgumentItems( var argumentItem = new ArgumentItem(argument, syntax, description); - if (filter?.IsMatch(argumentItem.Text) != false) + if (matcher?.IsMatch(argumentItem.Text) != false) { builder.Add(argumentItem); } @@ -75,7 +75,7 @@ public static ImmutableArray GetArgumentItems( return builder.ToImmutableArray(); } - public static ImmutableArray GetOptionItems(IEnumerable options, Filter? filter = null) + public static ImmutableArray GetOptionItems(IEnumerable options, Matcher? matcher = null) { int width = CalculateOptionsWidths(options); @@ -119,7 +119,7 @@ public static ImmutableArray GetOptionItems(IEnumerable GetOptionItems(IEnumerable GetOptionValues( IEnumerable options, IEnumerable providers, - Filter? filter = null) + Matcher? matcher = null) { providers = OptionValueProvider.GetProviders(options, providers).ToImmutableArray(); @@ -147,8 +147,8 @@ public static ImmutableArray GetOptionValues( builder.Add(new OptionValueItemList(provider.Name, valueItems)); } - return (filter is not null) - ? FilterOptionValues(builder, filter) + return (matcher is not null) + ? FilterOptionValues(builder, matcher) : builder.ToImmutableArray(); } @@ -192,20 +192,20 @@ public static ImmutableArray GetOptionValueItems( private static ImmutableArray FilterOptionValues( IEnumerable values, - Filter filter) + Matcher matcher) { ImmutableArray.Builder builder = ImmutableArray.CreateBuilder(); foreach (OptionValueItemList valueList in values) { - if (filter.IsMatch(valueList.MetaValue)) + if (matcher.IsMatch(valueList.MetaValue)) { builder.Add(valueList); } else { ImmutableArray valueItems = valueList.Values - .Where(f => filter.IsMatch(f.Text)) + .Where(f => matcher.IsMatch(f.Text)) .ToImmutableArray(); if (valueItems.Any()) @@ -261,15 +261,15 @@ public static ImmutableArray GetExpressionLines(string? variableName = n variableName ??= "x"; builder.Add(($"{variableName}=n", "")); - builder.Add(($"{variableName}n", "")); - builder.Add(($"{variableName}<=n", "")); - builder.Add(($"{variableName}>=n", "")); - builder.Add(($"{variableName}=", "Inclusive interval")); - builder.Add(($"{variableName}=(min;max)", "Exclusive interval")); + builder.Add(($@"""{variableName}n""", "")); + builder.Add(($@"""{variableName}<=n""", "")); + builder.Add(($@" ""{variableName}>=n""", "")); + builder.Add(($@" ""{variableName}=""", "Inclusive interval")); + builder.Add(($@" ""{variableName}=(min;max)""", "Exclusive interval")); if (includeDate) - builder.Add(($"{variableName}=-d|[d.]hh:mm[:ss]", $"{variableName} is greater than actual date - ")); + builder.Add(($@"""{variableName}=-d|[d.]hh:mm[:ss]""", $"{variableName} is greater than actual date - ")); return builder.ToImmutableArray(); } diff --git a/src/CommandLine.Core/Help/HelpWriter.cs b/src/CommandLine.Core/Help/HelpWriter.cs index b4ed1de5..a6f1effa 100644 --- a/src/CommandLine.Core/Help/HelpWriter.cs +++ b/src/CommandLine.Core/Help/HelpWriter.cs @@ -27,7 +27,7 @@ public virtual void WriteCommand(CommandHelp commandHelp) WriteArguments(arguments); WriteEndArguments(commandHelp); } - else if (Options.Filter is not null + else if (Options.Matcher is not null && commandHelp.Command.Arguments.Any()) { WriteLine(); @@ -42,7 +42,7 @@ public virtual void WriteCommand(CommandHelp commandHelp) WriteOptions(options); WriteEndOptions(commandHelp); } - else if (Options.Filter is not null + else if (Options.Matcher is not null && commandHelp.Command.Options.Any()) { WriteLine(); @@ -114,7 +114,7 @@ public virtual void WriteCommands(CommandsHelp commandsHelp) WriteEndCommands(commandsHelp); } - else if (Options.Filter is not null) + else if (Options.Matcher is not null) { WriteLine("No command found"); } @@ -171,6 +171,23 @@ public void WriteValues(IEnumerable optionValues) } } + public void WriteExpressionSyntax(IEnumerable optionValues) + { + ImmutableArray expressions = HelpProvider.GetExpressionItems(optionValues); + + if (!expressions.IsEmpty) + { + WriteLine(); + WriteHeading("Expression syntax"); + + foreach (string expression in expressions) + { + Write(Options.Indent); + WriteLine(expression); + } + } + } + private void WriteValues(ImmutableArray values) { foreach (OptionValueItem value in values) diff --git a/src/CommandLine.Core/Help/HelpWriterOptions.cs b/src/CommandLine.Core/Help/HelpWriterOptions.cs index 8ce5981a..4bb6a948 100644 --- a/src/CommandLine.Core/Help/HelpWriterOptions.cs +++ b/src/CommandLine.Core/Help/HelpWriterOptions.cs @@ -8,13 +8,13 @@ public class HelpWriterOptions public HelpWriterOptions( string indent = " ", - Filter? filter = null) + Matcher? matcher = null) { Indent = indent; - Filter = filter; + Matcher = matcher; } public string Indent { get; } - public Filter? Filter { get; } + public Matcher? Matcher { get; } } diff --git a/src/CommandLine.Core/Properties/AssemblyInfo.cs b/src/CommandLine.Core/Properties/AssemblyInfo.cs deleted file mode 100644 index 73afcbeb..00000000 --- a/src/CommandLine.Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -#pragma warning disable RCS0056 - -[assembly: InternalsVisibleTo("Orang, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] -[assembly: InternalsVisibleTo("Orang.DocumentationGenerator, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] diff --git a/src/CommandLine/Aggregation/StorageSectionComparer.cs b/src/CommandLine/Aggregation/StorageSectionComparer.cs index 20bf40dc..1ab05111 100644 --- a/src/CommandLine/Aggregation/StorageSectionComparer.cs +++ b/src/CommandLine/Aggregation/StorageSectionComparer.cs @@ -20,12 +20,12 @@ private class PathStorageSectionComparer : StorageSectionComparer { public override int Compare([AllowNull] StorageSection x, [AllowNull] StorageSection y) { - return FileSystemHelpers.Comparer.Compare(x.FileMatch?.Path, y.FileMatch?.Path); + return FileSystemUtilities.Comparer.Compare(x.FileMatch?.Path, y.FileMatch?.Path); } public override bool Equals([AllowNull] StorageSection x, [AllowNull] StorageSection y) { - return FileSystemHelpers.Comparer.Equals(x.FileMatch?.Path, y.FileMatch?.Path); + return FileSystemUtilities.Comparer.Equals(x.FileMatch?.Path, y.FileMatch?.Path); } public override int GetHashCode([DisallowNull] StorageSection obj) @@ -35,7 +35,7 @@ public override int GetHashCode([DisallowNull] StorageSection obj) if (path is null) return 0; - return FileSystemHelpers.Comparer.GetHashCode(path); + return FileSystemUtilities.Comparer.GetHashCode(path); } } } diff --git a/src/CommandLine/CommandLine.csproj b/src/CommandLine/CommandLine.csproj index b6ed5512..0428f53e 100644 --- a/src/CommandLine/CommandLine.csproj +++ b/src/CommandLine/CommandLine.csproj @@ -8,32 +8,12 @@ + true true orang Orang.DotNet.Cli - Search, replace, rename and delete directories, files and its content using the power of .NET regular expressions. - https://github.com/JosefPihrt/Orang - Apache-2.0 - icon.png - RegularExpression;Regex;RegExp;CLI - docs/README.md - https://github.com/JosefPihrt/Orang.git - git - false - - true - true - true - true - snupkg - - - - - - @@ -41,7 +21,7 @@ - + @@ -54,4 +34,13 @@ + + + <_Parameter1>Orang.DocumentationGenerator, PublicKey=$(OrangPublicKey) + + + <_Parameter1>Orang.CommandLine.Tests, PublicKey=$(OrangPublicKey) + + + diff --git a/src/CommandLine/CommandLineExtensions.cs b/src/CommandLine/CommandLineExtensions.cs index 1284edfe..58e35ed2 100644 --- a/src/CommandLine/CommandLineExtensions.cs +++ b/src/CommandLine/CommandLineExtensions.cs @@ -72,7 +72,7 @@ public static void WriteFileError( if (!string.IsNullOrEmpty(path) && !string.IsNullOrEmpty(message) - && !message.Contains(path, FileSystemHelpers.Comparison)) + && !message.Contains(path, FileSystemUtilities.Comparison)) { logger.WriteLine($"{indent}PATH: {path}", Colors.Message_Warning, verbosity); } @@ -84,7 +84,7 @@ public static void WriteFileError( public static void WriteSearchedFilesAndDirectories( this Logger logger, - CommandLine.SearchTelemetry telemetry, + SearchTelemetry telemetry, SearchTarget searchTarget, Verbosity verbosity = Verbosity.Detailed) { @@ -134,7 +134,7 @@ public static void WriteSearchedFilesAndDirectories( public static void WriteProcessedFilesAndDirectories( this Logger logger, - CommandLine.SearchTelemetry telemetry, + SearchTelemetry telemetry, SearchTarget searchTarget, string processedFilesTitle, string processedDirectoriesTitle, diff --git a/src/CommandLine/CommandLineOptions/CommonCopyCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/CommonCopyCommandLineOptions.cs index 843adf0c..d7c65003 100644 --- a/src/CommandLine/CommandLineOptions/CommonCopyCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/CommonCopyCommandLineOptions.cs @@ -43,7 +43,7 @@ public bool TryParse(CommonCopyCommandOptions options, ParseContext context) Name, OptionNames.Name, OptionValueProviders.PatternOptionsProvider, - out Filter? nameFilter, + out Matcher? nameFilter, out FileNamePart namePart, allowNull: true)) { diff --git a/src/CommandLine/CommandLineOptions/CommonFindCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/CommonFindCommandLineOptions.cs index f39459a1..7257f281 100644 --- a/src/CommandLine/CommandLineOptions/CommonFindCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/CommonFindCommandLineOptions.cs @@ -1,6 +1,5 @@ // Copyright (c) Josef Pihrt. 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 CommandLine; using Orang.CommandLine.Annotations; @@ -62,7 +61,7 @@ public bool TryParse(CommonFindCommandOptions options, ParseContext context) Content, OptionNames.Content, OptionValueProviders.PatternOptionsWithoutPartProvider, - out Filter? contentFilter, + out Matcher? contentFilter, allowNull: true, allowEmptyPattern: true)) { @@ -196,7 +195,7 @@ public bool TryParse(CommonFindCommandOptions options, ParseContext context) options.Format = new OutputDisplayFormat( contentDisplayStyle: contentDisplayStyle - ?? ((ReferenceEquals(contentFilter, Filter.EntireInput)) ? ContentDisplayStyle.AllLines : DefaultContentDisplayStyle), + ?? ((ReferenceEquals(contentFilter, Matcher.EntireInput)) ? ContentDisplayStyle.AllLines : DefaultContentDisplayStyle), pathDisplayStyle: pathDisplayStyle ?? PathDisplayStyle.Full, lineOptions: lineDisplayOptions, lineContext: lineContext, diff --git a/src/CommandLine/CommandLineOptions/CommonReplaceCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/CommonReplaceCommandLineOptions.cs index 0303ef8e..98d935ee 100644 --- a/src/CommandLine/CommandLineOptions/CommonReplaceCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/CommonReplaceCommandLineOptions.cs @@ -114,7 +114,7 @@ public bool TryParse(CommonReplaceCommandOptions options, ParseContext context) options = (CommonReplaceCommandOptions)baseOptions; - Filter? contentFilter = null; + Matcher? contentFilter = null; if (!Content.Any()) { contentFilter = GetDefaultContentFilter(); @@ -322,7 +322,7 @@ public bool TryParse(CommonReplaceCommandOptions options, ParseContext context) return true; } - protected virtual Filter? GetDefaultContentFilter() + protected virtual Matcher? GetDefaultContentFilter() { return null; } diff --git a/src/CommandLine/CommandLineOptions/CopyCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/CopyCommandLineOptions.cs index 32553ca5..3f3e1609 100644 --- a/src/CommandLine/CommandLineOptions/CopyCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/CopyCommandLineOptions.cs @@ -58,8 +58,8 @@ public bool TryParse(CopyCommandOptions options, ParseContext context) if (!context.TryParseAsEnumFlags( Compare, OptionNames.Compare, - out FileCompareOptions compareOptions, - FileCompareOptions.None, + out FileCompareProperties compareProperties, + FileCompareProperties.None, OptionValueProviders.FileCompareOptionsProvider)) { return false; @@ -78,7 +78,7 @@ public bool TryParse(CopyCommandOptions options, ParseContext context) return false; } - options.CompareOptions = compareOptions; + options.CompareProperties = compareProperties; options.DryRun = DryRun; options.Flat = Flat; options.ConflictResolution = conflictResolution; diff --git a/src/CommandLine/CommandLineOptions/DeleteCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/DeleteCommandLineOptions.cs index 6c143775..93d992a4 100644 --- a/src/CommandLine/CommandLineOptions/DeleteCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/DeleteCommandLineOptions.cs @@ -80,7 +80,7 @@ public bool TryParse(DeleteCommandOptions options, ParseContext context) Name, OptionNames.Name, OptionValueProviders.PatternOptionsProvider, - out Filter? nameFilter, + out Matcher? nameFilter, out FileNamePart namePart, allowNull: true)) { @@ -100,7 +100,7 @@ public bool TryParse(DeleteCommandOptions options, ParseContext context) Content, OptionNames.Content, OptionValueProviders.PatternOptionsWithoutPartProvider, - out Filter? contentFilter, + out Matcher? contentFilter, allowNull: true, allowEmptyPattern: true)) { @@ -207,7 +207,7 @@ public bool TryParse(DeleteCommandOptions options, ParseContext context) options.Format = new OutputDisplayFormat( contentDisplayStyle: contentDisplayStyle - ?? ((ReferenceEquals(contentFilter, Filter.EntireInput)) ? ContentDisplayStyle.AllLines : ContentDisplayStyle.Omit), + ?? ((ReferenceEquals(contentFilter, Matcher.EntireInput)) ? ContentDisplayStyle.AllLines : ContentDisplayStyle.Omit), pathDisplayStyle: pathDisplayStyle ?? PathDisplayStyle.Full, lineOptions: lineDisplayOptions, lineContext: lineContext, diff --git a/src/CommandLine/CommandLineOptions/FileSystemCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/FileSystemCommandLineOptions.cs index c16f2b08..8795f4c3 100644 --- a/src/CommandLine/CommandLineOptions/FileSystemCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/FileSystemCommandLineOptions.cs @@ -218,7 +218,7 @@ public bool TryParse(FileSystemCommandOptions options, ParseContext context) IncludeDirectory, OptionNames.IncludeDirectory, OptionValueProviders.PatternOptionsProvider, - out Filter? directoryFilter, + out Matcher? directoryFilter, out FileNamePart directoryNamePart, allowNull: true, namePartProvider: OptionValueProviders.NamePartKindProvider_WithoutExtension)) @@ -230,7 +230,7 @@ public bool TryParse(FileSystemCommandOptions options, ParseContext context) Extension, OptionNames.Extension, OptionValueProviders.ExtensionOptionsProvider, - out Filter? extensionFilter, + out Matcher? extensionFilter, allowNull: true, defaultNamePart: FileNamePart.Extension, includedPatternOptions: PatternOptions.List | PatternOptions.Equals | PatternOptions.IgnoreCase)) @@ -324,7 +324,7 @@ protected virtual bool TryParsePaths(out ImmutableArray paths, ParseCo if (PathsFrom is not null) { - if (!FileSystemHelpers.TryReadAllText(PathsFrom, out string? content, ex => context.WriteError(ex))) + if (!FileSystemUtilities.TryReadAllText(PathsFrom, out string? content, ex => context.WriteError(ex))) return false; IEnumerable lines = TextHelpers.ReadLines(content).Where(f => !string.IsNullOrWhiteSpace(f)); diff --git a/src/CommandLine/CommandLineOptions/HelpCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/HelpCommandLineOptions.cs index 243ae525..a7c980ba 100644 --- a/src/CommandLine/CommandLineOptions/HelpCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/HelpCommandLineOptions.cs @@ -43,7 +43,7 @@ public bool TryParse(HelpCommandOptions options, ParseContext context) Filter, OptionNames.Filter, OptionValueProviders.PatternOptions_List_Provider, - out Filter? filter, + out Matcher? matcher, allowNull: true, includedPatternOptions: PatternOptions.IgnoreCase)) { @@ -51,7 +51,7 @@ public bool TryParse(HelpCommandOptions options, ParseContext context) } options.Command = Command.ToArray(); - options.Filter = filter; + options.Matcher = matcher; options.Manual = Manual; options.Online = Online; diff --git a/src/CommandLine/CommandLineOptions/MoveCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/MoveCommandLineOptions.cs index 6de2b80b..db1b8e4e 100644 --- a/src/CommandLine/CommandLineOptions/MoveCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/MoveCommandLineOptions.cs @@ -58,8 +58,8 @@ public bool TryParse(MoveCommandOptions options, ParseContext context) if (!context.TryParseAsEnumFlags( Compare, OptionNames.Compare, - out FileCompareOptions compareOptions, - FileCompareOptions.None, + out FileCompareProperties compareProperties, + FileCompareProperties.None, OptionValueProviders.FileCompareOptionsProvider)) { return false; @@ -78,7 +78,7 @@ public bool TryParse(MoveCommandOptions options, ParseContext context) return false; } - options.CompareOptions = compareOptions; + options.CompareProperties = compareProperties; options.DryRun = DryRun; options.Flat = Flat; options.ConflictResolution = conflictResolution; diff --git a/src/CommandLine/CommandLineOptions/RegexListCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/RegexListCommandLineOptions.cs index 9721bd70..cbdad31a 100644 --- a/src/CommandLine/CommandLineOptions/RegexListCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/RegexListCommandLineOptions.cs @@ -82,14 +82,14 @@ public bool TryParse(RegexListCommandOptions options, ParseContext context) Filter, OptionNames.Filter, OptionValueProviders.PatternOptions_List_Provider, - out Filter? filter, + out Matcher? matcher, allowNull: true, includedPatternOptions: PatternOptions.IgnoreCase)) { return false; } - options.Filter = filter; + options.Matcher = matcher; options.Value = value; options.RegexOptions = regexOptions; options.InCharGroup = CharGroup; diff --git a/src/CommandLine/CommandLineOptions/RegexMatchCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/RegexMatchCommandLineOptions.cs index 094d20cb..b74b5d45 100644 --- a/src/CommandLine/CommandLineOptions/RegexMatchCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/RegexMatchCommandLineOptions.cs @@ -42,7 +42,7 @@ public bool TryParse(RegexMatchCommandOptions options, ParseContext context) Content, OptionNames.Content, OptionValueProviders.PatternOptions_Match_Provider, - out Filter? filter)) + out Matcher? matcher)) { return false; } @@ -56,7 +56,7 @@ public bool TryParse(RegexMatchCommandOptions options, ParseContext context) return false; } - options.Filter = filter!; + options.Matcher = matcher!; options.HighlightOptions = highlightOptions; options.MaxCount = MaxCount; diff --git a/src/CommandLine/CommandLineOptions/RegexSplitCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/RegexSplitCommandLineOptions.cs index afe33f68..3ec34eb9 100644 --- a/src/CommandLine/CommandLineOptions/RegexSplitCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/RegexSplitCommandLineOptions.cs @@ -49,7 +49,7 @@ public bool TryParse(RegexSplitCommandOptions options, ParseContext context) Content, OptionNames.Content, OptionValueProviders.PatternOptionsWithoutGroupAndPartAndNegativeProvider, - out Filter? filter)) + out Matcher? matcher)) { return false; } @@ -63,7 +63,7 @@ public bool TryParse(RegexSplitCommandOptions options, ParseContext context) return false; } - options.Filter = filter!; + options.Matcher = matcher!; options.HighlightOptions = highlightOptions; options.OmitGroups = NoGroups; options.MaxCount = MaxCount; diff --git a/src/CommandLine/CommandLineOptions/RenameCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/RenameCommandLineOptions.cs index dae0bf81..dcd940f7 100644 --- a/src/CommandLine/CommandLineOptions/RenameCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/RenameCommandLineOptions.cs @@ -111,7 +111,7 @@ public bool TryParse(RenameCommandOptions options, ParseContext context) Name, OptionNames.Name, OptionValueProviders.PatternOptionsWithoutGroupAndNegativeProvider, - out Filter? nameFilter, + out Matcher? nameFilter, out FileNamePart namePart, namePartProvider: OptionValueProviders.NamePartKindProvider_WithoutFullName)) { @@ -122,7 +122,7 @@ public bool TryParse(RenameCommandOptions options, ParseContext context) Content, OptionNames.Content, OptionValueProviders.PatternOptionsWithoutPartProvider, - out Filter? contentFilter, + out Matcher? contentFilter, allowNull: true, allowEmptyPattern: true)) { @@ -148,7 +148,7 @@ public bool TryParse(RenameCommandOptions options, ParseContext context) OptionNames.Modify, replacement, matchEvaluator, - out ReplaceOptions? replaceOptions)) + out Replacer? replacer)) { return false; } @@ -263,7 +263,7 @@ public bool TryParse(RenameCommandOptions options, ParseContext context) options.Format = new OutputDisplayFormat( contentDisplayStyle: contentDisplayStyle - ?? ((ReferenceEquals(contentFilter, Filter.EntireInput)) ? ContentDisplayStyle.AllLines : ContentDisplayStyle.Omit), + ?? ((ReferenceEquals(contentFilter, Matcher.EntireInput)) ? ContentDisplayStyle.AllLines : ContentDisplayStyle.Omit), pathDisplayStyle: pathDisplayStyle ?? PathDisplayStyle.Full, lineOptions: lineDisplayOptions, lineContext: lineContext, @@ -273,7 +273,7 @@ public bool TryParse(RenameCommandOptions options, ParseContext context) options.HighlightOptions = highlightOptions; options.SearchTarget = GetSearchTarget(); - options.ReplaceOptions = replaceOptions; + options.Replacer = replacer; options.AskMode = (Ask) ? AskMode.File : AskMode.None; options.DryRun = DryRun; options.NameFilter = nameFilter; diff --git a/src/CommandLine/CommandLineOptions/ReplaceCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/ReplaceCommandLineOptions.cs index 45aa156a..f8b68967 100644 --- a/src/CommandLine/CommandLineOptions/ReplaceCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/ReplaceCommandLineOptions.cs @@ -72,16 +72,16 @@ public bool TryParse(ReplaceCommandOptions options, ParseContext context) OptionNames.Modify, replacement, matchEvaluator, - out ReplaceOptions? replaceOptions)) + out Replacer? replacer)) { return false; } - options.Replacer = replaceOptions; + options.Replacer = replacer; #if DEBUG // --find if (Find) { - options.Replacer = ReplaceOptions.Empty; + options.Replacer = Replacer.Empty; options.HighlightOptions = HighlightOptions.Match; options.DryRun = true; } diff --git a/src/CommandLine/CommandLineOptions/SpellcheckCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/SpellcheckCommandLineOptions.cs index 63987cc2..eb18ab97 100644 --- a/src/CommandLine/CommandLineOptions/SpellcheckCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/SpellcheckCommandLineOptions.cs @@ -72,7 +72,7 @@ public bool TryParse(SpellcheckCommandOptions options, ParseContext context) Word, OptionNames.Word, OptionValueProviders.PatternOptions_Word_Provider, - out Filter? wordFilter)) + out Matcher? wordFilter)) { return false; } @@ -100,8 +100,8 @@ public bool TryParse(SpellcheckCommandOptions options, ParseContext context) return true; } - protected override Filter? GetDefaultContentFilter() + protected override Matcher? GetDefaultContentFilter() { - return Filter.EntireInput; + return Matcher.EntireInput; } } diff --git a/src/CommandLine/CommandLineOptions/SyncCommandLineOptions.cs b/src/CommandLine/CommandLineOptions/SyncCommandLineOptions.cs index ba39146d..b84b0ff4 100644 --- a/src/CommandLine/CommandLineOptions/SyncCommandLineOptions.cs +++ b/src/CommandLine/CommandLineOptions/SyncCommandLineOptions.cs @@ -61,8 +61,11 @@ public bool TryParse(SyncCommandOptions options, ParseContext context) if (!context.TryParseAsEnumFlags( Compare, OptionNames.Compare, - out FileCompareOptions compareOptions, - FileCompareOptions.Attributes | FileCompareOptions.Content | FileCompareOptions.ModifiedTime | FileCompareOptions.Size, + out FileCompareProperties compareProperties, + FileCompareProperties.Attributes + | FileCompareProperties.Content + | FileCompareProperties.ModifiedTime + | FileCompareProperties.Size, OptionValueProviders.FileCompareOptionsProvider)) { return false; @@ -79,7 +82,7 @@ public bool TryParse(SyncCommandOptions options, ParseContext context) } options.SearchTarget = SearchTarget.All; - options.CompareOptions = compareOptions; + options.CompareProperties = compareProperties; options.DryRun = DryRun; options.Target = second; options.ConflictResolution = conflictResolution; diff --git a/src/CommandLine/CommandOptions/CommonCopyCommandOptions.cs b/src/CommandLine/CommandOptions/CommonCopyCommandOptions.cs index 3f99d25f..1c7b1924 100644 --- a/src/CommandLine/CommandOptions/CommonCopyCommandOptions.cs +++ b/src/CommandLine/CommandOptions/CommonCopyCommandOptions.cs @@ -14,9 +14,19 @@ protected CommonCopyCommandOptions() public TimeSpan AllowedTimeDiff { get; internal set; } - public FileAttributes NoCompareAttributes { get; internal set; } + public FileAttributes? NoCompareAttributes { get; internal set; } - public FileCompareOptions CompareOptions { get; internal set; } + public FileAttributes? CompareAttributes + { + get + { + return (NoCompareAttributes is not null) + ? FileSystemUtilities.AllFileAttributes & ~NoCompareAttributes + : null; + } + } + + public FileCompareProperties CompareProperties { get; internal set; } public bool DryRun { get; internal set; } diff --git a/src/CommandLine/CommandOptions/DeleteCommandOptions.cs b/src/CommandLine/CommandOptions/DeleteCommandOptions.cs index 52538574..305129dc 100644 --- a/src/CommandLine/CommandOptions/DeleteCommandOptions.cs +++ b/src/CommandLine/CommandOptions/DeleteCommandOptions.cs @@ -12,10 +12,6 @@ internal DeleteCommandOptions() public bool IncludingBom { get; internal set; } - public bool FilesOnly { get; internal set; } - - public bool DirectoriesOnly { get; internal set; } - protected override void WriteDiagnosticCore(DiagnosticWriter writer) { writer.WriteDeleteCommand(this); diff --git a/src/CommandLine/CommandOptions/FileSystemCommandOptions.cs b/src/CommandLine/CommandOptions/FileSystemCommandOptions.cs index d940389e..e7872b4b 100644 --- a/src/CommandLine/CommandOptions/FileSystemCommandOptions.cs +++ b/src/CommandLine/CommandOptions/FileSystemCommandOptions.cs @@ -18,15 +18,15 @@ internal FileSystemCommandOptions() public ImmutableArray Paths { get; internal set; } - public Filter? NameFilter { get; internal set; } + public Matcher? NameFilter { get; internal set; } public FileNamePart NamePart { get; internal set; } - public Filter? ExtensionFilter { get; internal set; } + public Matcher? ExtensionFilter { get; internal set; } - public Filter? ContentFilter { get; internal set; } + public Matcher? ContentFilter { get; internal set; } - public Filter? DirectoryFilter { get; internal set; } + public Matcher? DirectoryFilter { get; internal set; } public FileNamePart DirectoryNamePart { get; internal set; } @@ -70,15 +70,15 @@ internal FileSystemCommandOptions() public string Indent => Format.Indent; - internal MatchOutputInfo? CreateOutputInfo(string input, Match match, Filter filter) + internal MatchOutputInfo? CreateOutputInfo(string input, Match match, Matcher matcher) { if (ContentDisplayStyle != ContentDisplayStyle.ValueDetail) return null; - int groupNumber = filter.GroupNumber; + int groupNumber = matcher.GroupNumber; return MatchOutputInfo.Create( - MatchData.Create(input, filter.Regex, match), + MatchData.Create(input, matcher.Regex, match), groupNumber, includeGroupNumber: groupNumber >= 0, includeCaptureNumber: false, diff --git a/src/CommandLine/CommandOptions/HelpCommandOptions.cs b/src/CommandLine/CommandOptions/HelpCommandOptions.cs index 20ab4a52..c2cf3549 100644 --- a/src/CommandLine/CommandOptions/HelpCommandOptions.cs +++ b/src/CommandLine/CommandOptions/HelpCommandOptions.cs @@ -10,7 +10,7 @@ internal HelpCommandOptions() public string[] Command { get; internal set; } = null!; - public Filter? Filter { get; internal set; } + public Matcher? Matcher { get; internal set; } public bool Manual { get; internal set; } diff --git a/src/CommandLine/CommandOptions/RegexCommandOptions.cs b/src/CommandLine/CommandOptions/RegexCommandOptions.cs index 37ff6db5..a7cfa96b 100644 --- a/src/CommandLine/CommandOptions/RegexCommandOptions.cs +++ b/src/CommandLine/CommandOptions/RegexCommandOptions.cs @@ -10,7 +10,7 @@ internal RegexCommandOptions() { } - public Filter Filter { get; internal set; } = null!; + public Matcher Matcher { get; internal set; } = null!; public string Input { get; internal set; } = null!; diff --git a/src/CommandLine/CommandOptions/RegexListCommandOptions.cs b/src/CommandLine/CommandOptions/RegexListCommandOptions.cs index 842b2d4e..19682773 100644 --- a/src/CommandLine/CommandOptions/RegexListCommandOptions.cs +++ b/src/CommandLine/CommandOptions/RegexListCommandOptions.cs @@ -14,7 +14,7 @@ internal RegexListCommandOptions() public char? Value { get; internal set; } - public Filter? Filter { get; internal set; } + public Matcher? Matcher { get; internal set; } public bool InCharGroup { get; internal set; } diff --git a/src/CommandLine/CommandOptions/RenameCommandOptions.cs b/src/CommandLine/CommandOptions/RenameCommandOptions.cs index 4b7a669c..ebefd5e2 100644 --- a/src/CommandLine/CommandOptions/RenameCommandOptions.cs +++ b/src/CommandLine/CommandOptions/RenameCommandOptions.cs @@ -14,7 +14,7 @@ internal RenameCommandOptions() public ConflictResolution ConflictResolution { get; internal set; } - public ReplaceOptions ReplaceOptions { get; internal set; } = null!; + public Replacer Replacer { get; internal set; } = null!; protected override void WriteDiagnosticCore(DiagnosticWriter writer) { diff --git a/src/CommandLine/Commands/CommonCopyCommand`1.cs b/src/CommandLine/Commands/CommonCopyCommand`1.cs index a69b0d7c..0b44a503 100644 --- a/src/CommandLine/Commands/CommonCopyCommand`1.cs +++ b/src/CommandLine/Commands/CommonCopyCommand`1.cs @@ -34,7 +34,7 @@ public ConflictResolution ConflictResolution private PathWriter? PathWriter { get; } - protected override void OnSearchCreating(FileSystemSearch search) + protected override void OnSearchCreating(SearchState search) { if (Options.SearchTarget != SearchTarget.Files && !Options.StructureOnly) @@ -43,9 +43,9 @@ protected override void OnSearchCreating(FileSystemSearch search) } } - protected override NameFilter? CreateAdditionalDirectoryFilter() + protected override Func? CreateExcludeDirectoryPredicate(Func? predicate = null) { - return FileSystem.NameFilter.CreateFromDirectoryPath(Target, isNegative: true); + return DirectoryPredicate.Create(Target); } protected abstract string GetQuestionText(bool isDirectory); @@ -54,7 +54,7 @@ protected override void OnSearchCreating(FileSystemSearch search) protected override void ExecuteDirectory(string directoryPath, SearchContext context) { - if (FileSystemHelpers.IsSubdirectory(Target, directoryPath)) + if (FileSystemUtilities.IsSubdirectory(Target, directoryPath)) { _logger.WriteWarning("Source directory cannot be subdirectory of a destination directory."); return; @@ -84,7 +84,7 @@ private void ExecuteOperation( if (fileMatch.IsDirectory || (baseDirectoryPath is not null && !Options.Flat)) { - Debug.Assert(sourcePath.StartsWith(baseDirectoryPath!, FileSystemHelpers.Comparison)); + Debug.Assert(sourcePath.StartsWith(baseDirectoryPath!, FileSystemUtilities.Comparison)); string relativePath = sourcePath.Substring(baseDirectoryPath!.Length + 1); @@ -128,7 +128,7 @@ private void ExecuteOperation( else if (directoryExists) { if (Options.StructureOnly - && FileSystemHelpers.AttributeEquals(sourcePath, destinationPath, Options.NoCompareAttributes)) + && FileSystemUtilities.AttributeEquals(sourcePath, destinationPath, Options.CompareAttributes)) { return null; } @@ -138,12 +138,12 @@ private void ExecuteOperation( } else if (fileExists) { - if (Options.CompareOptions != FileCompareOptions.None - && FileSystemHelpers.FileEquals( + if (Options.CompareProperties != FileCompareProperties.None + && FileSystemUtilities.FileEquals( sourcePath, destinationPath, - Options.CompareOptions, - Options.NoCompareAttributes, + Options.CompareProperties, + Options.CompareAttributes, Options.AllowedTimeDiff)) { return null; @@ -166,7 +166,7 @@ private void ExecuteOperation( && ((isDirectory && directoryExists) || (!isDirectory && fileExists))) { - destinationPath = FileSystemHelpers.CreateNewFilePath(destinationPath); + destinationPath = FileSystemUtilities.CreateNewFilePath(destinationPath); } if (!Options.OmitPath) @@ -238,7 +238,7 @@ private void ExecuteOperation( { if (Options.StructureOnly) { - FileSystemHelpers.UpdateAttributes(sourcePath, destinationPath); + FileSystemUtilities.UpdateAttributes(sourcePath, destinationPath); } else { diff --git a/src/CommandLine/Commands/CommonFindCommand`1.cs b/src/CommandLine/Commands/CommonFindCommand`1.cs index 7649c2fd..123ee8f4 100644 --- a/src/CommandLine/Commands/CommonFindCommand`1.cs +++ b/src/CommandLine/Commands/CommonFindCommand`1.cs @@ -28,7 +28,7 @@ protected CommonFindCommand(TOptions options, Logger logger) : base(options, log } } - public Filter? ContentFilter => Options.ContentFilter; + public Matcher? ContentFilter => Options.ContentFilter; private OutputSymbols Symbols => _symbols ??= OutputSymbols.Create(Options.HighlightOptions); @@ -99,7 +99,7 @@ protected virtual ContentWriterOptions CreateContentWriterOptions(string indent) HighlightOptions highlightOptions = Options.HighlightOptions; - if (ReferenceEquals(ContentFilter, Filter.EntireInput)) + if (ReferenceEquals(ContentFilter, Matcher.EntireInput)) highlightOptions &= ~HighlightOptions.DefaultOrMatch; return new ContentWriterOptions( @@ -199,7 +199,7 @@ protected virtual void ExecuteMatchWithContentCore( ColumnWidths? columnWidths = null) { if (!fileMatch.IsDirectory - && ContentFilter?.IsNegative == false + && ContentFilter?.Invert == false && !Options.OmitContent && _logger.ShouldWrite(Verbosity.Normal)) { @@ -210,7 +210,7 @@ protected virtual void ExecuteMatchWithContentCore( if (isPathDisplayed) WritePath(context, fileMatch, baseDirectoryPath, indent, columnWidths, includeNewline: false); - if (ContentFilter!.IsNegative + if (ContentFilter!.Invert || fileMatch.IsDirectory) { _logger.WriteLineIf(isPathDisplayed, Verbosity.Minimal); @@ -356,13 +356,13 @@ private void WritePath( ColumnWidths? columnWidths) { if (Options.PathDisplayStyle == PathDisplayStyle.Match - && fileMatch.NameMatch is not null - && !object.ReferenceEquals(fileMatch.NameMatch, Match.Empty)) + && fileMatch.Name is not null + && !object.ReferenceEquals(fileMatch.Name, Match.Empty)) { if (_logger.ShouldWrite(Verbosity.Minimal)) { _logger.Write(indent, Verbosity.Minimal); - _logger.Write(fileMatch.NameMatch.Value, (Options.HighlightMatch) ? Colors.Match_Path : default, Verbosity.Minimal); + _logger.Write(fileMatch.Name.Value, (Options.HighlightMatch) ? Colors.Match_Path : default, Verbosity.Minimal); } } else @@ -383,7 +383,7 @@ private void WriteContent( try { - Match match = fileMatch.ContentMatch!; + Match match = fileMatch.Content!; contentWriter = CommandLine.ContentWriter.CreateFind( contentDisplayStyle: Options.ContentDisplayStyle, diff --git a/src/CommandLine/Commands/CommonReplaceCommand`1.cs b/src/CommandLine/Commands/CommonReplaceCommand`1.cs index 1bf7461f..0c13d027 100644 --- a/src/CommandLine/Commands/CommonReplaceCommand`1.cs +++ b/src/CommandLine/Commands/CommonReplaceCommand`1.cs @@ -18,7 +18,7 @@ internal class CommonReplaceCommand : CommonFindCommand wher public CommonReplaceCommand(TOptions options, Logger logger) : base(options, logger) { - Debug.Assert(!options.ContentFilter!.IsNegative); + Debug.Assert(!options.ContentFilter!.Invert); } protected override bool CanDisplaySummary => Options.Input is null; @@ -51,7 +51,7 @@ private void ExecuteInput(SearchContext context, string input) { int count = 0; var maxReason = MaxReason.None; - Filter contentFilter = ContentFilter!; + Matcher contentFilter = ContentFilter!; Match? match = contentFilter.Match(input); if (match is not null) @@ -144,7 +144,7 @@ protected override void ExecuteMatchWithContentCore( groups = ListCache.GetInstance(); MaxReason maxReason = GetCaptures( - fileMatch.ContentMatch!, + fileMatch.Content!, writerOptions.GroupNumber, context, isPathDisplayed: false, @@ -232,7 +232,7 @@ private void ExecuteMatchWithContentCore( { MatchOutputInfo? outputInfo = Options.CreateOutputInfo( fileMatch.ContentText, - fileMatch.ContentMatch!, + fileMatch.Content!, ContentFilter!); if (Options.AskMode == AskMode.Value diff --git a/src/CommandLine/Commands/CopyCommand.cs b/src/CommandLine/Commands/CopyCommand.cs index f101320a..6c0eb063 100644 --- a/src/CommandLine/Commands/CopyCommand.cs +++ b/src/CommandLine/Commands/CopyCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using Orang.FileSystem; namespace Orang.CommandLine; diff --git a/src/CommandLine/Commands/DeleteCommand.cs b/src/CommandLine/Commands/DeleteCommand.cs index 323cf32b..db3dee11 100644 --- a/src/CommandLine/Commands/DeleteCommand.cs +++ b/src/CommandLine/Commands/DeleteCommand.cs @@ -12,7 +12,7 @@ public DeleteCommand(DeleteCommandOptions options, Logger logger) : base(options { } - protected override void OnSearchCreating(FileSystemSearch search) + protected override void OnSearchCreating(SearchState search) { search.CanRecurseMatch = false; } @@ -38,12 +38,10 @@ protected override void ExecuteMatchCore( { if (!Options.DryRun) { - FileSystemHelpers.Delete( + FileSystemUtilities.Delete( fileMatch, contentOnly: Options.ContentOnly, - includingBom: Options.IncludingBom, - filesOnly: Options.FilesOnly, - directoriesOnly: Options.DirectoriesOnly); + includingBom: Options.IncludingBom); } if (fileMatch.IsDirectory) diff --git a/src/CommandLine/Commands/FileSystemCommand`1.cs b/src/CommandLine/Commands/FileSystemCommand`1.cs index 947c59bb..08fd3b48 100644 --- a/src/CommandLine/Commands/FileSystemCommand`1.cs +++ b/src/CommandLine/Commands/FileSystemCommand`1.cs @@ -17,14 +17,14 @@ protected FileSystemCommand(TOptions options, Logger logger) : base(options, log { } - private FileSystemSearch? _search; + private SearchState? _search; private ProgressReporter? _progressReporter; - protected FileSystemSearch Search => _search ??= CreateSearch(); + protected SearchState Search => _search ??= CreateSearch(); private ProgressReporter? ProgressReporter => _progressReporter ??= CreateProgressReporter(); - public Filter? NameFilter => Options.NameFilter; + public Matcher? NameFilter => Options.NameFilter; protected virtual bool CanDisplaySummary => true; @@ -32,60 +32,166 @@ protected FileSystemCommand(TOptions options, Logger logger) : base(options, log public virtual bool CanUseResults => true; - protected virtual void OnSearchCreating(FileSystemSearch search) + protected virtual void OnSearchCreating(SearchState search) { } - private FileSystemSearch CreateSearch() + private SearchState CreateSearch() { - var filter = new FileSystemFilter( - name: Options.NameFilter, - part: Options.NamePart, - extension: Options.ExtensionFilter, - content: Options.ContentFilter, - properties: new FilePropertyFilter( - Options.FilePropertyOptions.CreationTimePredicate, - Options.FilePropertyOptions.ModifiedTimePredicate, - Options.FilePropertyOptions.SizePredicate), - attributes: Options.Attributes, - attributesToSkip: Options.AttributesToSkip, - emptyOption: Options.EmptyOption); + FilePropertyOptions properties = Options.FilePropertyOptions; + Func? filePredicate = null; + Func? directoryPredicate = null; - var directoryFilters = new List(); + if (Options.SearchTarget == SearchTarget.Directories) + { + if (properties.CreationTimePredicate is not null) + { + if (properties.ModifiedTimePredicate is not null) + { + directoryPredicate = di => properties.CreationTimePredicate(di.CreationTime) + && properties.ModifiedTimePredicate(di.LastWriteTime); + } + else + { + directoryPredicate = di => properties.CreationTimePredicate(di.CreationTime); + } + } + else if (properties.ModifiedTimePredicate is not null) + { + directoryPredicate = di => properties.ModifiedTimePredicate(di.LastWriteTime); + } + } + else if (properties.CreationTimePredicate is not null) + { + if (properties.ModifiedTimePredicate is not null) + { + if (properties.SizePredicate is not null) + { + filePredicate = fi => properties.CreationTimePredicate(fi.CreationTime) + && properties.ModifiedTimePredicate(fi.LastWriteTime) + && properties.SizePredicate(fi.Length); + } + else + { + filePredicate = fi => properties.CreationTimePredicate(fi.CreationTime) + && properties.ModifiedTimePredicate(fi.LastWriteTime); + } + } + else if (properties.SizePredicate is not null) + { + filePredicate = fi => properties.CreationTimePredicate(fi.CreationTime) + && properties.SizePredicate(fi.Length); + } + else + { + filePredicate = fi => properties.CreationTimePredicate(fi.CreationTime); + } + } + else if (properties.ModifiedTimePredicate is not null) + { + if (properties.SizePredicate is not null) + { + filePredicate = fi => properties.ModifiedTimePredicate(fi.LastWriteTime) + && properties.SizePredicate(fi.Length); + } + else + { + filePredicate = fi => properties.ModifiedTimePredicate(fi.LastWriteTime); + } + } + else if (properties.SizePredicate is not null) + { + filePredicate = fi => properties.SizePredicate(fi.Length); + } - if (Options.DirectoryFilter is not null) + FileMatcher? fileMatcher = null; + DirectoryMatcher? directoryMatcher = null; + + if (Options.SearchTarget != SearchTarget.Directories) { - var directoryFilter = new NameFilter( - name: Options.DirectoryFilter, - part: Options.DirectoryNamePart); + fileMatcher = new FileMatcher() + { + Name = Options.NameFilter, + NamePart = Options.NamePart, + Extension = Options.ExtensionFilter, + Content = Options.ContentFilter, + MatchFileInfo = filePredicate, + WithAttributes = Options.Attributes, + WithoutAttributes = Options.AttributesToSkip, + EmptyOption = Options.EmptyOption + }; + } - directoryFilters.Add(directoryFilter); + if (Options.SearchTarget != SearchTarget.Files) + { + directoryMatcher = new DirectoryMatcher() + { + Name = Options.NameFilter, + NamePart = Options.NamePart, + MatchDirectoryInfo = directoryPredicate, + WithAttributes = Options.Attributes, + WithoutAttributes = Options.AttributesToSkip, + EmptyOption = Options.EmptyOption + }; } - NameFilter? additionalDirectoryFilter = CreateAdditionalDirectoryFilter(); + Func? includeDirectory = null; + Func? excludeDirectory = null; - if (additionalDirectoryFilter is not null) - directoryFilters.Add(additionalDirectoryFilter); + Matcher? directoryFilter = Options.DirectoryFilter; - var options = new FileSystemSearchOptions( - searchTarget: Options.SearchTarget, - recurseSubdirectories: Options.RecurseSubdirectories, - defaultEncoding: Options.DefaultEncoding); + if (directoryFilter is not null) + { + if (directoryFilter.Invert) + { + excludeDirectory = DirectoryPredicate.Create(directoryFilter, Options.DirectoryNamePart); + } + else + { + includeDirectory = DirectoryPredicate.Create(directoryFilter, Options.DirectoryNamePart); + } + } - var search = new FileSystemSearch( - filter: filter, - directoryFilters: directoryFilters, - searchProgress: ProgressReporter, - options: options); + includeDirectory = CombinePredicates(includeDirectory, CreateIncludeDirectoryPredicate()); + excludeDirectory = CombinePredicates(excludeDirectory, CreateExcludeDirectoryPredicate()); + + var search = new SearchState(fileMatcher, directoryMatcher) + { + IncludeDirectory = includeDirectory, + ExcludeDirectory = excludeDirectory, + LogProgress = (ProgressReporter is not null) ? p => ProgressReporter.Report(p) : null, + RecurseSubdirectories = Options.RecurseSubdirectories, + DefaultEncoding = Options.DefaultEncoding, + }; OnSearchCreating(search); return search; + + static Func? CombinePredicates(Func? predicate, Func? predicate2) + { + if (predicate is not null) + { + if (predicate2 is not null) + { + return path => predicate(path) && predicate2(path); + } + + return predicate; + } + + return predicate2; + } + } + + protected virtual Func? CreateIncludeDirectoryPredicate(Func? predicate = null) + { + return predicate; } - protected virtual NameFilter? CreateAdditionalDirectoryFilter() + protected virtual Func? CreateExcludeDirectoryPredicate(Func? predicate = null) { - return null; + return predicate; } protected abstract void ExecuteDirectory(string directoryPath, SearchContext context); @@ -473,7 +579,7 @@ protected void WriteProperties(SearchContext context, FileMatch fileMatch, Colum && Options.FilePropertyOptions.IncludeSize) { size = (fileMatch.IsDirectory) - ? (context.DirectorySizeMap?[fileMatch.Path] ?? FileSystemHelpers.GetDirectorySize(fileMatch.Path)) + ? (context.DirectorySizeMap?[fileMatch.Path] ?? FileSystemUtilities.GetDirectorySize(fileMatch.Path)) : new FileInfo(fileMatch.Path).Length; context.Telemetry.FilesTotalSize += size; @@ -497,7 +603,7 @@ protected void WriteProperties(SearchContext context, FileMatch fileMatch, Colum if (size == -1) { size = (fileMatch.IsDirectory) - ? (context.DirectorySizeMap?[fileMatch.Path] ?? FileSystemHelpers.GetDirectorySize(fileMatch.Path)) + ? (context.DirectorySizeMap?[fileMatch.Path] ?? FileSystemUtilities.GetDirectorySize(fileMatch.Path)) : new FileInfo(fileMatch.Path).Length; } diff --git a/src/CommandLine/Commands/FindCommand`1.cs b/src/CommandLine/Commands/FindCommand`1.cs index eadd43e1..c0046364 100644 --- a/src/CommandLine/Commands/FindCommand`1.cs +++ b/src/CommandLine/Commands/FindCommand`1.cs @@ -109,7 +109,7 @@ protected override void ExecuteMatchWithContentCore( if (isPathDisplayed) WritePath(context, fileMatch, baseDirectoryPath, indent, columnWidths, includeNewline: false); - if (ContentFilter!.IsNegative + if (ContentFilter!.Invert || fileMatch.IsDirectory) { _logger.WriteLineIf(isPathDisplayed, Verbosity.Minimal); @@ -118,7 +118,7 @@ protected override void ExecuteMatchWithContentCore( { WriteContent( context, - fileMatch.ContentMatch!, + fileMatch.Content!, fileMatch.ContentText, writerOptions, fileMatch, @@ -150,10 +150,10 @@ protected override void ExecuteMatchCore( } if (_aggregate?.Storage is not null - && fileMatch.NameMatch is not null - && !object.ReferenceEquals(fileMatch.NameMatch, Match.Empty)) + && fileMatch.Name is not null + && !object.ReferenceEquals(fileMatch.Name, Match.Empty)) { - _aggregate.Storage.Add(fileMatch.NameMatch.Value); + _aggregate.Storage.Add(fileMatch.Name.Value); _aggregate.Sections?.Add(new StorageSection(fileMatch, baseDirectoryPath, 0)); } diff --git a/src/CommandLine/Commands/HelpCommand.cs b/src/CommandLine/Commands/HelpCommand.cs index 1e1b519b..5126afa5 100644 --- a/src/CommandLine/Commands/HelpCommand.cs +++ b/src/CommandLine/Commands/HelpCommand.cs @@ -28,7 +28,7 @@ protected override CommandResult ExecuteCore(CancellationToken cancellationToken online: Options.Online, manual: Options.Manual, verbose: _logger.ConsoleOut.Verbosity > Verbosity.Normal, - filter: Options.Filter); + matcher: Options.Matcher); } catch (ArgumentException ex) { @@ -44,7 +44,7 @@ private void WriteHelp( bool online, bool manual, bool verbose, - Filter? filter = null) + Matcher? matcher = null) { var isCompoundCommand = false; string? commandName2 = commandName.FirstOrDefault(); @@ -71,15 +71,15 @@ private void WriteHelp( if (command is null) throw new InvalidOperationException($"Command '{string.Join(' ', commandName)}' does not exist."); - WriteCommandHelp(command, _logger, verbose: verbose, filter: filter); + WriteCommandHelp(command, _logger, verbose: verbose, matcher: matcher); } else if (manual) { - WriteManual(_logger, verbose: verbose, filter: filter); + WriteManual(_logger, verbose: verbose, matcher: matcher); } else { - WriteCommandsHelp(_logger, verbose: verbose, filter: filter); + WriteCommandsHelp(_logger, verbose: verbose, matcher: matcher); } } @@ -124,9 +124,9 @@ private void OpenHelpInBrowser(string? commandName) } } - public static void WriteCommandHelp(Command command, Logger logger, bool verbose = false, Filter? filter = null) + public static void WriteCommandHelp(Command command, Logger logger, bool verbose = false, Matcher? matcher = null) { - var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(filter: filter)); + var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(matcher: matcher)); IEnumerable options = command.Options; @@ -141,7 +141,7 @@ public static void WriteCommandHelp(Command command, Logger logger, bool verbose command = command.WithOptions(options); - CommandHelp commandHelp = CommandHelp.Create(command, OptionValueProviders.Providers, filter: filter); + CommandHelp commandHelp = CommandHelp.Create(command, OptionValueProviders.Providers, matcher: matcher); writer.WriteCommand(commandHelp); @@ -166,13 +166,13 @@ public static void WriteCommandHelp(Command command, Logger logger, bool verbose } } - public static void WriteCommandsHelp(Logger logger, bool verbose = false, Filter? filter = null) + public static void WriteCommandsHelp(Logger logger, bool verbose = false, Matcher? matcher = null) { IEnumerable commands = LoadCommands().Where(f => f.Name != "help"); - CommandsHelp commandsHelp = CommandsHelp.Create(commands, OptionValueProviders.Providers, filter: filter); + CommandsHelp commandsHelp = CommandsHelp.Create(commands, OptionValueProviders.Providers, matcher: matcher); - var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(filter: filter)); + var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(matcher: matcher)); writer.WriteCommands(commandsHelp); @@ -183,13 +183,13 @@ public static void WriteCommandsHelp(Logger logger, bool verbose = false, Filter logger.WriteLine(GetFooterText()); } - private static void WriteManual(Logger logger, bool verbose = false, Filter? filter = null) + private static void WriteManual(Logger logger, bool verbose = false, Matcher? matcher = null) { IEnumerable commands = LoadCommands(); - var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(filter: filter)); + var writer = new ConsoleHelpWriter(logger, new HelpWriterOptions(matcher: matcher)); - IEnumerable commandHelps = commands.Select(f => CommandHelp.Create(f, filter: filter)) + IEnumerable commandHelps = commands.Select(f => CommandHelp.Create(f, matcher: matcher)) .Where(f => f.Arguments.Any() || f.Options.Any()) .ToImmutableArray(); @@ -202,7 +202,7 @@ private static void WriteManual(Logger logger, bool verbose = false, Filter? fil values = HelpProvider.GetOptionValues( commandHelps.SelectMany(f => f.Command.Options), OptionValueProviders.Providers, - filter); + matcher); var commandsHelp = new CommandsHelp(commandItems, values); @@ -239,7 +239,7 @@ private static void WriteManual(Logger logger, bool verbose = false, Filter? fil values = HelpProvider.GetOptionValues( commands.Select(f => CommandHelp.Create(f)).SelectMany(f => f.Command.Options), OptionValueProviders.Providers, - filter); + matcher); } } diff --git a/src/CommandLine/Commands/MoveCommand.cs b/src/CommandLine/Commands/MoveCommand.cs index 057f47ca..620389c8 100644 --- a/src/CommandLine/Commands/MoveCommand.cs +++ b/src/CommandLine/Commands/MoveCommand.cs @@ -1,6 +1,7 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; +using Orang.FileSystem; namespace Orang.CommandLine; diff --git a/src/CommandLine/Commands/RegexListCommand.cs b/src/CommandLine/Commands/RegexListCommand.cs index 1f1b21a9..42251132 100644 --- a/src/CommandLine/Commands/RegexListCommand.cs +++ b/src/CommandLine/Commands/RegexListCommand.cs @@ -35,7 +35,7 @@ private CommandResult ListPatterns() { IEnumerable items = SyntaxItems.Load(); - Filter? filter = Options.Filter; + Matcher? matcher = Options.Matcher; ImmutableArray sections = Options.Sections; @@ -46,10 +46,10 @@ private CommandResult ListPatterns() .ToImmutableArray(); } - if (filter is not null) + if (matcher is not null) { - items = items.Where(f => filter.IsMatch(f.Text) - || filter.IsMatch(f.Description)); + items = items.Where(f => matcher.IsMatch(f.Text) + || matcher.IsMatch(f.Description)); } if (sections.Any()) @@ -191,12 +191,12 @@ private IEnumerable GetPatterns(char ch) inCharGroup: Options.InCharGroup, options: Options.RegexOptions); - Filter? filter = Options.Filter; + Matcher? matcher = Options.Matcher; - if (filter is not null) + if (matcher is not null) { - patterns = patterns.Where(f => filter.IsMatch(f.Pattern) - || (!string.IsNullOrEmpty(f.Description) && filter.IsMatch(f.Description))); + patterns = patterns.Where(f => matcher.IsMatch(f.Pattern) + || (!string.IsNullOrEmpty(f.Description) && matcher.IsMatch(f.Description))); } ImmutableArray sections = Options.Sections; diff --git a/src/CommandLine/Commands/RegexMatchCommand.cs b/src/CommandLine/Commands/RegexMatchCommand.cs index 95db7a94..0099203c 100644 --- a/src/CommandLine/Commands/RegexMatchCommand.cs +++ b/src/CommandLine/Commands/RegexMatchCommand.cs @@ -15,7 +15,7 @@ protected override CommandResult ExecuteCore(CancellationToken cancellationToken { MatchData matchData = MatchData.Create( Options.Input, - Options.Filter.Regex, + Options.Matcher.Regex, Options.MaxCount, cancellationToken); diff --git a/src/CommandLine/Commands/RegexSplitCommand.cs b/src/CommandLine/Commands/RegexSplitCommand.cs index 03e28b63..df670d3a 100644 --- a/src/CommandLine/Commands/RegexSplitCommand.cs +++ b/src/CommandLine/Commands/RegexSplitCommand.cs @@ -14,7 +14,7 @@ public RegexSplitCommand(RegexSplitCommandOptions options, Logger logger) : base protected override CommandResult ExecuteCore(CancellationToken cancellationToken = default) { SplitData splitData = SplitData.Create( - Options.Filter.Regex, + Options.Matcher.Regex, Options.Input, Options.MaxCount, omitGroups: Options.OmitGroups, diff --git a/src/CommandLine/Commands/RenameCommand.cs b/src/CommandLine/Commands/RenameCommand.cs index 5c79ab51..b80ee11c 100644 --- a/src/CommandLine/Commands/RenameCommand.cs +++ b/src/CommandLine/Commands/RenameCommand.cs @@ -1,7 +1,6 @@ // Copyright (c) Josef Pihrt. 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 System.Diagnostics; using System.IO; using Orang.FileSystem; @@ -13,7 +12,7 @@ internal class RenameCommand : DeleteOrRenameCommand { public RenameCommand(RenameCommandOptions options, Logger logger) : base(options, logger) { - Debug.Assert(options.NameFilter?.IsNegative == false); + Debug.Assert(options.NameFilter?.Invert == false); if (_logger.ShouldWrite(Verbosity.Minimal)) { @@ -35,10 +34,9 @@ public ConflictResolution ConflictResolution private PathWriter? PathWriter { get; } - protected override void OnSearchCreating(FileSystemSearch search) + protected override void OnSearchCreating(SearchState search) { - search.DisallowEnumeration = !Options.DryRun; - search.MatchPartOnly = true; + search.SupportsEnumeration = Options.DryRun; base.OnSearchCreating(search); } @@ -51,35 +49,36 @@ protected override void ExecuteMatchCore( { string indent = GetPathIndent(baseDirectoryPath); - (List replaceItems, MaxReason maxReason) = ReplaceHelpers.GetReplaceItems( - fileMatch.NameMatch!, - Options.ReplaceOptions, + ReplaceResult result = fileMatch.GetReplaceResult( + Options.Replacer, count: Options.MaxMatchesInFile, predicate: NameFilter!.Predicate, cancellationToken: context.CancellationToken); - string path = fileMatch.Path; - - string newName = ReplaceHelpers.GetNewName(fileMatch, replaceItems); + string newValue = result.NewName; - if (string.IsNullOrWhiteSpace(newName)) + if (string.IsNullOrWhiteSpace(newValue)) { _logger.WriteLine($"{indent}New file name cannot be empty or contains only white-space.", Colors.Message_Warning); return; } + string path = fileMatch.Path; + bool changed = string.Compare( path, fileMatch.NameSpan.Start, - newName, + newValue, 0, - Math.Max(fileMatch.NameSpan.Length, newName.Length), + Math.Max(fileMatch.NameSpan.Length, newValue.Length), StringComparison.Ordinal) != 0; bool isInvalidName = changed - && FileSystemHelpers.ContainsInvalidFileNameChars(newName); + && FileSystemUtilities.ContainsInvalidFileNameChars(newValue); - string newPath = path.Substring(0, fileMatch.NameSpan.Start) + newName; + string newPath = path.Substring(0, fileMatch.NameSpan.Start) + + newValue + + path.Substring(fileMatch.NameSpan.Start + fileMatch.NameSpan.Length); if (Options.Interactive || (!Options.OmitPath && changed)) @@ -91,28 +90,28 @@ protected override void ExecuteMatchCore( PathWriter?.WritePath( fileMatch, - replaceItems, + result.Items, replaceColors, baseDirectoryPath, indent); WriteProperties(context, fileMatch, columnWidths); - _logger.WriteFilePathEnd(replaceItems.Count, maxReason, Options.IncludeCount); + _logger.WriteFilePathEnd(result.Count, result.MaxReason, Options.IncludeCount); } - ListCache.Free(replaceItems); + ListCache.Free(result.Items); if (Options.Interactive) { - string newName2 = ConsoleHelpers.ReadUserInput(newName, indent + "New name: "); + string newName2 = ConsoleHelpers.ReadUserInput(newValue, indent + "New name: "); - newPath = newPath.Substring(0, newPath.Length - newName.Length) + newName2; + newPath = newPath.Substring(0, newPath.Length - newValue.Length) + newName2; changed = !string.Equals(path, newPath, StringComparison.Ordinal); if (changed) { - isInvalidName = FileSystemHelpers.ContainsInvalidFileNameChars(newName2); + isInvalidName = FileSystemUtilities.ContainsInvalidFileNameChars(newName2); if (isInvalidName) { diff --git a/src/CommandLine/Commands/ReplaceCommand.cs b/src/CommandLine/Commands/ReplaceCommand.cs index 0223fa5d..a83a8bd0 100644 --- a/src/CommandLine/Commands/ReplaceCommand.cs +++ b/src/CommandLine/Commands/ReplaceCommand.cs @@ -8,6 +8,6 @@ internal class ReplaceCommand : CommonReplaceCommand { public ReplaceCommand(ReplaceCommandOptions options, Logger logger) : base(options, logger) { - Debug.Assert(!options.ContentFilter!.IsNegative); + Debug.Assert(!options.ContentFilter!.Invert); } } diff --git a/src/CommandLine/Commands/SpellcheckCommand.cs b/src/CommandLine/Commands/SpellcheckCommand.cs index 567cd40c..27196ef1 100644 --- a/src/CommandLine/Commands/SpellcheckCommand.cs +++ b/src/CommandLine/Commands/SpellcheckCommand.cs @@ -75,9 +75,9 @@ private List GetFilteredSpans(List groups, CancellationToken foreach (Capture group in groups) { - foreach (Filter filter in SpellcheckState.Filters) + foreach (Matcher matcher in SpellcheckState.Matchers) { - Match? match = filter.Match(group.Value); + Match? match = matcher.Match(group.Value); if (match is not null) { @@ -86,9 +86,9 @@ private List GetFilteredSpans(List groups, CancellationToken MaxReason _ = CaptureFactory.GetCaptures( ref captures, match, - filter.GroupNumber, + matcher.GroupNumber, count: 0, - filter.Predicate, + matcher.Predicate, cancellationToken); if (allSpans is null) diff --git a/src/CommandLine/Commands/SyncCommand.cs b/src/CommandLine/Commands/SyncCommand.cs index db559722..56926207 100644 --- a/src/CommandLine/Commands/SyncCommand.cs +++ b/src/CommandLine/Commands/SyncCommand.cs @@ -45,7 +45,7 @@ protected override string GetQuestionText(bool isDirectory) return (isDirectory) ? "Sync directory?" : "Sync file?"; } - protected override void OnSearchCreating(FileSystemSearch search) + protected override void OnSearchCreating(SearchState search) { search.CanRecurseMatch = true; } @@ -55,7 +55,7 @@ protected override void ExecuteDirectory(string directoryPath, SearchContext con bool secondExists = Directory.Exists(Options.Target); if (secondExists) - _destinationPaths = new HashSet(FileSystemHelpers.Comparer); + _destinationPaths = new HashSet(FileSystemUtilities.Comparer); try { @@ -123,7 +123,7 @@ void ExecuteOperation() bool fileExists = File.Exists(destinationPath); bool directoryExists = !fileExists && Directory.Exists(destinationPath); bool? preferLeft = null; - FileCompareOptions? diffProperty = null; + FileCompareProperties? diffProperty = null; if (isDirectory) { @@ -134,7 +134,7 @@ void ExecuteOperation() if (_isSecondToFirst) return; - if (FileSystemHelpers.AttributeEquals(sourcePath, destinationPath, Options.NoCompareAttributes)) + if (FileSystemUtilities.AttributeEquals(sourcePath, destinationPath, Options.CompareAttributes)) return; } } @@ -143,9 +143,9 @@ void ExecuteOperation() if (_isSecondToFirst) return; - if ((Options.CompareOptions & FileCompareOptions.ModifiedTime) != 0) + if ((Options.CompareProperties & FileCompareProperties.ModifiedTime) != 0) { - int diff = FileSystemHelpers.CompareLastWriteTimeUtc( + int diff = FileSystemUtilities.CompareLastWriteTimeUtc( sourcePath, destinationPath, Options.AllowedTimeDiff); @@ -168,28 +168,28 @@ void ExecuteOperation() { if (fileExists) { - if (Options.CompareOptions != FileCompareOptions.None) + if (Options.CompareProperties != FileCompareProperties.None) { - diffProperty = FileSystemHelpers.CompareFiles( + diffProperty = FileSystemUtilities.CompareFiles( sourcePath, destinationPath, - Options.CompareOptions, - Options.NoCompareAttributes, + Options.CompareProperties, + Options.CompareAttributes, Options.AllowedTimeDiff); - if (diffProperty == FileCompareOptions.None) + if (diffProperty == FileCompareProperties.None) return; } } else if (Options.DetectRename - && (Options.CompareOptions & FileCompareOptions.Content) != 0) + && (Options.CompareProperties & FileCompareProperties.Content) != 0) { if (_directoryData is null) { _directoryData = new DirectoryData(); _directoryData.Load(Path.GetDirectoryName(destinationPath)!); } - else if (!FileSystemHelpers.IsParentDirectory(_directoryData.Path!, destinationPath)) + else if (!FileSystemUtilities.IsParentDirectory(_directoryData.Path!, destinationPath)) { _directoryData.Load(Path.GetDirectoryName(destinationPath)!); } @@ -404,7 +404,7 @@ private void ExecuteFileOperations( bool fileExists, bool directoryExists, bool preferLeft, - FileCompareOptions? diffProperty, + FileCompareProperties? diffProperty, string indent) { if (preferLeft) @@ -413,7 +413,7 @@ private void ExecuteFileOperations( { WritePath(context, destinationPath, OperationKind.Update, indent); - if (diffProperty == FileCompareOptions.Attributes) + if (diffProperty == FileCompareProperties.Attributes) { // update attributes File.SetAttributes(destinationPath, File.GetAttributes(sourcePath)); @@ -440,7 +440,7 @@ private void ExecuteFileOperations( WritePath(context, destinationPath, OperationKind.Add, indent); if (!fileExists - || diffProperty != FileCompareOptions.Attributes) + || diffProperty != FileCompareProperties.Attributes) { CopyFile(sourcePath, destinationPath); } @@ -459,7 +459,7 @@ private void ExecuteFileOperations( WritePath(context, sourcePath, (fileExists) ? OperationKind.Update : OperationKind.Delete, indent); if (!fileExists - || diffProperty != FileCompareOptions.Attributes) + || diffProperty != FileCompareProperties.Attributes) { DeleteFile(sourcePath); @@ -469,7 +469,7 @@ private void ExecuteFileOperations( if (fileExists) { - if (diffProperty == FileCompareOptions.Attributes) + if (diffProperty == FileCompareProperties.Attributes) { // update attributes File.SetAttributes(sourcePath, File.GetAttributes(destinationPath)); @@ -541,7 +541,7 @@ private void CopyFile(string sourcePath, string destinationPath) private void UpdateAttributes(string sourcePath, string destinationPath) { if (!DryRun) - FileSystemHelpers.UpdateAttributes(sourcePath, destinationPath); + FileSystemUtilities.UpdateAttributes(sourcePath, destinationPath); } protected override void ExecuteOperation(string sourcePath, string destinationPath) diff --git a/src/CommandLine/ConsoleHelpWriter.cs b/src/CommandLine/ConsoleHelpWriter.cs index 57e10d60..a7cfd012 100644 --- a/src/CommandLine/ConsoleHelpWriter.cs +++ b/src/CommandLine/ConsoleHelpWriter.cs @@ -21,18 +21,18 @@ public ConsoleHelpWriter(Logger logger, HelpWriterOptions? options = null) : bas public ContentTextWriter ContentWriter { get; } - public Filter? Filter => Options.Filter; + public Matcher? Matcher => Options.Matcher; internal ContentWriterOptions? ContentWriterOptions { get { if (_contentWriterOptions is null - && Filter is not null) + && Matcher is not null) { _contentWriterOptions = new ContentWriterOptions( format: new OutputDisplayFormat(ContentDisplayStyle.AllLines), - (Filter.GroupNumber >= 0) ? new GroupDefinition(Filter.GroupNumber, Filter.GroupName) : default, + (Matcher.GroupNumber >= 0) ? new GroupDefinition(Matcher.GroupNumber, Matcher.GroupName) : default, indent: ""); } @@ -81,7 +81,7 @@ public override void WriteCommands(CommandsHelp commands) WriteEndCommands(commands); } - else if (Options.Filter is not null) + else if (Options.Matcher is not null) { WriteLine("No command found"); } @@ -137,15 +137,15 @@ protected override void WriteTextLine(HelpItem helpItem) { string value = helpItem.Text; - if (Filter is not null) + if (Matcher is not null) { - Match? match = Filter.Match(value); + Match? match = Matcher.Match(value); if (match is not null) { List captures = ListCache.GetInstance(); - CaptureFactory.GetCaptures(ref captures, match, Filter.GroupNumber); + CaptureFactory.GetCaptures(ref captures, match, Matcher.GroupNumber); var writer = new AllLinesContentWriter(value, ContentWriter, ContentWriterOptions!); diff --git a/src/CommandLine/DiagnosticWriter.cs b/src/CommandLine/DiagnosticWriter.cs index 7e2acb2d..cfb40369 100644 --- a/src/CommandLine/DiagnosticWriter.cs +++ b/src/CommandLine/DiagnosticWriter.cs @@ -154,7 +154,7 @@ internal void WriteCopyCommand(CopyCommandOptions options) WriteOption("ask", options.AskMode); WriteOption("attributes", options.Attributes); WriteOption("attributes to skip", options.AttributesToSkip); - WriteOption("compare", options.CompareOptions); + WriteOption("compare", options.CompareProperties); WriteOption("conflict resolution", options.ConflictResolution); WriteFilter("content filter", options.ContentFilter); #if DEBUG @@ -214,7 +214,6 @@ internal void WriteDeleteCommand(DeleteCommandOptions options) WriteContext("context", options.Format.LineContext); WriteOption("count", options.Format.Includes(DisplayParts.Count)); WriteEncoding("default encoding", options.DefaultEncoding); - WriteOption("directories only", options.DirectoriesOnly); WriteFilter("directory filter", options.DirectoryFilter, options.DirectoryNamePart); WriteOption("dry run", options.DryRun); WriteOption("empty", options.EmptyOption); @@ -224,7 +223,6 @@ internal void WriteDeleteCommand(DeleteCommandOptions options) options.SizePredicate, options.CreationTimePredicate, options.ModifiedTimePredicate); - WriteOption("files only", options.FilesOnly); WriteOption("highlight options", options.HighlightOptions); WriteOption("including bom", options.IncludingBom); WriteOption("line number", options.Format.Includes(LineDisplayOptions.IncludeLineNumber)); @@ -287,7 +285,7 @@ internal void WriteHelpCommand(HelpCommandOptions options) { WriteOption("command", string.Join(' ', options.Command)); WriteOption("manual", options.Manual); - WriteFilter("filter", options.Filter); + WriteFilter("filter", options.Matcher); } internal void WriteMoveCommand(MoveCommandOptions options) @@ -299,7 +297,7 @@ internal void WriteMoveCommand(MoveCommandOptions options) WriteOption("ask", options.AskMode); WriteOption("attributes", options.Attributes); WriteOption("attributes to skip", options.AttributesToSkip); - WriteOption("compare", options.CompareOptions); + WriteOption("compare", options.CompareProperties); WriteOption("conflict resolution", options.ConflictResolution); WriteFilter("content filter", options.ContentFilter); #if DEBUG @@ -357,7 +355,7 @@ internal void WriteRegexMatchCommand(RegexMatchCommandOptions options) #if DEBUG WriteOption("content separator", options.Format.Separator); #endif - WriteFilter("filter", options.Filter); + WriteFilter("filter", options.Matcher); WriteOption("highlight options", options.HighlightOptions); WriteInput(options.Input); WriteOption("max count", options.MaxCount); @@ -368,7 +366,7 @@ internal void WriteRegexListCommand(RegexListCommandOptions options) { WriteOption("char", options.Value); WriteOption("char group", options.InCharGroup); - WriteFilter("filter", options.Filter); + WriteFilter("filter", options.Matcher); WriteOption("regex options", options.RegexOptions); WriteOption("sections", options.Sections); } @@ -382,7 +380,7 @@ internal void WriteRegexSplitCommand(RegexSplitCommandOptions options) #if DEBUG WriteOption("content separator", options.Format.Separator); #endif - WriteFilter("filter", options.Filter); + WriteFilter("filter", options.Matcher); WriteOption("highlight options", options.HighlightOptions); WriteInput(options.Input); WriteOption("max count", options.MaxCount); @@ -411,7 +409,7 @@ internal void WriteRenameCommand(RenameCommandOptions options) WriteFilter("directory filter", options.DirectoryFilter, options.DirectoryNamePart); WriteOption("dry run", options.DryRun); WriteOption("empty", options.EmptyOption); - WriteEvaluator("evaluator", options.ReplaceOptions.MatchEvaluator); + WriteEvaluator("evaluator", options.Replacer.MatchEvaluator); WriteFilter("extension filter", options.ExtensionFilter); WriteFilePropertyFilter( "file properties", @@ -422,14 +420,14 @@ internal void WriteRenameCommand(RenameCommandOptions options) WriteOption("interactive", options.Interactive); WriteOption("line number", options.Format.Includes(LineDisplayOptions.IncludeLineNumber)); WriteOption("max matching files", options.MaxMatchingFiles); - WriteReplaceModify("modify", options.ReplaceOptions); + WriteReplaceModify("modify", options.Replacer); WriteFilter("name filter", options.NameFilter, options.NamePart); WriteOption("path mode", options.Format.PathDisplayStyle); WritePaths("paths", options.Paths); WriteOption("progress", options.Progress); WriteProperties(options); WriteOption("recurse subdirectories", options.RecurseSubdirectories); - WriteOption("replacement", options.ReplaceOptions.Replacement); + WriteOption("replacement", options.Replacer.Replacement); WriteOption("search target", options.SearchTarget); WriteSortOptions("sort", options.SortOptions); WriteOption("summary", options.Format.Includes(DisplayParts.Summary)); @@ -437,7 +435,7 @@ internal void WriteRenameCommand(RenameCommandOptions options) internal void WriteReplaceCommand(ReplaceCommandOptions options) { - var replaceOptions = (ReplaceOptions)options.Replacer; + var replacer = (Replacer)options.Replacer; WriteOption("align columns", options.AlignColumns); WriteOption("ask", options.AskMode); @@ -457,7 +455,7 @@ internal void WriteReplaceCommand(ReplaceCommandOptions options) WriteFilter("directory filter", options.DirectoryFilter, options.DirectoryNamePart); WriteOption("dry run", options.DryRun); WriteOption("empty", options.EmptyOption); - WriteEvaluator("evaluator", replaceOptions.MatchEvaluator); + WriteEvaluator("evaluator", replacer.MatchEvaluator); WriteFilter("extension filter", options.ExtensionFilter); WriteFilePropertyFilter( "file properties", @@ -470,14 +468,14 @@ internal void WriteReplaceCommand(ReplaceCommandOptions options) WriteOption("line number", options.Format.Includes(LineDisplayOptions.IncludeLineNumber)); WriteOption("max matching files", options.MaxMatchingFiles); WriteOption("max matches in file", options.MaxMatchesInFile); - WriteReplaceModify("modify", replaceOptions); + WriteReplaceModify("modify", replacer); WriteFilter("name filter", options.NameFilter, options.NamePart); WriteOption("path mode", options.Format.PathDisplayStyle); WritePaths("paths", options.Paths); WriteOption("progress", options.Progress); WriteProperties(options); WriteOption("recurse subdirectories", options.RecurseSubdirectories); - WriteOption("replacement", replaceOptions.Replacement); + WriteOption("replacement", replacer.Replacement); WriteOption("search target", options.SearchTarget); WriteSortOptions("sort", options.SortOptions); WriteOption("summary", options.Format.Includes(DisplayParts.Summary)); @@ -545,7 +543,7 @@ internal void WriteSyncCommand(SyncCommandOptions options) WriteOption("ask", options.AskMode); WriteOption("attributes", options.Attributes); WriteOption("attributes to skip", options.AttributesToSkip); - WriteOption("compare", options.CompareOptions); + WriteOption("compare", options.CompareProperties); WriteOption("conflict resolution", options.ConflictResolution); WriteFilter("content filter", options.ContentFilter); #if DEBUG @@ -653,6 +651,13 @@ private void WriteOption(string name, TEnum value) where TEnum : Enum WriteLine(); } + private void WriteOption(string name, object? value) + { + WriteName(name); + WriteValue(value?.ToString()); + WriteLine(); + } + private void WriteOption(string name, ImmutableArray values) where TEnum : Enum { WriteName(name); @@ -685,27 +690,27 @@ private void WriteEncoding(string name, Encoding encoding) WriteLine(); } - private void WriteFilter(string name, Filter? filter, FileNamePart? part = null) + private void WriteFilter(string name, Matcher? matcher, FileNamePart? part = null) { WriteName(name); - if (filter is null) + if (matcher is null) { WriteNullValue(); WriteLine(); return; } - WriteRegex(filter.Regex); + WriteRegex(matcher.Regex); WriteIndent(); WriteName("negative"); WriteIndent(); - WriteLine(filter.IsNegative.ToString().ToLowerInvariant()); + WriteLine(matcher.Invert.ToString().ToLowerInvariant()); WriteIndent(); WriteName("group"); - if (string.IsNullOrEmpty(filter.GroupName)) + if (string.IsNullOrEmpty(matcher.GroupName)) { WriteNullValue(); WriteLine(); @@ -713,7 +718,7 @@ private void WriteFilter(string name, Filter? filter, FileNamePart? part = null) else { WriteIndent(); - WriteLine(filter.GroupName); + WriteLine(matcher.GroupName); } if (part is not null) @@ -903,24 +908,24 @@ private void WriteEvaluator(string name, MatchEvaluator? matchEvaluator) } } - private void WriteReplaceModify(string name, ReplaceOptions options) + private void WriteReplaceModify(string name, Replacer replacer) { WriteName(name); - if (options.Functions != ReplaceFunctions.None) + if (replacer.Functions != ReplaceFunctions.None) { - if (options.CultureInvariant) + if (replacer.CultureInvariant) { - WriteValue(nameof(options.CultureInvariant) + ", " + options.Functions.ToString()); + WriteValue(nameof(replacer.CultureInvariant) + ", " + replacer.Functions.ToString()); } else { - WriteValue(options.Functions.ToString()); + WriteValue(replacer.Functions.ToString()); } } - else if (options.CultureInvariant) + else if (replacer.CultureInvariant) { - WriteValue(nameof(options.CultureInvariant)); + WriteValue(nameof(replacer.CultureInvariant)); } else { diff --git a/src/CommandLine/OptionValueProviders.cs b/src/CommandLine/OptionValueProviders.cs index d2c66521..7f8ca573 100644 --- a/src/CommandLine/OptionValueProviders.cs +++ b/src/CommandLine/OptionValueProviders.cs @@ -541,15 +541,15 @@ internal static class OptionValueProviders public static OptionValueProvider FileCompareOptionsProvider { get; } = new( MetaValues.CompareOptions, - SimpleOptionValue.Create(FileCompareOptions.None, description: "Compare files only by name."), - SimpleOptionValue.Create(FileCompareOptions.Attributes, description: "Compare file attributes."), - SimpleOptionValue.Create(FileCompareOptions.Content, description: "Compare file content."), + SimpleOptionValue.Create(FileCompareProperties.None, description: "Compare files only by name."), + SimpleOptionValue.Create(FileCompareProperties.Attributes, description: "Compare file attributes."), + SimpleOptionValue.Create(FileCompareProperties.Content, description: "Compare file content."), SimpleOptionValue.Create( - FileCompareOptions.ModifiedTime, + FileCompareProperties.ModifiedTime, shortValue: "mt", helpValue: "m[odified-]t[ime]", description: "Compare time a file was last modified."), - SimpleOptionValue.Create(FileCompareOptions.Size, description: "Compare file size.") + SimpleOptionValue.Create(FileCompareProperties.Size, description: "Compare file size.") ); public static OptionValueProvider SyncConflictResolutionProvider { get; } = new( diff --git a/src/CommandLine/OutputWriter/OutputWriter.cs b/src/CommandLine/OutputWriter/OutputWriter.cs index cef19eb1..2a93cb48 100644 --- a/src/CommandLine/OutputWriter/OutputWriter.cs +++ b/src/CommandLine/OutputWriter/OutputWriter.cs @@ -36,7 +36,7 @@ public int WriteMatches( if (matchItems.Count == 0) return default; - int groupNumber = options.Filter.GroupNumber; + int groupNumber = options.Matcher.GroupNumber; OutputSymbols symbols = OutputSymbols.Create(HighlightOptions); @@ -64,7 +64,7 @@ public int WriteMatches( .ThenBy(f => f.Number) .SelectMany(f => f.CaptureItems) .Select(f => f.Value) - .Where(f => options.Filter.Predicate?.Invoke(f) != false) + .Where(f => options.Matcher.Predicate?.Invoke(f) != false) .Modify(options.ModifyOptions) .GetEnumerator()) { @@ -121,7 +121,7 @@ public int WriteMatches( { CaptureItem captureItem = groupItem.CaptureItems[j]; - if (options.Filter.Predicate?.Invoke(captureItem.Value) == false) + if (options.Matcher.Predicate?.Invoke(captureItem.Value) == false) continue; if (addSeparator) @@ -168,7 +168,7 @@ public int WriteMatches( { cancellationToken.ThrowIfCancellationRequested(); - if (options.Filter.Predicate?.Invoke(matchItem.Value) == false) + if (options.Matcher.Predicate?.Invoke(matchItem.Value) == false) continue; valueWriter.Write(input, lastPos, matchItem.Index - lastPos, symbols: null); @@ -224,7 +224,7 @@ public int WriteSplits( { cancellationToken.ThrowIfCancellationRequested(); - bool success = options.Filter.Predicate?.Invoke(en.Current) != false; + bool success = options.Matcher.Predicate?.Invoke(en.Current) != false; if (success) { @@ -272,7 +272,7 @@ public int WriteSplits( SplitItem item = en.Current; - bool success = options.Filter.Predicate?.Invoke(item.Value) != false; + bool success = options.Matcher.Predicate?.Invoke(item.Value) != false; if (success) { @@ -308,7 +308,7 @@ public int WriteSplits( { cancellationToken.ThrowIfCancellationRequested(); - if (options.Filter.Predicate?.Invoke(item.Value) == false) + if (options.Matcher.Predicate?.Invoke(item.Value) == false) continue; valueWriter.Write(input, lastPos, item.Index - lastPos, symbols: null); diff --git a/src/CommandLine/ParseContext.cs b/src/CommandLine/ParseContext.cs index a78eb901..1aa0f5ac 100644 --- a/src/CommandLine/ParseContext.cs +++ b/src/CommandLine/ParseContext.cs @@ -534,7 +534,7 @@ public bool TryParseModifier( } if ((options & ModifierOptions.FromFile) != 0 - && !FileSystemHelpers.TryReadAllText(value, out value!, ex => Logger.WriteError(ex))) + && !FileSystemUtilities.TryReadAllText(value, out value!, ex => Logger.WriteError(ex))) { return false; } @@ -567,9 +567,9 @@ public bool TryParseReplaceOptions( string optionName, string? replacement, MatchEvaluator? matchEvaluator, - [NotNullWhen(true)] out ReplaceOptions? replaceOptions) + [NotNullWhen(true)] out Replacer? replacer) { - replaceOptions = null; + replacer = null; var replaceFlags = ReplaceFlags.None; if (values is not null @@ -598,14 +598,14 @@ public bool TryParseReplaceOptions( if (matchEvaluator is not null) { - replaceOptions = new ReplaceOptions( + replacer = new Replacer( matchEvaluator: matchEvaluator, functions: functions, cultureInvariant: (replaceFlags & ReplaceFlags.CultureInvariant) != 0); } else { - replaceOptions = new ReplaceOptions( + replacer = new Replacer( replacement: replacement, functions: functions, cultureInvariant: (replaceFlags & ReplaceFlags.CultureInvariant) != 0); @@ -872,7 +872,7 @@ public bool TryParseReplacement( } if ((options & ReplacementOptions.FromFile) != 0 - && !FileSystemHelpers.TryReadAllText(value, out value!, ex => Logger.WriteError(ex))) + && !FileSystemUtilities.TryReadAllText(value, out value!, ex => Logger.WriteError(ex))) { return false; } @@ -1314,7 +1314,7 @@ public bool TryEnsureFullPath(string path, [NotNullWhen(true)] out string? fullP { try { - fullPath = FileSystemHelpers.EnsureFullPath(path); + fullPath = FileSystemUtilities.EnsureFullPath(path); return true; } catch (ArgumentException ex) @@ -1362,7 +1362,7 @@ public bool TryParseFilter( IEnumerable values, string optionName, OptionValueProvider provider, - out Filter? filter, + out Matcher? matcher, bool allowNull = false, bool allowEmptyPattern = false, OptionValueProvider? namePartProvider = null, @@ -1373,7 +1373,7 @@ public bool TryParseFilter( values: values, optionName: optionName, provider: provider, - filter: out filter, + matcher: out matcher, namePart: out _, allowNull: allowNull, allowEmptyPattern: allowEmptyPattern, @@ -1386,7 +1386,7 @@ public bool TryParseFilter( IEnumerable values, string optionName, OptionValueProvider provider, - out Filter? filter, + out Matcher? matcher, out FileNamePart namePart, bool allowNull = false, bool allowEmptyPattern = false, @@ -1394,7 +1394,7 @@ public bool TryParseFilter( FileNamePart defaultNamePart = FileNamePart.Name, PatternOptions includedPatternOptions = PatternOptions.None) { - filter = null; + matcher = null; namePart = defaultNamePart; string? pattern = values.FirstOrDefault(); @@ -1505,7 +1505,7 @@ public bool TryParseFilter( if (pattern.Length == 0 && allowEmptyPattern) { - filter = new Filter(new Regex(".+", RegexOptions.Singleline), predicate: predicate); + matcher = new Matcher(new Regex(".+", RegexOptions.Singleline), predicate: predicate); return true; } @@ -1544,7 +1544,7 @@ public bool TryParseFilter( if ((patternOptions & PatternOptions.FromFile) != 0) { - if (!FileSystemHelpers.TryReadAllText(pattern, out pattern, ex => Logger.WriteError(ex))) + if (!FileSystemUtilities.TryReadAllText(pattern, out pattern, ex => Logger.WriteError(ex))) return false; if (pattern.Length == 0 @@ -1598,10 +1598,10 @@ public bool TryParseFilter( } } - filter = new Filter( + matcher = new Matcher( regex, - isNegative: (patternOptions & PatternOptions.Negative) != 0, - groupNumber: groupIndex, + invert: (patternOptions & PatternOptions.Negative) != 0, + group: groupIndex, predicate: predicate); return true; @@ -1818,7 +1818,7 @@ internal bool TryParseProperties( name, OptionNames.Name, OptionValueProviders.PatternOptionsProvider, - out Filter? nameFilter, + out Matcher? nameFilter, out FileNamePart namePart, allowNull: true, allowEmptyPattern: allowEmptyPattern)) diff --git a/src/CommandLine/PathWriter.cs b/src/CommandLine/PathWriter.cs index 6c71ee21..4c290b6e 100644 --- a/src/CommandLine/PathWriter.cs +++ b/src/CommandLine/PathWriter.cs @@ -49,7 +49,7 @@ public void WritePath( string path = fileMatch.Path; if (RelativePath - && string.Equals(path, basePath, FileSystemHelpers.Comparison)) + && string.Equals(path, basePath, FileSystemUtilities.Comparison)) { _logger.Write(".", PathColors, Verbosity); return; @@ -95,7 +95,7 @@ public void WritePath( _logger.Write(indent ?? Indent, Verbosity); if (RelativePath - && string.Equals(path, basePath, FileSystemHelpers.Comparison)) + && string.Equals(path, basePath, FileSystemUtilities.Comparison)) { _logger.Write(".", PathColors, Verbosity); } @@ -129,14 +129,14 @@ public void WritePath( { Match match = item.Match; - _logger.Write(path, startIndex, fileMatch.NameSpan.Start + match.Index - startIndex, Verbosity); + _logger.Write(path, startIndex, match.Index - startIndex, Verbosity); if (!MatchColors.IsDefault) _logger.Write(match.Value, MatchColors, Verbosity); _logger.Write(item.Value, replacementColors, Verbosity); - startIndex = fileMatch.NameSpan.Start + match.Index + match.Length; + startIndex = match.Index + match.Length; } _logger.Write(path, startIndex, path.Length - startIndex, Verbosity); @@ -146,11 +146,11 @@ protected int GetBasePathLength(string path, string? basePath) { if (basePath is not null && path.Length > basePath.Length - && path.StartsWith(basePath, FileSystemHelpers.Comparison)) + && path.StartsWith(basePath, FileSystemUtilities.Comparison)) { int length = basePath.Length; - if (FileSystemHelpers.IsDirectorySeparator(path[length])) + if (FileSystemUtilities.IsDirectorySeparator(path[length])) length++; return length; diff --git a/src/CommandLine/Progress/DiagnosticProgressReporter.cs b/src/CommandLine/Progress/DiagnosticProgressReporter.cs index 22a1ff13..005b7cb1 100644 --- a/src/CommandLine/Progress/DiagnosticProgressReporter.cs +++ b/src/CommandLine/Progress/DiagnosticProgressReporter.cs @@ -122,17 +122,17 @@ private static ReadOnlySpan GetPath( string? basePath, bool relativePath) { - if (string.Equals(path, basePath, FileSystemHelpers.Comparison)) + if (string.Equals(path, basePath, FileSystemUtilities.Comparison)) return (relativePath) ? "." : path; if (relativePath && basePath is not null && path.Length > basePath.Length - && path.StartsWith(basePath, FileSystemHelpers.Comparison)) + && path.StartsWith(basePath, FileSystemUtilities.Comparison)) { int startIndex = basePath.Length; - if (FileSystemHelpers.IsDirectorySeparator(path[startIndex])) + if (FileSystemUtilities.IsDirectorySeparator(path[startIndex])) startIndex++; return path.AsSpan(startIndex); diff --git a/src/CommandLine/Properties/AssemblyInfo.cs b/src/CommandLine/Properties/AssemblyInfo.cs deleted file mode 100644 index 344d1e2c..00000000 --- a/src/CommandLine/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -#pragma warning disable RCS0056 - -[assembly: InternalsVisibleTo("Orang.DocumentationGenerator, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] -[assembly: InternalsVisibleTo("Orang.CommandLine.Tests, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] diff --git a/src/CommandLine/SearchResult.cs b/src/CommandLine/SearchResult.cs index a37dbb1a..35c1d36b 100644 --- a/src/CommandLine/SearchResult.cs +++ b/src/CommandLine/SearchResult.cs @@ -32,7 +32,7 @@ public SearchResult( public bool IsDirectory => FileMatch.IsDirectory; - public Match? ContentMatch => FileMatch.ContentMatch; + public Match? ContentMatch => FileMatch.Content; public string ContentText => FileMatch.ContentText; @@ -45,7 +45,7 @@ public long GetSize() { return _size ?? (_size = (IsDirectory) - ? FileSystemHelpers.GetDirectorySize(Path) + ? FileSystemUtilities.GetDirectorySize(Path) : ((FileInfo)FileSystemInfo).Length).Value; } } diff --git a/src/CommandLine/SearchResultComparer.cs b/src/CommandLine/SearchResultComparer.cs index 8e623afb..b80b9016 100644 --- a/src/CommandLine/SearchResultComparer.cs +++ b/src/CommandLine/SearchResultComparer.cs @@ -52,8 +52,8 @@ public override int Compare(SearchResult? x, SearchResult? y) while (true) { - int l1 = FileSystemHelpers.IndexOfDirectorySeparator(path1, ++i1) - i1; - int l2 = FileSystemHelpers.IndexOfDirectorySeparator(path2, ++i2) - i2; + int l1 = FileSystemUtilities.IndexOfDirectorySeparator(path1, ++i1) - i1; + int l2 = FileSystemUtilities.IndexOfDirectorySeparator(path2, ++i2) - i2; bool isLast1 = i1 + l1 == path1.Length; bool isLast2 = i2 + l2 == path2.Length; @@ -185,8 +185,8 @@ public override int Compare(SearchResult? x, SearchResult? y) return 1; return string.Compare( - x.FileMatch.NameMatch!.Value, - y.FileMatch.NameMatch!.Value, + x.FileMatch.Name!.Value, + y.FileMatch.Name!.Value, StringComparison); } } diff --git a/src/CommandLine/SearchTelemetry.cs b/src/CommandLine/SearchTelemetry.cs deleted file mode 100644 index 02559722..00000000 --- a/src/CommandLine/SearchTelemetry.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -namespace Orang.CommandLine; - -public class SearchTelemetry -{ - internal SearchTelemetry() - { - } - - public int SearchedDirectoryCount { get; internal set; } - - public int FileCount { get; internal set; } - - public int DirectoryCount { get; internal set; } - - public int MatchingFileCount { get; internal set; } - - public int MatchingDirectoryCount { get; internal set; } - - internal int MatchingFileDirectoryCount => MatchingFileCount + MatchingDirectoryCount; - - public int ProcessedFileCount { get; internal set; } - - public int ProcessedDirectoryCount { get; internal set; } - - public int MatchCount { get; internal set; } - - public int ProcessedMatchCount { get; internal set; } - - public int MatchingLineCount { get; internal set; } - - public TimeSpan Elapsed { get; internal set; } - - public long FilesTotalSize { get; internal set; } - - public int UpdatedCount { get; internal set; } - - public int AddedCount { get; internal set; } - - public int RenamedCount { get; internal set; } - - public int DeletedCount { get; internal set; } -} diff --git a/src/CommandLine/Spelling/SpellcheckState.cs b/src/CommandLine/Spelling/SpellcheckState.cs index a2e90ea5..2b8b1fb7 100644 --- a/src/CommandLine/Spelling/SpellcheckState.cs +++ b/src/CommandLine/Spelling/SpellcheckState.cs @@ -12,11 +12,11 @@ internal class SpellcheckState : IReplacer public SpellcheckState( Spellchecker spellchecker, SpellingData data, - IEnumerable? filters = null) + IEnumerable? matchers = null) { Spellchecker = spellchecker; Data = data; - Filters = filters?.ToImmutableArray() ?? ImmutableArray.Empty; + Matchers = matchers?.ToImmutableArray() ?? ImmutableArray.Empty; OriginalFixes = data.Fixes; } @@ -25,7 +25,7 @@ public SpellcheckState( public SpellingData Data { get; internal set; } - public ImmutableArray Filters { get; } + public ImmutableArray Matchers { get; } internal FixList OriginalFixes { get; } diff --git a/src/Core/.editorconfig b/src/Common/.editorconfig similarity index 100% rename from src/Core/.editorconfig rename to src/Common/.editorconfig diff --git a/src/Common/Common.csproj b/src/Common/Common.csproj new file mode 100644 index 00000000..9e83911d --- /dev/null +++ b/src/Common/Common.csproj @@ -0,0 +1,36 @@ + + + + netstandard2.1 + Orang.Common + Orang + + + + true + Orang.Common + A shared package used by Orang. Do not install this package manually, it will be added as a prerequisite by other packages that require it. + + + + true + + + + + + + + + + <_Parameter1>Orang, PublicKey=$(OrangPublicKey) + + + <_Parameter1>Orang.FileSystem, PublicKey=$(OrangPublicKey) + + + <_Parameter1>Orang.CommandLine.Core, PublicKey=$(OrangPublicKey) + + + + diff --git a/src/Core/CoreExtensions.cs b/src/Common/CoreExtensions.cs similarity index 100% rename from src/Core/CoreExtensions.cs rename to src/Common/CoreExtensions.cs diff --git a/src/Core/ICapture.cs b/src/Common/ICapture.cs similarity index 100% rename from src/Core/ICapture.cs rename to src/Common/ICapture.cs diff --git a/src/Core/ListCache`1.cs b/src/Common/ListCache`1.cs similarity index 100% rename from src/Core/ListCache`1.cs rename to src/Common/ListCache`1.cs diff --git a/src/Core/Logging/ConsoleColors.cs b/src/Common/Logging/ConsoleColors.cs similarity index 100% rename from src/Core/Logging/ConsoleColors.cs rename to src/Common/Logging/ConsoleColors.cs diff --git a/src/Core/Logging/ConsoleWriter.cs b/src/Common/Logging/ConsoleWriter.cs similarity index 100% rename from src/Core/Logging/ConsoleWriter.cs rename to src/Common/Logging/ConsoleWriter.cs diff --git a/src/Core/Logging/ILogWriter.cs b/src/Common/Logging/ILogWriter.cs similarity index 100% rename from src/Core/Logging/ILogWriter.cs rename to src/Common/Logging/ILogWriter.cs diff --git a/src/Core/Logging/LogWriter.cs b/src/Common/Logging/LogWriter.cs similarity index 100% rename from src/Core/Logging/LogWriter.cs rename to src/Common/Logging/LogWriter.cs diff --git a/src/Core/Logging/Logger.cs b/src/Common/Logging/Logger.cs similarity index 100% rename from src/Core/Logging/Logger.cs rename to src/Common/Logging/Logger.cs diff --git a/src/Core/Logging/LoggingExtensions.cs b/src/Common/Logging/LoggingExtensions.cs similarity index 100% rename from src/Core/Logging/LoggingExtensions.cs rename to src/Common/Logging/LoggingExtensions.cs diff --git a/src/Core/Logging/Verbosity.cs b/src/Common/Logging/Verbosity.cs similarity index 100% rename from src/Core/Logging/Verbosity.cs rename to src/Common/Logging/Verbosity.cs diff --git a/src/Core/Filter.cs b/src/Common/Matcher.cs similarity index 56% rename from src/Core/Filter.cs rename to src/Common/Matcher.cs index 452acfb5..ee654fe5 100644 --- a/src/Core/Filter.cs +++ b/src/Common/Matcher.cs @@ -2,56 +2,72 @@ using System; using System.Diagnostics; -using System.Linq; using System.Text.RegularExpressions; namespace Orang; [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class Filter +public class Matcher { - internal static Filter EntireInput { get; } = new(@"\A.+\z", RegexOptions.Singleline); + internal static Matcher EntireInput { get; } = new(@"\A.+\z", RegexOptions.Singleline); - public Filter( + public Matcher( string pattern, - bool isNegative = false) : this(pattern, RegexOptions.None, isNegative: isNegative) + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) : this(new Regex(pattern, options), invert, group, predicate) { } - public Filter( - string pattern, - RegexOptions options, - bool isNegative = false) : this(new Regex(pattern, options), isNegative: isNegative) - { - } - - public Filter( + internal Matcher( Regex regex, - bool isNegative = false, - int groupNumber = -1, + bool invert = false, + object? group = null, Func? predicate = null) { Regex = regex ?? throw new ArgumentNullException(nameof(regex)); - Debug.Assert(groupNumber < 0 || regex.GetGroupNumbers().Contains(groupNumber), groupNumber.ToString()); + if (group is string groupName) + { + GroupNumber = regex.GroupNumberFromName(groupName); + + if (GroupNumber == -1) + throw new ArgumentException($"Group name '{groupName}' was not found.", nameof(groupName)); + } + else + { + GroupNumber = -1; + } + + if (group is int groupNumber) + { + GroupName = regex.GroupNameFromNumber(groupNumber); + + if (GroupName.Length == 0) + throw new ArgumentException($"Group number '{groupNumber}' was not found.", nameof(groupNumber)); + } + else + { + GroupName = ""; + } - GroupNumber = groupNumber; - IsNegative = isNegative; + Invert = invert; Predicate = predicate; } public Regex Regex { get; } - public bool IsNegative { get; } + public bool Invert { get; } public int GroupNumber { get; } - public Func? Predicate { get; } + public string GroupName { get; } - public string GroupName => Regex.GroupNameFromNumber(GroupNumber); + public Func? Predicate { get; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => $"{nameof(IsNegative)} = {IsNegative} {Regex}"; + private string DebuggerDisplay => Regex.ToString(); public Match? Match(string input) { @@ -69,7 +85,7 @@ public Filter( while (match.Success) { if (Predicate.Invoke(match.Value)) - return (IsNegative) ? null : match; + return (Invert) ? null : match; match = match.NextMatch(); } @@ -83,7 +99,7 @@ public Filter( if (group.Success && Predicate.Invoke(group.Value)) { - return (IsNegative) ? null : match; + return (Invert) ? null : match; } match = match.NextMatch(); @@ -93,17 +109,17 @@ public Filter( else if (GroupNumber < 1) { if (match.Success) - return (IsNegative) ? null : match; + return (Invert) ? null : match; } else { Group group = match.Groups[GroupNumber]; if (group.Success) - return (IsNegative) ? null : match; + return (Invert) ? null : match; } - return (IsNegative) + return (Invert) ? System.Text.RegularExpressions.Match.Empty : null; } @@ -112,9 +128,4 @@ internal bool IsMatch(string input) { return Match(input) is not null; } - - internal bool IsMatch(Match match) - { - return Match(match) is not null; - } } diff --git a/src/Common/Pattern.cs b/src/Common/Pattern.cs new file mode 100644 index 00000000..80bc215e --- /dev/null +++ b/src/Common/Pattern.cs @@ -0,0 +1,131 @@ +// Copyright (c) Josef Pihrt. 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 System.Text; +using Orang.Text.RegularExpressions; + +namespace Orang; + +public static class Pattern +{ + public static string Create( + string value, + PatternOptions options) + { + if (value is null) + throw new ArgumentNullException(nameof(value)); + + if ((options & PatternOptions.Literal) != 0) + value = RegexEscape.Escape(value); + + return CreateCore(value, options); + } + + public static string Any( + string value1, + string value2, + PatternOptions options = PatternOptions.None) + { + return Any(new string[] { value1, value2 }, options); + } + + public static string Any( + string value1, + string value2, + string value3, + PatternOptions options = PatternOptions.None) + { + return Any(new string[] { value1, value2, value3 }, options); + } + + public static string Any( + IEnumerable values, + PatternOptions options = PatternOptions.None) + { + if (values is null) + throw new ArgumentNullException(nameof(values)); + + string text = JoinValues(values, (options & PatternOptions.Literal) != 0); + + return CreateCore(text, options); + + static string JoinValues(IEnumerable values, bool literal) + { + using (IEnumerator en = values.GetEnumerator()) + { + if (en.MoveNext()) + { + string value = en.Current; + + if (en.MoveNext()) + { + StringBuilder sb = StringBuilderCache.GetInstance(); + + AppendValue(value, literal, sb); + + do + { + sb.Append("|"); + AppendValue(en.Current, literal, sb); + } + while (en.MoveNext()); + + return StringBuilderCache.GetStringAndFree(sb); + } + + return (literal) ? RegexEscape.Escape(value) : value; + } + + return ""; + } + } + + static void AppendValue(string value, bool literal, StringBuilder sb) + { + if (literal) + { + sb.Append(RegexEscape.Escape(value)); + } + else + { + sb.Append("(?:"); + sb.Append(value); + sb.Append(")"); + } + } + } + + private static string CreateCore(string value, PatternOptions options) + { + if ((options & (PatternOptions.WholeLine | PatternOptions.WholeWord)) == (PatternOptions.WholeLine | PatternOptions.WholeWord)) + { + throw new InvalidOperationException($"'{nameof(PatternOptions)}.{PatternOptions.WholeLine}' " + + $"and '{nameof(PatternOptions)}.{nameof(PatternOptions.WholeWord)}' cannot be set both at the same time."); + } + + if ((options & PatternOptions.WholeLine) != 0) + { + value = @"(?:\A|(?<=\n))(?:" + value + @")(?:\z|(?=\r?\n))"; + } + else if ((options & PatternOptions.WholeWord) != 0) + { + value = @"\b(?:" + value + @")\b"; + } + + if ((options & PatternOptions.Equals) == PatternOptions.Equals) + { + return @"\A(?:" + value + @")\z"; + } + else if ((options & PatternOptions.StartsWith) != 0) + { + return @"\A(?:" + value + ")"; + } + else if ((options & PatternOptions.EndsWith) != 0) + { + return "(?:" + value + @")\z"; + } + + return value; + } +} diff --git a/src/Common/PatternOptions.cs b/src/Common/PatternOptions.cs new file mode 100644 index 00000000..16255cad --- /dev/null +++ b/src/Common/PatternOptions.cs @@ -0,0 +1,17 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Orang; + +[Flags] +public enum PatternOptions +{ + None = 0, + Literal = 1, + StartsWith = 1 << 1, + EndsWith = 1 << 2, + WholeWord = 1 << 3, + WholeLine = 1 << 4, + Equals = StartsWith | EndsWith, +} diff --git a/src/Core/StringBuilderCache.cs b/src/Common/StringBuilderCache.cs similarity index 100% rename from src/Core/StringBuilderCache.cs rename to src/Common/StringBuilderCache.cs diff --git a/src/Core/Text/EncodingHelpers.cs b/src/Common/Text/EncodingHelpers.cs similarity index 100% rename from src/Core/Text/EncodingHelpers.cs rename to src/Common/Text/EncodingHelpers.cs diff --git a/src/Core/Text/RegularExpressions/CaptureItem.cs b/src/Common/Text/RegularExpressions/CaptureItem.cs similarity index 100% rename from src/Core/Text/RegularExpressions/CaptureItem.cs rename to src/Common/Text/RegularExpressions/CaptureItem.cs diff --git a/src/Core/Text/RegularExpressions/CaptureItemCollection.cs b/src/Common/Text/RegularExpressions/CaptureItemCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/CaptureItemCollection.cs rename to src/Common/Text/RegularExpressions/CaptureItemCollection.cs diff --git a/src/Core/Text/RegularExpressions/CharEscapeMode.cs b/src/Common/Text/RegularExpressions/CharEscapeMode.cs similarity index 100% rename from src/Core/Text/RegularExpressions/CharEscapeMode.cs rename to src/Common/Text/RegularExpressions/CharEscapeMode.cs diff --git a/src/Core/Text/RegularExpressions/GroupDefinition.cs b/src/Common/Text/RegularExpressions/GroupDefinition.cs similarity index 100% rename from src/Core/Text/RegularExpressions/GroupDefinition.cs rename to src/Common/Text/RegularExpressions/GroupDefinition.cs diff --git a/src/Core/Text/RegularExpressions/GroupDefinitionCollection.cs b/src/Common/Text/RegularExpressions/GroupDefinitionCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/GroupDefinitionCollection.cs rename to src/Common/Text/RegularExpressions/GroupDefinitionCollection.cs diff --git a/src/Core/Text/RegularExpressions/GroupDefinitionEqualityComparer.cs b/src/Common/Text/RegularExpressions/GroupDefinitionEqualityComparer.cs similarity index 100% rename from src/Core/Text/RegularExpressions/GroupDefinitionEqualityComparer.cs rename to src/Common/Text/RegularExpressions/GroupDefinitionEqualityComparer.cs diff --git a/src/Core/Text/RegularExpressions/GroupItem.cs b/src/Common/Text/RegularExpressions/GroupItem.cs similarity index 100% rename from src/Core/Text/RegularExpressions/GroupItem.cs rename to src/Common/Text/RegularExpressions/GroupItem.cs diff --git a/src/Core/Text/RegularExpressions/GroupItemCollection.cs b/src/Common/Text/RegularExpressions/GroupItemCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/GroupItemCollection.cs rename to src/Common/Text/RegularExpressions/GroupItemCollection.cs diff --git a/src/Core/Text/RegularExpressions/MatchData.cs b/src/Common/Text/RegularExpressions/MatchData.cs similarity index 100% rename from src/Core/Text/RegularExpressions/MatchData.cs rename to src/Common/Text/RegularExpressions/MatchData.cs diff --git a/src/Core/Text/RegularExpressions/MatchItem.cs b/src/Common/Text/RegularExpressions/MatchItem.cs similarity index 100% rename from src/Core/Text/RegularExpressions/MatchItem.cs rename to src/Common/Text/RegularExpressions/MatchItem.cs diff --git a/src/Core/Text/RegularExpressions/MatchItemCollection.cs b/src/Common/Text/RegularExpressions/MatchItemCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/MatchItemCollection.cs rename to src/Common/Text/RegularExpressions/MatchItemCollection.cs diff --git a/src/Core/Text/RegularExpressions/RegexCapture.cs b/src/Common/Text/RegularExpressions/RegexCapture.cs similarity index 95% rename from src/Core/Text/RegularExpressions/RegexCapture.cs rename to src/Common/Text/RegularExpressions/RegexCapture.cs index f37e5d31..2bd3439b 100644 --- a/src/Core/Text/RegularExpressions/RegexCapture.cs +++ b/src/Common/Text/RegularExpressions/RegexCapture.cs @@ -7,7 +7,7 @@ namespace Orang.Text.RegularExpressions; [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class RegexCapture : ICapture +internal class RegexCapture : ICapture { public RegexCapture(Capture capture) { diff --git a/src/Core/Text/RegularExpressions/RegexEscape.cs b/src/Common/Text/RegularExpressions/RegexEscape.cs similarity index 100% rename from src/Core/Text/RegularExpressions/RegexEscape.cs rename to src/Common/Text/RegularExpressions/RegexEscape.cs diff --git a/src/Core/Text/RegularExpressions/ReplaceData.cs b/src/Common/Text/RegularExpressions/ReplaceData.cs similarity index 100% rename from src/Core/Text/RegularExpressions/ReplaceData.cs rename to src/Common/Text/RegularExpressions/ReplaceData.cs diff --git a/src/Core/Text/RegularExpressions/ReplaceItem.cs b/src/Common/Text/RegularExpressions/ReplaceItem.cs similarity index 100% rename from src/Core/Text/RegularExpressions/ReplaceItem.cs rename to src/Common/Text/RegularExpressions/ReplaceItem.cs diff --git a/src/Core/Text/RegularExpressions/ReplaceItemCollection.cs b/src/Common/Text/RegularExpressions/ReplaceItemCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/ReplaceItemCollection.cs rename to src/Common/Text/RegularExpressions/ReplaceItemCollection.cs diff --git a/src/Core/Text/RegularExpressions/SplitCapture.cs b/src/Common/Text/RegularExpressions/SplitCapture.cs similarity index 95% rename from src/Core/Text/RegularExpressions/SplitCapture.cs rename to src/Common/Text/RegularExpressions/SplitCapture.cs index d67c2e23..d3cb3ef3 100644 --- a/src/Core/Text/RegularExpressions/SplitCapture.cs +++ b/src/Common/Text/RegularExpressions/SplitCapture.cs @@ -7,7 +7,7 @@ namespace Orang.Text.RegularExpressions; [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class SplitCapture : ICapture +internal class SplitCapture : ICapture { public SplitCapture(string value, int index) { diff --git a/src/Core/Text/RegularExpressions/SplitData.cs b/src/Common/Text/RegularExpressions/SplitData.cs similarity index 100% rename from src/Core/Text/RegularExpressions/SplitData.cs rename to src/Common/Text/RegularExpressions/SplitData.cs diff --git a/src/Core/Text/RegularExpressions/SplitItem.cs b/src/Common/Text/RegularExpressions/SplitItem.cs similarity index 100% rename from src/Core/Text/RegularExpressions/SplitItem.cs rename to src/Common/Text/RegularExpressions/SplitItem.cs diff --git a/src/Core/Text/RegularExpressions/SplitItemCollection.cs b/src/Common/Text/RegularExpressions/SplitItemCollection.cs similarity index 100% rename from src/Core/Text/RegularExpressions/SplitItemCollection.cs rename to src/Common/Text/RegularExpressions/SplitItemCollection.cs diff --git a/src/Core/TextHelpers.cs b/src/Common/TextHelpers.cs similarity index 100% rename from src/Core/TextHelpers.cs rename to src/Common/TextHelpers.cs diff --git a/src/Common/docs/NuGetReadme.md b/src/Common/docs/NuGetReadme.md new file mode 100644 index 00000000..e7cd03f4 --- /dev/null +++ b/src/Common/docs/NuGetReadme.md @@ -0,0 +1,3 @@ +# Orang.Common + +A shared package used by Orang. Do not install this package manually, it will be added as a prerequisite by other packages that require it. diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj deleted file mode 100644 index c6e02386..00000000 --- a/src/Core/Core.csproj +++ /dev/null @@ -1,10 +0,0 @@ - - - - netstandard2.1 - Orang.Core - Orang - false - - - diff --git a/src/Core/Properties/AssemblyInfo.cs b/src/Core/Properties/AssemblyInfo.cs deleted file mode 100644 index eb0153d3..00000000 --- a/src/Core/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -#pragma warning disable RCS0056 - -[assembly: InternalsVisibleTo("Orang, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] -[assembly: InternalsVisibleTo("Orang.CommandLine.Core, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] -[assembly: InternalsVisibleTo("Orang.FileSystem, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 95eb4404..5ac0e0c7 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,8 +5,35 @@ true $(MSBuildThisFileDirectory)Orang.snk Josef Pihrt - Copyright (c) 2019-2022 Josef Pihrt + Copyright (c) 2019-2023 Josef Pihrt + 00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4 enable + + 1.0.0 + false + Search, replace, rename and delete directories, files and its content using the power of .NET regular expressions. + https://github.com/JosefPihrt/Orang + Apache-2.0 + icon.png + FileSystem;RegularExpression;Regex;RegExp + docs/README.md + https://github.com/JosefPihrt/Orang.git + git + false + + + + true + true + true + true + snupkg + + + + + + diff --git a/src/DocumentationGenerator/DocumentationGenerator.csproj b/src/DocumentationGenerator/DocumentationGenerator.csproj index f3a24e3c..30480c46 100644 --- a/src/DocumentationGenerator/DocumentationGenerator.csproj +++ b/src/DocumentationGenerator/DocumentationGenerator.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/DocumentationGenerator/DocumentationWriter.cs b/src/DocumentationGenerator/DocumentationWriter.cs index 1b16b4ea..9692b054 100644 --- a/src/DocumentationGenerator/DocumentationWriter.cs +++ b/src/DocumentationGenerator/DocumentationWriter.cs @@ -36,7 +36,7 @@ public override void WriteOptionDescription(CommandOption option) string metaValueUrl = provider2.Name; - _writer.WriteLink(provider2.Name, "../option-values" + MarkdownHelpers.CreateGitHubHeadingLink(metaValueUrl)); + _writer.WriteLink(provider2.Name, "../options-values" + MarkdownHelpers.CreateGitHubHeadingLink(metaValueUrl)); _writer.WriteString(": "); @@ -60,7 +60,7 @@ public override void WriteOptionDescription(CommandOption option) _writer.WriteInlineCode(value.Remove(metaValueMatch.Index)); _writer.WriteLink( metaValueMatch.Value, - "../option-values" + MarkdownHelpers.CreateGitHubHeadingLink(metaValueMatch.Value)); + "../options-values" + MarkdownHelpers.CreateGitHubHeadingLink(metaValueMatch.Value)); } else { diff --git a/src/DocumentationGenerator/Program.cs b/src/DocumentationGenerator/Program.cs index bf6864c7..1f6b26f9 100644 --- a/src/DocumentationGenerator/Program.cs +++ b/src/DocumentationGenerator/Program.cs @@ -9,6 +9,7 @@ using System.Text; using System.Text.RegularExpressions; using DotMarkdown; +using DotMarkdown.Docusaurus; using DotMarkdown.Linq; using Orang.CommandLine; using Orang.CommandLine.Annotations; @@ -60,7 +61,8 @@ private static void Main(params string[] args) var settings = new MarkdownWriterSettings(markdownFormat); using (var sw = new StreamWriter(filePath, append: false, Encoding.UTF8)) - using (MarkdownWriter mw = MarkdownWriter.Create(sw, settings)) + using (MarkdownWriter mw2 = MarkdownWriter.Create(sw, settings)) + using (var mw = new DocusaurusMarkdownWriter(mw2)) { WriteFrontMatter(mw, 0, "Orang CLI"); @@ -95,6 +97,8 @@ private static void Main(params string[] args) GenerateOptionValues(commands, destinationDirectoryPath, settings); + GenerateExpressionSyntax(destinationDirectoryPath, settings); + foreach (Command command in application.Commands) { filePath = Path.GetFullPath(Path.Combine(destinationDirectoryPath, "cli/commands", $"{command.Name}.md")); @@ -102,16 +106,12 @@ private static void Main(params string[] args) Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); using (var sw = new StreamWriter(filePath, append: false, Encoding.UTF8)) - using (MarkdownWriter mw = MarkdownWriter.Create(sw, settings)) + using (MarkdownWriter mw2 = MarkdownWriter.Create(sw, settings)) + using (var mw = new DocusaurusMarkdownWriter(mw2)) { var writer = new DocumentationWriter(mw); - mw.WriteRaw("---"); - mw.WriteLine(); - mw.WriteRaw($"sidebar_label: {command.DisplayName}"); - mw.WriteLine(); - mw.WriteRaw("---"); - mw.WriteLine(); + WriteFrontMatter(mw, label: command.DisplayName); writer.WriteCommandHeading(command, application); writer.WriteCommandDescription(command); @@ -142,7 +142,8 @@ private static void GenerateCommands(IEnumerable commands, string desti Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); using (var sw = new StreamWriter(filePath, append: false, Encoding.UTF8)) - using (MarkdownWriter mw = MarkdownWriter.Create(sw, settings)) + using (MarkdownWriter mw2 = MarkdownWriter.Create(sw, settings)) + using (var mw = new DocusaurusMarkdownWriter(mw2)) { WriteFrontMatter(mw, 0, "Commands"); @@ -164,7 +165,7 @@ private static void GenerateOptionValues( string destinationDirectoryPath, MarkdownWriterSettings settings) { - string filePath = Path.GetFullPath(Path.Combine(destinationDirectoryPath, "cli/option-values.md")); + string filePath = Path.GetFullPath(Path.Combine(destinationDirectoryPath, "cli/options-values.md")); ImmutableArray providers = OptionValueProvider.GetProviders( commands.SelectMany(f => f.Options), @@ -184,8 +185,19 @@ private static void GenerateOptionValues( }; })); - document.Add( - Heading2("Expression Syntax"), + AddFootnote(document); + + File.WriteAllText(filePath, document.ToString(settings)); + } + + private static void GenerateExpressionSyntax( + string destinationDirectoryPath, + MarkdownWriterSettings settings) + { + string filePath = Path.GetFullPath(Path.Combine(destinationDirectoryPath, "cli/expression-syntax.md")); + + MDocument document = Document( + Heading1("Expression Syntax"), Table( TableRow("Expression", "Description"), HelpProvider.GetExpressionItems(includeDate: true) @@ -235,7 +247,7 @@ private static MTableRow CreateTableRow( string description = _removeNewlineRegex.Replace(optionValue.Description ?? "", " "); return TableRow( - value, + value!, (string.IsNullOrEmpty(shortValue)) ? " " : shortValue, (string.IsNullOrEmpty(description)) ? " " : description); } @@ -250,27 +262,20 @@ private static void AddFootnote(MDocument document) } #pragma warning restore IDE0060, RCS1163 - private static void WriteFrontMatter(MarkdownWriter mw, int? position = null, string? label = null) + private static void WriteFrontMatter(DocusaurusMarkdownWriter mw, int? position = null, string? label = null) { if (position is not null || label is not null) { - mw.WriteRaw("---"); - mw.WriteLine(); + var items = new List<(string, object)>(); + if (position is not null) - { - mw.WriteRaw($"sidebar_position: {position}"); - mw.WriteLine(); - } + items.Add(("sidebar_position", position)); if (label is not null) - { - mw.WriteRaw($"sidebar_label: {label}"); - mw.WriteLine(); - } + items.Add(("sidebar_label", label)); - mw.WriteRaw("---"); - mw.WriteLine(); + mw.WriteDocusaurusFrontMatter(items); } } } diff --git a/src/DocumentationGenerator/Properties/launchSettings.json b/src/DocumentationGenerator/Properties/launchSettings.json index b063935d..f6bf92b4 100644 --- a/src/DocumentationGenerator/Properties/launchSettings.json +++ b/src/DocumentationGenerator/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "DocumentationGenerator": { "commandName": "Project", - "commandLineArgs": "\"../../../../../docs/cli\" \"../../../data\"" + "commandLineArgs": "\"../../../../../tools/build\" \"../../../data\"" } } } \ No newline at end of file diff --git a/src/DocumentationGenerator/data/readme_bottom.md b/src/DocumentationGenerator/data/readme_bottom.md index a0d55e3f..6403983f 100644 --- a/src/DocumentationGenerator/data/readme_bottom.md +++ b/src/DocumentationGenerator/data/readme_bottom.md @@ -30,30 +30,17 @@ a user cannot specify argument (usually path(s)) as a last value of a command if the argument is preceded with multi-value parameter. Following command is invalid because path `C:/Documents` is treated as a value of multi-value parameter `-c | --content`. -``` +```sh orang find -c "^abc" i m "C:/Documents" ``` To fix this problem you can either add parameter `--paths` -``` +```sh orang find -c "abc" i m --paths "C:/Documents" ``` or you can specify path right after the command name: -``` +```sh orang find "C:/Documents" -c "abc" i m ``` - -## Links - -* [List of Option Values](cli/option-values) -* [How to's](cli/how-to) - -## External Links - -* [.NET Core Global Tools Overview](https://docs.microsoft.com/dotnet/core/tools/global-tools) -* [Create a .NET Core Global Tool Using the .NET Core CLI](https://docs.microsoft.com/dotnet/core/tools/global-tools-how-to-create) -* [.NET Core 2.1 Global Tools](https://natemcmaster.com/blog/2018/05/12/dotnet-global-tools/) -* [Windows CMD Shell](https://ss64.com/nt/syntax.html) -* [Parsing C++ Command-Line Arguments](https://docs.microsoft.com/cpp/cpp/parsing-cpp-command-line-arguments?view=vs-2019) diff --git a/src/FileSystem.ApiTest/Class.cs b/src/FileSystem.ApiTest/Class.cs deleted file mode 100644 index ae528790..00000000 --- a/src/FileSystem.ApiTest/Class.cs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Text.RegularExpressions; -using Orang; -using Orang.FileSystem; - -namespace N; - -public class C -{ - public void M() - { - var regex = new Regex("pattern", RegexOptions.IgnoreCase); - - var name = new Filter(regex); - - var filter = new FileSystemFilter(name); - - var searchOptions = new FileSystemSearchOptions(recurseSubdirectories: false); - - var search = new FileSystemSearch(filter, options: searchOptions); - - search.Replace("directoryPath", ReplaceOptions.Empty); - } - - public void M2() - { - var name = new Filter("pattern", RegexOptions.IgnoreCase); - - _ = new FileSystemFilter(name); - - //FileSystemSearch.Replace("directoryPath", filter, recurseSubdirectories: false); - } - - public static void SM() - { - } -} diff --git a/src/FileSystem.ApiTest/FileSystem.ApiTest.csproj b/src/FileSystem.ApiTest/FileSystem.ApiTest.csproj index ee89899b..b89c11a0 100644 --- a/src/FileSystem.ApiTest/FileSystem.ApiTest.csproj +++ b/src/FileSystem.ApiTest/FileSystem.ApiTest.csproj @@ -1,15 +1,15 @@ - netstandard2.1 + netcoreapp3.1 + Exe Orang.FileSystem.ApiTest Orang.FileSystem - false false - + diff --git a/src/FileSystem.ApiTest/Program.cs b/src/FileSystem.ApiTest/Program.cs new file mode 100644 index 00000000..20267d14 --- /dev/null +++ b/src/FileSystem.ApiTest/Program.cs @@ -0,0 +1,56 @@ +// Copyright (c) Josef Pihrt. 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.Threading; +using Orang; +using Orang.FileSystem; +using Orang.FileSystem.Fluent; + +namespace N; + +public static class Program +{ + public static void Main() + { + const string directoryPath = ""; + + IOperationResult result = new SearchBuilder() + .MatchDirectory(d => d + .Name(Pattern.Any("bin", "obj", PatternOptions.Equals | PatternOptions.WholeWord | PatternOptions.WholeLine)) + .NonEmpty()) + .SkipDirectory(Pattern.Any(".git", ".vs", PatternOptions.Equals | PatternOptions.Literal)) + .Delete(d => d + .ContentOnly() + .DryRun() + .LogOperation(o => Console.WriteLine(o.Path))) + .Run(directoryPath, CancellationToken.None); + + Console.WriteLine(result.Telemetry.MatchingDirectoryCount); + + var search = new Search( + new DirectoryMatcher() + { + Name = new Matcher(@"\A(bin|obj)\z"), + EmptyOption = FileEmptyOption.NonEmpty, + }, + new SearchOptions() + { + SearchDirectory = new DirectoryMatcher() + { + Name = new Matcher(@"\A(\.git|\.vs)\z", invert: true) + } + }); + + result = search.Delete( + directoryPath, + new DeleteOptions() + { + ContentOnly = true, + DryRun = true, + LogOperation = o => Console.WriteLine(o.Path), + }, + CancellationToken.None); + + Console.WriteLine(result.Telemetry.MatchingDirectoryCount); + } +} diff --git a/src/FileSystem/DirectoryPredicate.cs b/src/FileSystem/DirectoryPredicate.cs new file mode 100644 index 00000000..cf82756b --- /dev/null +++ b/src/FileSystem/DirectoryPredicate.cs @@ -0,0 +1,75 @@ +// Copyright (c) Josef Pihrt. 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.Runtime.InteropServices; +using System.Text.RegularExpressions; +using Orang.FileSystem; +using Orang.Text.RegularExpressions; + +namespace Orang; + +internal static class DirectoryPredicate +{ + public static Func Create(Matcher matcher, FileNamePart part = FileNamePart.FullName) + { + return path => (matcher.Invert) ? !IsMatch(matcher, part, path) : IsMatch(matcher, part, path); + } + + public static Func Create(string path, FileNamePart part = FileNamePart.FullName) + { + Matcher matcher = CreateFilter(path); + + return path => IsMatch(matcher, part, path); + } + + public static Func Create(Func? predicate, string path) + { + Matcher directoryFilter = CreateFilter(path); + + if (predicate is not null) + { + return path => + { + return predicate(path) + && IsMatch(directoryFilter, FileNamePart.FullName, path); + }; + } + else + { + return path => IsMatch(directoryFilter, FileNamePart.FullName, path); + } + } + + private static Matcher CreateFilter(string path) + { + string pattern = RegexEscape.Escape(path); + + var options = RegexOptions.ExplicitCapture; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + pattern = Regex.Replace(pattern, @"(/|\\\\)", @"(/|\\)", options); + + pattern = $@"\A{pattern}(?=(/"; + + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) + { + pattern += @"|\\)"; + } + else + { + pattern += ")"; + } + + pattern += @"|\z)"; + + if (!FileSystemUtilities.IsCaseSensitive) + options |= RegexOptions.IgnoreCase; + + return new Matcher(pattern, options); + } + + private static bool IsMatch(Matcher matcher, FileNamePart part, string path) + { + return matcher.IsDirectoryMatch(path, part); + } +} diff --git a/src/FileSystem/Extensions.cs b/src/FileSystem/Extensions.cs new file mode 100644 index 00000000..9ec11bdc --- /dev/null +++ b/src/FileSystem/Extensions.cs @@ -0,0 +1,24 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Text.RegularExpressions; +using Orang.FileSystem; + +namespace Orang; + +internal static class Extensions +{ + public static Match? Match(this Matcher matcher, FileNameSpan name) + { + return matcher.Match(matcher.Regex.Match(name.Path, name.Start, name.Length)); + } + + public static bool IsMatch(this Matcher matcher, FileNameSpan name) + { + return Match(matcher, name) is not null; + } + + internal static bool IsDirectoryMatch(this Matcher matcher, string path, FileNamePart part) + { + return IsMatch(matcher, FileNameSpan.FromDirectory(path, part)); + } +} diff --git a/src/FileSystem/FileSystem.csproj b/src/FileSystem/FileSystem.csproj index 9967aa0b..c4dab376 100644 --- a/src/FileSystem/FileSystem.csproj +++ b/src/FileSystem/FileSystem.csproj @@ -1,26 +1,23 @@ - + netstandard2.1 Orang.FileSystem - Orang.FileSystem + Orang + true Orang.FileSystem - 0.3.1 - Search, replace, rename and delete directories, files and its content using the power of .NET regular expressions. - https://github.com/JosefPihrt/Orang - Apache-2.0 - icon.png - FileSystem;RegularExpression;Regex;RegExp - https://github.com/JosefPihrt/Orang.git - git - false + + + + true + @@ -28,7 +25,13 @@ - + + + + + + <_Parameter1>Orang, PublicKey=$(OrangPublicKey) + diff --git a/src/FileSystem/FileSystem/Commands/Command.cs b/src/FileSystem/FileSystem/Commands/Command.cs new file mode 100644 index 00000000..32100b37 --- /dev/null +++ b/src/FileSystem/FileSystem/Commands/Command.cs @@ -0,0 +1,79 @@ +// Copyright (c) Josef Pihrt. 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.Threading; + +namespace Orang.FileSystem.Commands; + +internal abstract class Command +{ + protected Command() + { + Telemetry = new SearchTelemetry(); + } + + public abstract OperationKind OperationKind { get; } + + public SearchTelemetry Telemetry { get; } + + public TerminationReason TerminationReason { get; set; } + + public int MaxMatchingFiles { get; set; } + + public Action? LogOperation { get; set; } + + public bool DryRun { get; set; } + + public CancellationToken CancellationToken { get; set; } + + protected abstract void ExecuteMatch(FileMatch fileMatch, string directoryPath); + + protected virtual void OnSearchStateCreating(SearchState search) + { + } + + public void Execute(string directoryPath, Search search, CancellationToken cancellationToken) + { + directoryPath = FileSystemUtilities.EnsureFullPath(directoryPath); + + if (!System.IO.Directory.Exists(directoryPath)) + throw new DirectoryNotFoundException($"Directory not found: {directoryPath}"); + + var state = new SearchState(search.FileMatcher, search.DirectoryMatcher) + { + IncludeDirectory = search.Options.IncludeDirectoryPredicate, + ExcludeDirectory = search.Options.ExcludeDirectoryPredicate, + LogProgress = search.Options.LogProgress, + RecurseSubdirectories = !search.Options.TopDirectoryOnly, + DefaultEncoding = search.Options.DefaultEncoding, + IgnoreInaccessible = search.Options.IgnoreInaccessible, + Telemetry = Telemetry, + }; + + OnSearchStateCreating(state); + + foreach (FileMatch fileMatch in state.Find(directoryPath, this as INotifyDirectoryChanged, cancellationToken)) + { + Telemetry.IncrementMatchingCount(fileMatch.IsDirectory); + + ExecuteMatch(fileMatch, directoryPath); + + if (MaxMatchingFiles == Telemetry.MatchingFileDirectoryCount) + { + TerminationReason = TerminationReason.MaxReached; + break; + } + } + } + + protected void Log(FileMatch fileMatch, Exception? exception = null) + { + Log(fileMatch, newPath: null, exception); + } + + protected void Log(FileMatch fileMatch, string? newPath, Exception? exception = null) + { + LogOperation?.Invoke(new OperationProgress(fileMatch, newPath, OperationKind, exception)); + } +} diff --git a/src/FileSystem/FileSystem/Commands/CommonCopyCommand.cs b/src/FileSystem/FileSystem/Commands/CommonCopyCommand.cs new file mode 100644 index 00000000..50f805b4 --- /dev/null +++ b/src/FileSystem/FileSystem/Commands/CommonCopyCommand.cs @@ -0,0 +1,299 @@ +// Copyright (c) Josef Pihrt. 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 System.Diagnostics; +using System.IO; + +namespace Orang.FileSystem.Commands; + +internal abstract class CommonCopyCommand : CommonFindCommand +{ + protected CommonCopyCommand() + { + } + + public string DestinationPath { get; set; } = null!; + + public IDialogProvider? DialogProvider { get; set; } + + public CopyOptions CopyOptions { get; set; } = null!; + + public ConflictResolution ConflictResolution { get; set; } + + protected override void OnSearchStateCreating(SearchState search) + { + search.ExcludeDirectory = DirectoryPredicate.Create(search.ExcludeDirectory, DestinationPath); + } + + protected abstract void ExecuteOperation(string sourcePath, string destinationPath); + + protected override void ExecuteMatch( + FileMatch fileMatch, + string directoryPath) + { + string sourcePath = fileMatch.Path; + string destinationPath; + + if (fileMatch.IsDirectory + || (directoryPath is not null && !CopyOptions.Flat)) + { + Debug.Assert(sourcePath.StartsWith(directoryPath, FileSystemUtilities.Comparison)); + + string relativePath = sourcePath.Substring(directoryPath.Length + 1); + + destinationPath = Path.Combine(DestinationPath, relativePath); + } + else + { + string fileName = Path.GetFileName(sourcePath); + + destinationPath = Path.Combine(DestinationPath, fileName); + } + + try + { + ExecuteOperation(fileMatch, fileMatch.Path, destinationPath, fileMatch.IsDirectory); + } + catch (Exception ex) when (ex is IOException + || ex is UnauthorizedAccessException) + { + Log(fileMatch, destinationPath, ex); + } + } + + private DialogResult? ExecuteOperation(FileMatch? fileMatch, string sourcePath, string destinationPath, bool isDirectory) + { + bool fileExists = File.Exists(destinationPath); + bool directoryExists = !fileExists && Directory.Exists(destinationPath); + var ask = false; + + if (isDirectory) + { + if (fileExists) + { + ask = true; + } + else if (directoryExists) + { + if (CopyOptions.StructureOnly + && File.GetAttributes(sourcePath) == File.GetAttributes(destinationPath)) + { + return null; + } + + if (CopyOptions.CompareDirectory?.Invoke(sourcePath, destinationPath) != false) + return null; + } + } + else if (fileExists) + { + if (CopyOptions.ComparedProperties != FileCompareProperties.None + && FileSystemUtilities.FileEquals( + sourcePath, + destinationPath, + CopyOptions.ComparedProperties, + CopyOptions.ComparedAttributes, + CopyOptions.AllowedTimeDiff)) + { + return null; + } + + if (CopyOptions.CompareFile?.Invoke(sourcePath, destinationPath) != false) + return null; + + ask = true; + } + else if (directoryExists) + { + ask = true; + } + + if (ask + && ConflictResolution == ConflictResolution.Skip) + { + return null; + } + + if (ConflictResolution == ConflictResolution.Suffix + && ((isDirectory && directoryExists) + || (!isDirectory && fileExists))) + { + destinationPath = FileSystemUtilities.CreateNewFilePath(destinationPath); + } + + if (ask + && ConflictResolution == ConflictResolution.Ask) + { + DialogResult dialogResult = DialogProvider!.GetResult( + new ConflictInfo(sourcePath, destinationPath)); + + switch (dialogResult) + { + case DialogResult.Yes: + { + break; + } + case DialogResult.YesToAll: + { + ConflictResolution = ConflictResolution.Overwrite; + break; + } + case DialogResult.No: + case DialogResult.None: + { + return null; + } + case DialogResult.NoToAll: + { + ConflictResolution = ConflictResolution.Skip; + return DialogResult.NoToAll; + } + case DialogResult.Cancel: + { + throw new OperationCanceledException(); + } + default: + { + throw new InvalidOperationException($"Unknown enum value '{dialogResult}'."); + } + } + } + + if (fileMatch is not null) + Log(fileMatch, destinationPath); + + if (isDirectory) + { + if (directoryExists) + { + if (!DryRun) + { + if (CopyOptions.StructureOnly) + { + FileSystemUtilities.UpdateAttributes(sourcePath, destinationPath); + } + else + { + CopyDirectory(sourcePath, destinationPath); + } + } + } + else + { + if (fileExists + && !DryRun) + { + File.Delete(destinationPath); + } + + if (!DryRun) + { + if (CopyOptions.StructureOnly) + { + Directory.CreateDirectory(destinationPath); + } + else + { + CopyDirectory(sourcePath, destinationPath); + } + } + } + + Telemetry.ProcessedDirectoryCount++; + } + else + { + if (!DryRun) + { + if (fileExists) + { + File.Delete(destinationPath); + } + else if (directoryExists) + { + Directory.Delete(destinationPath, recursive: true); + } + else + { + Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); + } + + ExecuteOperation(sourcePath, destinationPath); + } + + Telemetry.ProcessedFileCount++; + } + + return null; + } + + private void CopyDirectory( + string sourcePath, + string destinationPath) + { + var directories = new Stack<(string, string)>(); + + directories.Push((sourcePath, destinationPath)); + + CopyDirectory(directories); + } + + private void CopyDirectory(Stack<(string, string)> directories) + { + while (directories.Count > 0) + { + (string sourcePath, string destinationPath) = directories.Pop(); + + var isEmpty = true; + + using (IEnumerator en = Directory.EnumerateFiles(sourcePath).GetEnumerator()) + { + if (en.MoveNext()) + { + isEmpty = false; + + do + { + string newFilePath = Path.Combine(destinationPath, Path.GetFileName(en.Current)); + + DialogResult? result = ExecuteOperation( + null, + en.Current, + newFilePath, + isDirectory: false); + + if (result == DialogResult.NoToAll + || result == DialogResult.Cancel) + { + return; + } + } + while (en.MoveNext()); + } + } + + using (IEnumerator en = Directory.EnumerateDirectories(sourcePath).GetEnumerator()) + { + if (en.MoveNext()) + { + isEmpty = false; + + do + { + string newDirectoryPath = Path.Combine(destinationPath, Path.GetFileName(en.Current)); + + directories.Push((en.Current, newDirectoryPath)); + } + while (en.MoveNext()); + } + } + + if (isEmpty + && !DryRun) + { + Directory.CreateDirectory(destinationPath); + } + } + } +} diff --git a/src/FileSystem/Operations/CommonFindOperation.cs b/src/FileSystem/FileSystem/Commands/CommonFindCommand.cs similarity index 89% rename from src/FileSystem/Operations/CommonFindOperation.cs rename to src/FileSystem/FileSystem/Commands/CommonFindCommand.cs index 906d79d8..cb7ecef2 100644 --- a/src/FileSystem/Operations/CommonFindOperation.cs +++ b/src/FileSystem/FileSystem/Commands/CommonFindCommand.cs @@ -5,11 +5,11 @@ using System.Diagnostics; using System.Text.RegularExpressions; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal abstract class CommonFindOperation : FileSystemOperation +internal abstract class CommonFindCommand : Command { - protected CommonFindOperation() + protected CommonFindCommand() { } @@ -60,7 +60,7 @@ protected MaxReason GetCaptures( && maxTotalMatches > 0 && (maxMatchesInFile == 0 || maxTotalMatches <= maxMatchesInFile)) { - TerminationReason = FileSystem.TerminationReason.MaxReached; + TerminationReason = TerminationReason.MaxReached; } return maxReason; diff --git a/src/FileSystem/Operations/CopyOperation.cs b/src/FileSystem/FileSystem/Commands/CopyCommand.cs similarity index 79% rename from src/FileSystem/Operations/CopyOperation.cs rename to src/FileSystem/FileSystem/Commands/CopyCommand.cs index f0a6e3d6..48c6b1fc 100644 --- a/src/FileSystem/Operations/CopyOperation.cs +++ b/src/FileSystem/FileSystem/Commands/CopyCommand.cs @@ -1,11 +1,10 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; -using Orang.FileSystem; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal class CopyOperation : CommonCopyOperation +internal class CopyCommand : CommonCopyCommand { public override OperationKind OperationKind => OperationKind.Copy; diff --git a/src/FileSystem/Operations/DeleteOperation.cs b/src/FileSystem/FileSystem/Commands/DeleteCommand.cs similarity index 76% rename from src/FileSystem/Operations/DeleteOperation.cs rename to src/FileSystem/FileSystem/Commands/DeleteCommand.cs index 160a7d90..305fede6 100644 --- a/src/FileSystem/Operations/DeleteOperation.cs +++ b/src/FileSystem/FileSystem/Commands/DeleteCommand.cs @@ -2,15 +2,19 @@ using System; using System.IO; -using Orang.FileSystem; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal class DeleteOperation : DeleteOrRenameOperation +internal class DeleteCommand : DeleteOrRenameCommand { + public DeleteOptions DeleteOptions { get; set; } = null!; + public override OperationKind OperationKind => OperationKind.Delete; - public DeleteOptions DeleteOptions { get; set; } = null!; + protected override void OnSearchStateCreating(SearchState search) + { + search.CanRecurseMatch = false; + } protected override void ExecuteMatch( FileMatch fileMatch, @@ -20,7 +24,7 @@ protected override void ExecuteMatch( { if (!DryRun) { - FileSystemHelpers.Delete( + FileSystemUtilities.Delete( fileMatch, contentOnly: DeleteOptions.ContentOnly, includingBom: DeleteOptions.IncludingBom, @@ -28,14 +32,14 @@ protected override void ExecuteMatch( directoriesOnly: DeleteOptions.DirectoriesOnly); } - Report(fileMatch); + Log(fileMatch); Telemetry.IncrementProcessedCount(fileMatch.IsDirectory); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { - Report(fileMatch, ex); + Log(fileMatch, ex); } } } diff --git a/src/FileSystem/Operations/DeleteOrRenameOperation.cs b/src/FileSystem/FileSystem/Commands/DeleteOrRenameCommand.cs similarity index 68% rename from src/FileSystem/Operations/DeleteOrRenameOperation.cs rename to src/FileSystem/FileSystem/Commands/DeleteOrRenameCommand.cs index 2d193780..70aa76b0 100644 --- a/src/FileSystem/Operations/DeleteOrRenameOperation.cs +++ b/src/FileSystem/FileSystem/Commands/DeleteOrRenameCommand.cs @@ -1,13 +1,12 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using Orang.FileSystem; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal abstract class DeleteOrRenameOperation : FileSystemOperation, INotifyDirectoryChanged +internal abstract class DeleteOrRenameCommand : Command, INotifyDirectoryChanged { - protected DeleteOrRenameOperation() + protected DeleteOrRenameCommand() { } diff --git a/src/FileSystem/Operations/MoveOperation.cs b/src/FileSystem/FileSystem/Commands/MoveCommand.cs similarity index 79% rename from src/FileSystem/Operations/MoveOperation.cs rename to src/FileSystem/FileSystem/Commands/MoveCommand.cs index c1588552..f180400d 100644 --- a/src/FileSystem/Operations/MoveOperation.cs +++ b/src/FileSystem/FileSystem/Commands/MoveCommand.cs @@ -1,11 +1,10 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.IO; -using Orang.FileSystem; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal class MoveOperation : CommonCopyOperation +internal class MoveCommand : CommonCopyCommand { public override OperationKind OperationKind => OperationKind.Move; diff --git a/src/FileSystem/Operations/RenameOperation.cs b/src/FileSystem/FileSystem/Commands/RenameCommand.cs similarity index 55% rename from src/FileSystem/Operations/RenameOperation.cs rename to src/FileSystem/FileSystem/Commands/RenameCommand.cs index 65446752..2f4534f2 100644 --- a/src/FileSystem/Operations/RenameOperation.cs +++ b/src/FileSystem/FileSystem/Commands/RenameCommand.cs @@ -1,86 +1,84 @@ // Copyright (c) Josef Pihrt. 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 System.Diagnostics; using System.IO; -using Orang.FileSystem; -using Orang.Text.RegularExpressions; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal class RenameOperation : DeleteOrRenameOperation +internal class RenameCommand : DeleteOrRenameCommand { - public override OperationKind OperationKind => OperationKind.Rename; - public RenameOptions RenameOptions { get; set; } = null!; + public Replacer Replacer { get; set; } = null!; + public ConflictResolution ConflictResolution { get; set; } - public IDialogProvider? DialogProvider { get; set; } + public IDialogProvider? DialogProvider { get; set; } - protected override void ExecuteDirectory(string directoryPath) - { - Debug.Assert(!NameFilter!.IsNegative); + public Matcher? FileMatcher { get; set; } + + public Matcher? DirectoryMatcher { get; set; } - base.ExecuteDirectory(directoryPath); + public override OperationKind OperationKind => OperationKind.Rename; + + protected override void OnSearchStateCreating(SearchState search) + { + search.SupportsEnumeration = DryRun; } protected override void ExecuteMatch( FileMatch fileMatch, string directoryPath) { - (List replaceItems, MaxReason maxReason) = ReplaceHelpers.GetReplaceItems( - fileMatch.NameMatch!, - RenameOptions, - predicate: NameFilter?.Predicate, + string newValue = fileMatch.GetReplacement( + Replacer, + count: 0, + predicate: (fileMatch.IsDirectory) ? DirectoryMatcher!.Predicate : FileMatcher!.Predicate, cancellationToken: CancellationToken); - string path = fileMatch.Path; - - string newName = ReplaceHelpers.GetNewName(fileMatch, replaceItems); - - ListCache.Free(replaceItems); - - if (string.IsNullOrWhiteSpace(newName)) + if (string.IsNullOrWhiteSpace(newValue)) { - Report(fileMatch, null, new IOException("New file name cannot be empty or contains only white-space.")); + Log(fileMatch, null, new IOException("New file name cannot be empty or contains only white-space.")); return; } - if (string.Compare( + string path = fileMatch.Path; + + bool changed = string.Compare( path, fileMatch.NameSpan.Start, - newName, + newValue, 0, - fileMatch.NameSpan.Length, - StringComparison.Ordinal) == 0) - { + Math.Max(fileMatch.NameSpan.Length, newValue.Length), + StringComparison.Ordinal) != 0; + + bool isInvalidName = changed + && FileSystemUtilities.ContainsInvalidFileNameChars(newValue); + + string newPath = path.Substring(0, fileMatch.NameSpan.Start) + + newValue + + path.Substring(fileMatch.NameSpan.Start + fileMatch.NameSpan.Length); + + if (!changed) return; - } - if (FileSystemHelpers.ContainsInvalidFileNameChars(newName)) + if (isInvalidName) { - Report(fileMatch, newName, new IOException("New file name contains invalid characters.")); + Log(fileMatch, newValue, new IOException("New file name contains invalid characters.")); return; } - string newPath = path.Substring(0, fileMatch.NameSpan.Start) + newName; - - if (File.Exists(newPath)) + if (File.Exists(newPath) + || Directory.Exists(newPath)) { if (ConflictResolution == ConflictResolution.Skip) - { return; - } - else if (ConflictResolution == ConflictResolution.Suffix) - { - newPath = FileSystemHelpers.CreateNewFilePath(newPath); - } - else if (ConflictResolution == ConflictResolution.Ask) + + if (ConflictResolution == ConflictResolution.Ask + && !DryRun + && !AskToOverwrite(fileMatch, newPath)) { - if (!AskToOverwrite(fileMatch, newPath)) - return; + return; } } @@ -90,6 +88,15 @@ protected override void ExecuteMatch( { if (!DryRun) { + if (File.Exists(newPath)) + { + File.Delete(newPath); + } + else if (Directory.Exists(newPath)) + { + Directory.Delete(newPath, recursive: true); + } + if (fileMatch.IsDirectory) { Directory.Move(path, newPath); @@ -102,14 +109,21 @@ protected override void ExecuteMatch( renamed = true; } - Report(fileMatch, newPath); + if (fileMatch.IsDirectory) + { + Telemetry.ProcessedDirectoryCount++; + } + else + { + Telemetry.ProcessedFileCount++; + } - Telemetry.IncrementProcessedCount(fileMatch.IsDirectory); + Log(fileMatch, newPath); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { - Report(fileMatch, newPath, ex); + Log(fileMatch, newPath, ex); } if (fileMatch.IsDirectory @@ -121,7 +135,7 @@ protected override void ExecuteMatch( private bool AskToOverwrite(FileMatch fileMatch, string newPath) { - DialogResult result = DialogProvider!.GetResult(new OperationProgress(fileMatch, newPath, OperationKind)); + DialogResult result = DialogProvider!.GetResult(new ConflictInfo(fileMatch.Path, newPath)); switch (result) { diff --git a/src/FileSystem/Operations/ReplaceOperation.cs b/src/FileSystem/FileSystem/Commands/ReplaceCommand.cs similarity index 79% rename from src/FileSystem/Operations/ReplaceOperation.cs rename to src/FileSystem/FileSystem/Commands/ReplaceCommand.cs index 8f6f02ce..507a9607 100644 --- a/src/FileSystem/Operations/ReplaceOperation.cs +++ b/src/FileSystem/FileSystem/Commands/ReplaceCommand.cs @@ -2,22 +2,24 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Text.RegularExpressions; -using Orang.FileSystem; -namespace Orang.Operations; +namespace Orang.FileSystem.Commands; -internal class ReplaceOperation : CommonFindOperation +internal class ReplaceCommand : CommonFindCommand { - public override OperationKind OperationKind => OperationKind.Replace; - public ReplaceOptions ReplaceOptions { get; set; } = null!; - protected override void ExecuteDirectory(string path) + public Replacer Replacer { get; set; } = null!; + + public Matcher ContentFilter { get; set; } = null!; + + public override OperationKind OperationKind => OperationKind.Replace; + + protected override void OnSearchStateCreating(SearchState search) { - Debug.Assert(ContentFilter?.IsNegative == false); + search.CanRecurseMatch = false; } protected override void ExecuteMatch( @@ -32,7 +34,7 @@ protected override void ExecuteMatch( captures = ListCache.GetInstance(); GetCaptures( - fileMatch.ContentMatch!, + fileMatch.Content!, ContentFilter!.GroupNumber, predicate: ContentFilter.Predicate, captures: captures); @@ -41,9 +43,11 @@ protected override void ExecuteMatch( { textWriter = new StreamWriter(fileMatch.Path, false, fileMatch.Encoding); - WriteMatches(fileMatch.ContentText, captures, ReplaceOptions, textWriter); + WriteMatches(fileMatch.ContentText, captures, Replacer, textWriter); } + fileMatch.FileContent = default; + int fileMatchCount = captures.Count; int fileReplacementCount = fileMatchCount; Telemetry.MatchCount += fileMatchCount; @@ -52,12 +56,12 @@ protected override void ExecuteMatch( if (fileReplacementCount > 0) Telemetry.ProcessedFileCount++; - Report(fileMatch); + Log(fileMatch); } catch (Exception ex) when (ex is IOException || ex is UnauthorizedAccessException) { - Report(fileMatch, ex); + Log(fileMatch, ex); } finally { @@ -71,7 +75,7 @@ protected override void ExecuteMatch( private void WriteMatches( string input, IEnumerable captures, - ReplaceOptions replaceOptions, + Replacer replacer, TextWriter textWriter) { int index = 0; @@ -80,7 +84,7 @@ private void WriteMatches( { textWriter.Write(input.AsSpan(index, capture.Index - index)); - string result = replaceOptions.Replace((Match)capture); + string result = replacer.Replace((Match)capture); textWriter.Write(result); diff --git a/src/FileSystem/FileSystem/ConflictInfo.cs b/src/FileSystem/FileSystem/ConflictInfo.cs new file mode 100644 index 00000000..d6931602 --- /dev/null +++ b/src/FileSystem/FileSystem/ConflictInfo.cs @@ -0,0 +1,22 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Diagnostics; + +namespace Orang.FileSystem; + +[DebuggerDisplay("{DebuggerDisplay,nq}")] +public readonly struct ConflictInfo +{ + internal ConflictInfo(string sourcePath, string destinationPath) + { + SourcePath = sourcePath; + DestinationPath = destinationPath; + } + + public string SourcePath { get; } + + public string DestinationPath { get; } + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay => $"{SourcePath} {DestinationPath}"; +} diff --git a/src/FileSystem/FileSystem/ConflictResolution.cs b/src/FileSystem/FileSystem/ConflictResolution.cs index 529264d3..7fd8e5f6 100644 --- a/src/FileSystem/FileSystem/ConflictResolution.cs +++ b/src/FileSystem/FileSystem/ConflictResolution.cs @@ -2,11 +2,10 @@ namespace Orang.FileSystem; -// Stop, Throw public enum ConflictResolution { - Overwrite = 0, - Skip = 1, - Ask = 2, - Suffix = 3, + Overwrite, + Skip, + Ask, + Suffix, } diff --git a/src/FileSystem/FileSystem/CopyOptions.cs b/src/FileSystem/FileSystem/CopyOptions.cs index af100fce..af24433f 100644 --- a/src/FileSystem/FileSystem/CopyOptions.cs +++ b/src/FileSystem/FileSystem/CopyOptions.cs @@ -1,31 +1,33 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Diagnostics; +using System; +using System.IO; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. namespace Orang.FileSystem; -[DebuggerDisplay("{DebuggerDisplay,nq}")] public class CopyOptions { - internal static CopyOptions Default { get; } = new(); + public ConflictResolution ConflictResolution { get; set; } = ConflictResolution.Skip; + + public FileCompareProperties ComparedProperties { get; set; } + + internal Func? CompareFile { get; set; } + + internal Func? CompareDirectory { get; set; } + + public FileAttributes? ComparedAttributes { get; set; } + + public TimeSpan? AllowedTimeDiff { get; set; } - public CopyOptions( - ConflictResolution conflictResolution = ConflictResolution.Skip, - FileCompareOptions compareOptions = FileCompareOptions.None, - bool flat = false) - { - ConflictResolution = conflictResolution; - CompareOptions = compareOptions; - Flat = flat; - } + public bool Flat { get; set; } - public ConflictResolution ConflictResolution { get; } + internal bool StructureOnly { get; set; } - public FileCompareOptions CompareOptions { get; } + public bool DryRun { get; set; } - public bool Flat { get; } + public Action? LogOperation { get; set; } - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay - => $"{nameof(ConflictResolution)} = {ConflictResolution} {nameof(CompareOptions)} = {CompareOptions}"; + public IDialogProvider? DialogProvider { get; set; } } diff --git a/src/FileSystem/FileSystem/DeleteOptions.cs b/src/FileSystem/FileSystem/DeleteOptions.cs index 81990923..cd657c77 100644 --- a/src/FileSystem/FileSystem/DeleteOptions.cs +++ b/src/FileSystem/FileSystem/DeleteOptions.cs @@ -1,5 +1,6 @@ // Copyright (c) Josef Pihrt. 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.Diagnostics; namespace Orang.FileSystem; @@ -7,27 +8,17 @@ namespace Orang.FileSystem; [DebuggerDisplay("{DebuggerDisplay,nq}")] public class DeleteOptions { - internal static DeleteOptions Default { get; } = new(); + public bool ContentOnly { get; set; } - public DeleteOptions( - bool contentOnly = false, - bool includingBom = false, - bool filesOnly = false, - bool directoriesOnly = false) - { - ContentOnly = contentOnly; - IncludingBom = includingBom; - FilesOnly = filesOnly; - DirectoriesOnly = directoriesOnly; - } + public bool IncludingBom { get; set; } - public bool ContentOnly { get; } + internal bool FilesOnly { get; set; } - public bool IncludingBom { get; } + internal bool DirectoriesOnly { get; set; } - public bool FilesOnly { get; } + public bool DryRun { get; set; } - public bool DirectoriesOnly { get; } + public Action? LogOperation { get; set; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"{nameof(ContentOnly)} = {ContentOnly} {nameof(IncludingBom)} = {IncludingBom}"; diff --git a/src/FileSystem/FileSystem/DirectoryMatcher.cs b/src/FileSystem/FileSystem/DirectoryMatcher.cs new file mode 100644 index 00000000..14a4f325 --- /dev/null +++ b/src/FileSystem/FileSystem/DirectoryMatcher.cs @@ -0,0 +1,83 @@ +// Copyright (c) Josef Pihrt. 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.Linq; +using System.Text.RegularExpressions; + +#pragma warning disable RCS1223 + +namespace Orang.FileSystem; + +public class DirectoryMatcher +{ + public Matcher? Name { get; set; } + + public FileNamePart NamePart { get; set; } + + public FileAttributes WithAttributes { get; set; } + + public FileAttributes WithoutAttributes { get; set; } + + public FileEmptyOption EmptyOption { get; set; } + + internal Func? MatchDirectoryInfo { get; set; } + + internal (FileMatch? FileMatch, Exception? Exception) Match(string path) + { + FileNameSpan span = FileNameSpan.FromDirectory(path, NamePart); + Match? match = null; + + if (Name is not null) + { + match = Name.Match(span); + + if (match is null) + return default; + } + + DirectoryInfo? directoryInfo = null; + + if (WithAttributes != 0 + || EmptyOption != FileEmptyOption.None + || MatchDirectoryInfo is not null) + { + try + { + directoryInfo = new DirectoryInfo(path); + + if (WithAttributes != 0 + && (directoryInfo.Attributes & WithAttributes) != WithAttributes) + { + return default; + } + + if (MatchDirectoryInfo?.Invoke(directoryInfo) == false) + return default; + + if (EmptyOption == FileEmptyOption.Empty) + { + if (!IsEmptyDirectory(path)) + return default; + } + else if (EmptyOption == FileEmptyOption.NonEmpty) + { + if (IsEmptyDirectory(path)) + return default; + } + } + catch (Exception ex) when (ex is IOException + || ex is UnauthorizedAccessException) + { + return (null, ex); + } + } + + return (new FileMatch(span, match, directoryInfo, isDirectory: true), null); + + static bool IsEmptyDirectory(string path) + { + return !Directory.EnumerateFileSystemEntries(path).Any(); + } + } +} diff --git a/src/FileSystem/FileSystem/FileCompareOptions.cs b/src/FileSystem/FileSystem/FileCompareProperties.cs similarity index 90% rename from src/FileSystem/FileSystem/FileCompareOptions.cs rename to src/FileSystem/FileSystem/FileCompareProperties.cs index cfd8d734..f43cb72c 100644 --- a/src/FileSystem/FileSystem/FileCompareOptions.cs +++ b/src/FileSystem/FileSystem/FileCompareProperties.cs @@ -5,7 +5,7 @@ namespace Orang.FileSystem; [Flags] -public enum FileCompareOptions +public enum FileCompareProperties { None = 0, Size = 1, diff --git a/src/FileSystem/FileSystem/FileContent.cs b/src/FileSystem/FileSystem/FileContent.cs index 7f578f8c..d4a85801 100644 --- a/src/FileSystem/FileSystem/FileContent.cs +++ b/src/FileSystem/FileSystem/FileContent.cs @@ -7,9 +7,9 @@ namespace Orang.FileSystem; [DebuggerDisplay("{DebuggerDisplay,nq}")] -public readonly struct FileContent +internal readonly struct FileContent { - public FileContent(string text, Encoding encoding, bool hasBom) + internal FileContent(string text, Encoding encoding, bool hasBom) { Text = text ?? throw new ArgumentNullException(nameof(text)); Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); @@ -22,6 +22,8 @@ public FileContent(string text, Encoding encoding, bool hasBom) public bool HasBom { get; } + public override string ToString() => Text; + [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay { @@ -32,6 +34,4 @@ private string DebuggerDisplay : "Uninitialized"; } } - - public override string ToString() => Text; } diff --git a/src/FileSystem/FileSystem/FileEmptyOption.cs b/src/FileSystem/FileSystem/FileEmptyOption.cs index 7b3c64a2..c698186e 100644 --- a/src/FileSystem/FileSystem/FileEmptyOption.cs +++ b/src/FileSystem/FileSystem/FileEmptyOption.cs @@ -4,7 +4,7 @@ namespace Orang.FileSystem; public enum FileEmptyOption { - None = 0, - Empty = 1, - NonEmpty = 2, + None, + Empty, + NonEmpty, } diff --git a/src/FileSystem/FileSystem/FileMatch.cs b/src/FileSystem/FileSystem/FileMatch.cs index 2ef3807a..8bbb1bb2 100644 --- a/src/FileSystem/FileSystem/FileMatch.cs +++ b/src/FileSystem/FileSystem/FileMatch.cs @@ -13,37 +13,37 @@ public class FileMatch private FileSystemInfo? _fileSystemInfo; internal FileMatch( - in FileNameSpan nameSpan, - Match? nameMatch, + FileNameSpan nameSpan, + Match? name, FileSystemInfo? fileSystemInfo = null, bool isDirectory = false) - : this(nameSpan, nameMatch, default(FileContent), default(Match), fileSystemInfo, isDirectory) + : this(name, nameSpan, default(Match), default(FileContent), fileSystemInfo, isDirectory) { } internal FileMatch( - in FileNameSpan nameSpan, - Match? nameMatch, - in FileContent content, - Match? contentMatch, + Match? name, + FileNameSpan nameSpan, + Match? content, + FileContent fileContent, FileSystemInfo? fileSystemInfo = null, bool isDirectory = false) { - NameMatch = nameMatch; + Name = name; NameSpan = nameSpan; + FileContent = fileContent; Content = content; - ContentMatch = contentMatch; IsDirectory = isDirectory; _fileSystemInfo = fileSystemInfo; } public FileNameSpan NameSpan { get; } - public Match? NameMatch { get; } + public Match? Name { get; } - public FileContent Content { get; } + internal FileContent FileContent { get; set; } - public Match? ContentMatch { get; } + public Match? Content { get; } public bool IsDirectory { get; } @@ -69,13 +69,13 @@ internal FileSystemInfo FileSystemInfo public string Path => NameSpan.Path; - internal int Index => (NameMatch?.Success == true) ? NameMatch.Index : -1; + internal int Index => (Name?.Success == true) ? Name.Index : -1; - internal int Length => (NameMatch?.Success == true) ? NameMatch.Length : -1; + internal int Length => (Name?.Success == true) ? Name.Length : -1; - internal string ContentText => Content.Text; + internal string ContentText => FileContent.Text; - internal Encoding Encoding => Content.Encoding; + internal Encoding Encoding => FileContent.Encoding; [DebuggerBrowsable(DebuggerBrowsableState.Never)] private string DebuggerDisplay => $"{Path}"; diff --git a/src/FileSystem/FileSystem/FileMatcher.cs b/src/FileSystem/FileSystem/FileMatcher.cs new file mode 100644 index 00000000..1f491d12 --- /dev/null +++ b/src/FileSystem/FileSystem/FileMatcher.cs @@ -0,0 +1,160 @@ +// Copyright (c) Josef Pihrt. 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.Text; +using System.Text.RegularExpressions; +using Orang.Text; + +#pragma warning disable RCS1223 + +namespace Orang.FileSystem; + +public class FileMatcher +{ + public Matcher? Name { get; set; } + + public FileNamePart NamePart { get; set; } + + public Matcher? Extension { get; set; } + + public Matcher? Content { get; set; } + + public FileAttributes WithAttributes { get; set; } + + public FileAttributes WithoutAttributes { get; set; } + + public FileEmptyOption EmptyOption { get; set; } + + internal Func? MatchFileInfo { get; set; } + + internal (FileMatch? FileMatch, Exception? Exception) Match(string path, Encoding? defaultEncoding = null) + { + if (Extension?.IsMatch(FileNameSpan.FromFile(path, FileNamePart.Extension)) == false) + return default; + + FileNameSpan span = FileNameSpan.FromFile(path, NamePart); + Match? match = null; + + if (Name is not null) + { + match = Name.Match(span); + + if (match is null) + return default; + } + + FileEmptyOption emptyOption = EmptyOption; + var isEmpty = false; + FileInfo? fileInfo = null; + + if (WithAttributes != 0 + || emptyOption != FileEmptyOption.None + || MatchFileInfo is not null) + { + try + { + fileInfo = new FileInfo(path); + + if (WithAttributes != 0 + && (fileInfo.Attributes & WithAttributes) != WithAttributes) + { + return default; + } + + if (MatchFileInfo?.Invoke(fileInfo) == false) + return default; + + if (emptyOption != FileEmptyOption.None) + { + if (fileInfo.Length == 0) + { + if (emptyOption == FileEmptyOption.NonEmpty) + return default; + + isEmpty = true; + } + else if (fileInfo.Length > 4) + { + if (emptyOption == FileEmptyOption.Empty) + return default; + + emptyOption = FileEmptyOption.None; + } + } + } + catch (Exception ex) when (ex is IOException + || ex is UnauthorizedAccessException) + { + return (null, ex); + } + } + + if (Content is not null + || emptyOption != FileEmptyOption.None) + { + FileContent fileContent = default; + + if (isEmpty) + { + fileContent = new FileContent("", defaultEncoding ?? EncodingHelpers.UTF8NoBom, hasBom: false); + } + else + { + try + { + using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) + { + Encoding? bomEncoding = FileSystemUtilities.DetectEncoding(stream); + + if (emptyOption != FileEmptyOption.None) + { + if (bomEncoding?.Preamble.Length == stream.Length) + { + if (emptyOption == FileEmptyOption.NonEmpty) + return default; + } + else if (emptyOption == FileEmptyOption.Empty) + { + return default; + } + } + + if (Content is not null) + { + using (var reader = new StreamReader( + stream: stream, + encoding: bomEncoding ?? defaultEncoding ?? EncodingHelpers.UTF8NoBom, + detectEncodingFromByteOrderMarks: bomEncoding is null)) + { + string content = reader.ReadToEnd(); + + fileContent = new FileContent( + content, + reader.CurrentEncoding, + hasBom: bomEncoding is not null); + } + } + } + } + catch (Exception ex) when (ex is IOException + || ex is UnauthorizedAccessException) + { + return (null, ex); + } + } + + if (Content is not null) + { + Match? contentMatch = Content.Match(fileContent.Text); + + if (contentMatch is null) + return default; + + return (new FileMatch(match, span, contentMatch, fileContent, fileInfo), null); + } + } + + return (new FileMatch(span, match, fileInfo), null); + } +} diff --git a/src/FileSystem/FileSystem/FileNamePart.cs b/src/FileSystem/FileSystem/FileNamePart.cs index 9d82782e..305be02f 100644 --- a/src/FileSystem/FileSystem/FileNamePart.cs +++ b/src/FileSystem/FileSystem/FileNamePart.cs @@ -2,7 +2,6 @@ namespace Orang.FileSystem; -//TODO: PathPart, FilePathPart public enum FileNamePart { Name = 0, diff --git a/src/FileSystem/FileSystem/FileNameSpan.cs b/src/FileSystem/FileSystem/FileNameSpan.cs index 9e2b6c65..e9c3ed4c 100644 --- a/src/FileSystem/FileSystem/FileNameSpan.cs +++ b/src/FileSystem/FileSystem/FileNameSpan.cs @@ -2,11 +2,10 @@ using System; using System.Diagnostics; -using static Orang.FileSystem.FileSystemHelpers; +using static Orang.FileSystem.FileSystemUtilities; namespace Orang.FileSystem; -//TODO: PathSpan, FilePathSpan [DebuggerDisplay("{DebuggerDisplay,nq}")] public readonly struct FileNameSpan { @@ -38,7 +37,7 @@ internal FileNameSpan(string path, int start, int length, FileNamePart part) public override string ToString() => Path.Substring(Start, Length); - public static FileNameSpan FromFile(string path, FileNamePart part) + internal static FileNameSpan FromFile(string path, FileNamePart part) { if (path is null) throw new ArgumentNullException(nameof(path)); @@ -68,9 +67,7 @@ public static FileNameSpan FromFile(string path, FileNamePart part) int dotIndex = GetExtensionIndex(path); if (dotIndex >= path.Length - 1) - { return new FileNameSpan(path, path.Length, 0, part); - } return new FileNameSpan(path, dotIndex + 1, path.Length - dotIndex - 1, part); } @@ -81,7 +78,7 @@ public static FileNameSpan FromFile(string path, FileNamePart part) } } - public static FileNameSpan FromDirectory(string path, FileNamePart part) + internal static FileNameSpan FromDirectory(string path, FileNamePart part) { if (path is null) throw new ArgumentNullException(nameof(path)); @@ -99,7 +96,6 @@ public static FileNameSpan FromDirectory(string path, FileNamePart part) { return new FileNameSpan(path, 0, path.Length, part); } - //case NamePartKind.Extension: default: { throw new ArgumentException("", nameof(part)); diff --git a/src/FileSystem/FileSystem/FilePropertyFilter.cs b/src/FileSystem/FileSystem/FilePropertyFilter.cs deleted file mode 100644 index befc2e5e..00000000 --- a/src/FileSystem/FileSystem/FilePropertyFilter.cs +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; - -#pragma warning disable RCS1223 - -namespace Orang.FileSystem; - -public class FilePropertyFilter -{ - public FilePropertyFilter( - Func? creationTimePredicate = null, - Func? modifiedTimePredicate = null, - Func? sizePredicate = null) - { - CreationTimePredicate = creationTimePredicate; - ModifiedTimePredicate = modifiedTimePredicate; - SizePredicate = sizePredicate; - } - - public Func? CreationTimePredicate { get; } - - public Func? ModifiedTimePredicate { get; } - - public Func? SizePredicate { get; } -} diff --git a/src/FileSystem/FileSystem/FileSystemExtensions.cs b/src/FileSystem/FileSystem/FileSystemExtensions.cs new file mode 100644 index 00000000..6398ac32 --- /dev/null +++ b/src/FileSystem/FileSystem/FileSystemExtensions.cs @@ -0,0 +1,96 @@ +// Copyright (c) Josef Pihrt. 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 System.Text; +using System.Text.RegularExpressions; +using System.Threading; +using Orang.Text.RegularExpressions; + +namespace Orang.FileSystem; + +internal static class FileSystemExtensions +{ + internal static bool IsMatch(this DirectoryMatcher matcher, string path) + { + return matcher.Match(path).FileMatch is not null; + } + + internal static string GetReplacement( + this FileMatch fileMatch, + Replacer replacer, + int count = 0, + Func? predicate = null, + CancellationToken cancellationToken = default) + { + ReplaceResult result = GetReplaceResult(fileMatch, replacer, count, predicate, cancellationToken); + + ListCache.Free(result.Items); + + return result.NewName; + } + + internal static ReplaceResult GetReplaceResult( + this FileMatch fileMatch, + Replacer replacer, + int count = 0, + Func? predicate = null, + CancellationToken cancellationToken = default) + { + List captures = ListCache.GetInstance(); + + MaxReason maxReason = CaptureFactory.GetCaptures( + ref captures, + fileMatch.Name!, + count: count, + predicate: predicate, + cancellationToken: cancellationToken); + + int offset = 0; + List replaceItems = ListCache.GetInstance(); + + if (replaceItems.Capacity < captures.Count) + replaceItems.Capacity = captures.Count; + + foreach (Match match in captures) + { + string value = replacer.Replace(match); + + replaceItems.Add(new ReplaceItem(match, value, match.Index + offset)); + + offset += value.Length - match.Length; + + cancellationToken.ThrowIfCancellationRequested(); + } + + ListCache.Free(captures); + + string newName = GetNewValue(fileMatch, replaceItems); + + return new ReplaceResult(replaceItems, maxReason, newName); + } + + private static string GetNewValue(FileMatch fileMatch, List replaceItems) + { + StringBuilder sb = StringBuilderCache.GetInstance(); + + string path = fileMatch.Path; + FileNameSpan span = fileMatch.NameSpan; + + int lastPos = span.Start; + + foreach (ReplaceItem item in replaceItems) + { + Match match = item.Match; + + sb.Append(path, lastPos, match.Index - lastPos); + sb.Append(item.Value); + + lastPos = match.Index + match.Length; + } + + sb.Append(path, lastPos, span.Start + span.Length - lastPos); + + return StringBuilderCache.GetStringAndFree(sb); + } +} diff --git a/src/FileSystem/FileSystem/FileSystemFilter.cs b/src/FileSystem/FileSystem/FileSystemFilter.cs deleted file mode 100644 index 1a9efa47..00000000 --- a/src/FileSystem/FileSystem/FileSystemFilter.cs +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.IO; - -#pragma warning disable RCS1223 - -namespace Orang.FileSystem; - -public class FileSystemFilter -{ - public FileSystemFilter( - Filter? name = null, - FileNamePart part = FileNamePart.Name, - Filter? extension = null, - Filter? content = null, - FilePropertyFilter? properties = null, - FileAttributes attributes = default, - FileAttributes attributesToSkip = default, - FileEmptyOption emptyOption = FileEmptyOption.None) - { - Name = name; - Part = part; - Extension = extension; - Content = content; - Properties = properties; - Attributes = attributes; - AttributesToSkip = attributesToSkip; - EmptyOption = emptyOption; - } - - public Filter? Name { get; } - - public FileNamePart Part { get; } - - public Filter? Extension { get; } - - public Filter? Content { get; } - - public FilePropertyFilter? Properties { get; } - - public FileAttributes Attributes { get; } - - public FileAttributes AttributesToSkip { get; } - - public FileEmptyOption EmptyOption { get; } -} diff --git a/src/FileSystem/FileSystem/FileSystemSearch.Static.cs b/src/FileSystem/FileSystem/FileSystemSearch.Static.cs deleted file mode 100644 index e296999d..00000000 --- a/src/FileSystem/FileSystem/FileSystemSearch.Static.cs +++ /dev/null @@ -1,303 +0,0 @@ -// Copyright (c) Josef Pihrt. 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 System.Collections.Immutable; -using System.IO; -using System.Threading; -using Orang.Operations; -using static Orang.FileSystem.FileSystemHelpers; - -namespace Orang.FileSystem; - -public partial class FileSystemSearch -{ - internal static void Replace( - string directoryPath, - FileSystemFilter filter, - IEnumerable? directoryFilters = null, - ReplaceOptions? replaceOptions = null, - SearchTarget searchTarget = SearchTarget.Files, - bool recurseSubdirectories = true, - bool dryRun = false, - CancellationToken cancellationToken = default) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - var searchOptions = new FileSystemSearchOptions( - searchTarget: searchTarget, - recurseSubdirectories: recurseSubdirectories); - - var search = new FileSystemSearch( - filter, - directoryFilters: directoryFilters?.ToImmutableArray() ?? ImmutableArray.Empty, - searchProgress: null, - options: searchOptions); - - if (filter.Content is null) - throw new InvalidOperationException("Content filter is not defined."); - - if (filter.Content.IsNegative) - throw new InvalidOperationException("Content filter cannot be negative."); - - var command = new ReplaceOperation() - { - Search = search, - ReplaceOptions = replaceOptions ?? ReplaceOptions.Empty, - Progress = null, - DryRun = dryRun, - CancellationToken = cancellationToken, - MaxMatchingFiles = 0, - MaxMatchesInFile = 0, - MaxTotalMatches = 0, - }; - - command.Execute(directoryPath); - } - - internal static void Delete( - string directoryPath, - FileSystemFilter filter, - IEnumerable? directoryFilters = null, - DeleteOptions? deleteOptions = null, - SearchTarget searchTarget = SearchTarget.Files, - IProgress? progress = null, - bool recurseSubdirectories = true, - bool dryRun = false, - CancellationToken cancellationToken = default) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - var searchOptions = new FileSystemSearchOptions( - searchTarget: searchTarget, - recurseSubdirectories: recurseSubdirectories); - - var search = new FileSystemSearch( - filter, - directoryFilters: directoryFilters?.ToImmutableArray() ?? ImmutableArray.Empty, - searchProgress: null, - options: searchOptions) - { - CanRecurseMatch = false - }; - - var command = new DeleteOperation() - { - Search = search, - DeleteOptions = deleteOptions ?? DeleteOptions.Default, - Progress = progress, - DryRun = dryRun, - CancellationToken = cancellationToken, - MaxMatchingFiles = 0, - }; - - command.Execute(directoryPath); - } - - internal static void Rename( - string directoryPath, - FileSystemFilter filter, - IEnumerable? directoryFilters = null, - RenameOptions? renameOptions = null, - SearchTarget searchTarget = SearchTarget.Files, - IDialogProvider? dialogProvider = null, - IProgress? progress = null, - bool recurseSubdirectories = true, - bool dryRun = false, - CancellationToken cancellationToken = default) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - var searchOptions = new FileSystemSearchOptions( - searchTarget: searchTarget, - recurseSubdirectories: recurseSubdirectories); - - var search = new FileSystemSearch( - filter, - directoryFilters: directoryFilters?.ToImmutableArray() ?? ImmutableArray.Empty, - searchProgress: null, - options: searchOptions) - { - DisallowEnumeration = !dryRun, - MatchPartOnly = true - }; - - if (filter.Part == FileNamePart.FullName) - throw new InvalidOperationException($"Invalid file name part '{nameof(FileNamePart.FullName)}'."); - - if (filter.Name is null) - throw new InvalidOperationException("Name filter is not defined."); - - if (filter.Name.IsNegative) - throw new InvalidOperationException("Name filter cannot be negative."); - - renameOptions ??= new RenameOptions(replacement: ""); - - VerifyConflictResolution(renameOptions.ConflictResolution, dialogProvider); - - var command = new RenameOperation() - { - Search = search, - RenameOptions = renameOptions, - Progress = progress, - DryRun = dryRun, - CancellationToken = cancellationToken, - DialogProvider = dialogProvider, - MaxMatchingFiles = 0, - }; - - command.Execute(directoryPath); - } - - internal static void Copy( - string directoryPath, - string destinationPath, - FileSystemFilter filter, - IEnumerable? directoryFilters = null, - CopyOptions? copyOptions = null, - SearchTarget searchTarget = SearchTarget.Files, - IDialogProvider? dialogProvider = null, - IProgress? progress = null, - bool recurseSubdirectories = true, - bool dryRun = false, - CancellationToken cancellationToken = default) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - var searchOptions = new FileSystemSearchOptions( - searchTarget: searchTarget, - recurseSubdirectories: recurseSubdirectories); - - ImmutableArray directoryFilters2 = directoryFilters?.ToImmutableArray() ?? ImmutableArray.Empty; - - directoryFilters2 = directoryFilters2.Add(NameFilter.CreateFromDirectoryPath(destinationPath, isNegative: true)); - - var search = new FileSystemSearch( - filter, - directoryFilters: directoryFilters2, - searchProgress: null, - options: searchOptions); - - copyOptions ??= CopyOptions.Default; - - VerifyCopyMoveArguments(directoryPath, destinationPath, copyOptions, dialogProvider); - - var command = new CopyOperation() - { - Search = search, - DestinationPath = destinationPath, - CopyOptions = copyOptions, - Progress = progress, - DryRun = dryRun, - CancellationToken = cancellationToken, - DialogProvider = dialogProvider, - MaxMatchingFiles = 0, - MaxMatchesInFile = 0, - MaxTotalMatches = 0, - ConflictResolution = copyOptions.ConflictResolution, - }; - - command.Execute(directoryPath); - } - - internal static void Move( - string directoryPath, - string destinationPath, - FileSystemFilter filter, - IEnumerable? directoryFilters = null, - CopyOptions? copyOptions = null, - SearchTarget searchTarget = SearchTarget.Files, - IDialogProvider? dialogProvider = null, - IProgress? progress = null, - bool recurseSubdirectories = true, - bool dryRun = false, - CancellationToken cancellationToken = default) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - var searchOptions = new FileSystemSearchOptions( - searchTarget: searchTarget, - recurseSubdirectories: recurseSubdirectories); - - ImmutableArray directoryFilters2 = directoryFilters?.ToImmutableArray() ?? ImmutableArray.Empty; - - directoryFilters2 = directoryFilters2.Add(NameFilter.CreateFromDirectoryPath(destinationPath, isNegative: true)); - - var search = new FileSystemSearch( - filter, - directoryFilters: directoryFilters2, - searchProgress: null, - options: searchOptions); - - copyOptions ??= CopyOptions.Default; - - VerifyCopyMoveArguments(directoryPath, destinationPath, copyOptions, dialogProvider); - - var command = new MoveOperation() - { - Search = search, - DestinationPath = destinationPath, - CopyOptions = copyOptions, - Progress = progress, - DryRun = dryRun, - CancellationToken = cancellationToken, - DialogProvider = dialogProvider, - MaxMatchingFiles = 0, - MaxMatchesInFile = 0, - MaxTotalMatches = 0, - ConflictResolution = copyOptions.ConflictResolution, - }; - - command.Execute(directoryPath); - } - - private static void VerifyCopyMoveArguments( - string directoryPath, - string destinationPath, - CopyOptions copyOptions, - IDialogProvider? dialogProvider) - { - if (directoryPath is null) - throw new ArgumentNullException(nameof(directoryPath)); - - if (destinationPath is null) - throw new ArgumentNullException(nameof(destinationPath)); - - if (!System.IO.Directory.Exists(directoryPath)) - throw new DirectoryNotFoundException($"Directory not found: {directoryPath}"); - - if (!System.IO.Directory.Exists(destinationPath)) - throw new DirectoryNotFoundException($"Directory not found: {destinationPath}"); - - if (IsSubdirectory(destinationPath, directoryPath)) - { - throw new ArgumentException( - "Source directory cannot be subdirectory of a destination directory.", - nameof(directoryPath)); - } - - VerifyConflictResolution(copyOptions.ConflictResolution, dialogProvider); - } - - private static void VerifyConflictResolution( - ConflictResolution conflictResolution, - IDialogProvider? dialogProvider) - { - if (conflictResolution == ConflictResolution.Ask - && dialogProvider is null) - { - throw new ArgumentNullException( - nameof(dialogProvider), - $"'{nameof(dialogProvider)}' cannot be null when {nameof(ConflictResolution)} " - + $"is set to {nameof(ConflictResolution.Ask)}."); - } - } -} diff --git a/src/FileSystem/FileSystem/FileSystemSearch.cs b/src/FileSystem/FileSystem/FileSystemSearch.cs deleted file mode 100644 index 4a52e07f..00000000 --- a/src/FileSystem/FileSystem/FileSystemSearch.cs +++ /dev/null @@ -1,551 +0,0 @@ -// Copyright (c) Josef Pihrt. 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 System.Collections.Immutable; -using System.Diagnostics; -using System.IO; -using System.Linq; -using System.Security; -using System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using static Orang.FileSystem.FileSystemHelpers; - -#pragma warning disable RCS1223 - -namespace Orang.FileSystem; - -//TODO: FileSearch, Search -public partial class FileSystemSearch -{ - private readonly bool _allDirectoryFiltersArePositive; - - public FileSystemSearch( - FileSystemFilter filter, - NameFilter? directoryFilter = null, - IProgress? searchProgress = null, - FileSystemSearchOptions? options = null) - : this( - filter, - (directoryFilter is not null) ? ImmutableArray.Create(directoryFilter) : ImmutableArray.Empty, - searchProgress, - options) - { - } - - public FileSystemSearch( - FileSystemFilter filter, - IEnumerable directoryFilters, - IProgress? searchProgress = null, - FileSystemSearchOptions? options = null) - { - if (directoryFilters is null) - throw new ArgumentNullException(nameof(directoryFilters)); - - foreach (NameFilter directoryFilter in directoryFilters) - { - if (directoryFilter is null) - throw new ArgumentException("", nameof(directoryFilter)); - - if (directoryFilter?.Part == FileNamePart.Extension) - { - throw new ArgumentException( - $"Directory filter has invalid part '{FileNamePart.Extension}'.", - nameof(directoryFilter)); - } - } - - Filter = filter ?? throw new ArgumentNullException(nameof(filter)); - DirectoryFilters = directoryFilters.ToImmutableArray(); - SearchProgress = searchProgress; - Options = options ?? FileSystemSearchOptions.Default; - - _allDirectoryFiltersArePositive = DirectoryFilters.All(f => !f.IsNegative); - } - - public FileSystemFilter Filter { get; } - - public ImmutableArray DirectoryFilters { get; } - - public FileSystemSearchOptions Options { get; } - - public IProgress? SearchProgress { get; } - - internal bool DisallowEnumeration { get; set; } - - internal bool MatchPartOnly { get; set; } - - internal bool CanRecurseMatch { get; set; } = true; - - private FileNamePart Part => Filter.Part; - - private Filter? Name => Filter.Name; - - private Filter? Extension => Filter.Extension; - - private Filter? Content => Filter.Content; - - private FilePropertyFilter? Properties => Filter.Properties; - - private FileAttributes Attributes => Filter.Attributes; - - private FileAttributes AttributesToSkip => Filter.AttributesToSkip; - - private FileEmptyOption EmptyOption => Filter.EmptyOption; - - private SearchTarget SearchTarget => Options.SearchTarget; - - private bool RecurseSubdirectories => Options.RecurseSubdirectories; - - public IEnumerable Find( - string directoryPath, - CancellationToken cancellationToken = default) - { - return Find(directoryPath, default(INotifyDirectoryChanged), cancellationToken); - } - - internal IEnumerable Find( - string directoryPath, - INotifyDirectoryChanged? notifyDirectoryChanged = null, - CancellationToken cancellationToken = default) - { - var enumerationOptions = new EnumerationOptions() - { - AttributesToSkip = AttributesToSkip, - IgnoreInaccessible = Options.IgnoreInaccessible, - MatchCasing = MatchCasing.PlatformDefault, - MatchType = MatchType.Simple, - RecurseSubdirectories = false, - ReturnSpecialDirectories = false - }; - - var directories = new Queue(); - Queue? subdirectories = (RecurseSubdirectories) ? new Queue() : null; - - string? currentDirectory = null; - - if (notifyDirectoryChanged is not null) - { - notifyDirectoryChanged.DirectoryChanged - += (object sender, DirectoryChangedEventArgs e) => currentDirectory = e.NewName; - } - - MatchStatus matchStatus = (DirectoryFilters.Any()) ? MatchStatus.Unknown : MatchStatus.Success; - - foreach (NameFilter directoryFilter in DirectoryFilters.Where(f => !f.IsNegative)) - { - if (IsMatch(directoryPath, directoryFilter)) - { - matchStatus = MatchStatus.Success; - } - else - { - matchStatus = MatchStatus.FailFromPositive; - break; - } - } - - var directory = new Directory(directoryPath, matchStatus); - - while (true) - { - Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true); - - if (SearchTarget != SearchTarget.Directories - && !directory.IsFail) - { - IEnumerator fi = null!; - - try - { - fi = (DisallowEnumeration) - ? ((IEnumerable)GetFiles(directory.Path, enumerationOptions)).GetEnumerator() - : EnumerateFiles(directory.Path, enumerationOptions).GetEnumerator(); - } - catch (Exception ex) when (IsWellKnownException(ex)) - { - Debug.Fail(ex.ToString()); - Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true, ex); - } - - if (fi is not null) - { - using (fi) - { - while (fi.MoveNext()) - { - FileMatch? match = MatchFile(fi.Current); - - if (match is not null) - yield return match; - - cancellationToken.ThrowIfCancellationRequested(); - } - } - } - } - - IEnumerator di = null!; - - try - { - di = (DisallowEnumeration) - ? ((IEnumerable)GetDirectories(directory.Path, enumerationOptions)).GetEnumerator() - : EnumerateDirectories(directory.Path, enumerationOptions).GetEnumerator(); - } - catch (Exception ex) when (IsWellKnownException(ex)) - { - Debug.Fail(ex.ToString()); - Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true, ex); - } - - if (di is not null) - { - using (di) - { - while (di.MoveNext()) - { - currentDirectory = di.Current; - - matchStatus = (_allDirectoryFiltersArePositive && directory.IsSuccess) - ? MatchStatus.Success - : MatchStatus.Unknown; - - if (!directory.IsFail - && SearchTarget != SearchTarget.Files - && Part != FileNamePart.Extension) - { - if (matchStatus == MatchStatus.Unknown - && DirectoryFilters.Any()) - { - matchStatus = IncludeDirectory(currentDirectory); - } - - if (matchStatus == MatchStatus.Success - || matchStatus == MatchStatus.Unknown) - { - FileMatch? match = MatchDirectory(currentDirectory); - - if (match is not null) - { - yield return match; - - if (!CanRecurseMatch) - currentDirectory = null; - } - } - } - - if (currentDirectory is not null - && RecurseSubdirectories) - { - if (matchStatus == MatchStatus.Unknown - && DirectoryFilters.Any()) - { - matchStatus = IncludeDirectory(currentDirectory); - } - - if (matchStatus != MatchStatus.FailFromNegative) - subdirectories!.Enqueue(new Directory(currentDirectory, matchStatus)); - } - - cancellationToken.ThrowIfCancellationRequested(); - } - } - - if (RecurseSubdirectories) - { - while (subdirectories!.Count > 0) - directories.Enqueue(subdirectories.Dequeue()); - } - } - - if (directories.Count > 0) - { - directory = directories.Dequeue(); - } - else - { - break; - } - } - } - - public FileMatch? MatchFile(string path) - { - (FileMatch? fileMatch, Exception? exception) = MatchFileImpl(path); - - Report(path, SearchProgressKind.File, exception: exception); - - return fileMatch; - } - - private (FileMatch? fileMatch, Exception? exception) MatchFileImpl(string path) - { - if (Extension?.IsMatch(FileNameSpan.FromFile(path, FileNamePart.Extension)) == false) - return default; - - FileNameSpan span = FileNameSpan.FromFile(path, Part); - Match? match = null; - - if (Name is not null) - { - match = Name.Match(span, MatchPartOnly); - - if (match is null) - return default; - } - - FileEmptyOption emptyOption = EmptyOption; - var isEmpty = false; - FileInfo? fileInfo = null; - - if (Attributes != 0 - || emptyOption != FileEmptyOption.None - || Properties is not null) - { - try - { - fileInfo = new FileInfo(path); - - if (Attributes != 0 - && (fileInfo.Attributes & Attributes) != Attributes) - { - return default; - } - - if (Properties is not null) - { - if (Properties.SizePredicate?.Invoke(fileInfo.Length) == false) - return default; - - if (Properties.CreationTimePredicate?.Invoke(fileInfo.CreationTime) == false) - return default; - - if (Properties.ModifiedTimePredicate?.Invoke(fileInfo.LastWriteTime) == false) - return default; - } - - if (emptyOption != FileEmptyOption.None) - { - if (fileInfo.Length == 0) - { - if (emptyOption == FileEmptyOption.NonEmpty) - return default; - - isEmpty = true; - } - else if (fileInfo.Length > 4) - { - if (emptyOption == FileEmptyOption.Empty) - return default; - - emptyOption = FileEmptyOption.None; - } - } - } - catch (Exception ex) when (ex is IOException - || ex is UnauthorizedAccessException) - { - return (null, ex); - } - } - - if (Content is not null - || emptyOption != FileEmptyOption.None) - { - FileContent fileContent = default; - - if (isEmpty) - { - fileContent = new FileContent("", Options.DefaultEncoding, hasBom: false); - } - else - { - try - { - using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read)) - { - Encoding? bomEncoding = DetectEncoding(stream); - - if (emptyOption != FileEmptyOption.None) - { - if (bomEncoding?.Preamble.Length == stream.Length) - { - if (emptyOption == FileEmptyOption.NonEmpty) - return default; - } - else if (emptyOption == FileEmptyOption.Empty) - { - return default; - } - } - - if (Content is not null) - { - using (var reader = new StreamReader( - stream: stream, - encoding: bomEncoding ?? Options.DefaultEncoding, - detectEncodingFromByteOrderMarks: bomEncoding is null)) - { - string content = reader.ReadToEnd(); - - fileContent = new FileContent( - content, - reader.CurrentEncoding, - hasBom: bomEncoding is not null); - } - } - } - } - catch (Exception ex) when (ex is IOException - || ex is UnauthorizedAccessException) - { - return (null, ex); - } - } - - if (Content is not null) - { - Match? contentMatch = Content.Match(fileContent.Text); - - if (contentMatch is null) - return default; - - return (new FileMatch(span, match, fileContent, contentMatch, fileInfo), null); - } - } - - return (new FileMatch(span, match, fileInfo), null); - } - - public FileMatch? MatchDirectory(string path) - { - (FileMatch? fileMatch, Exception? exception) = MatchDirectoryImpl(path); - - Report(path, SearchProgressKind.Directory, isDirectory: true, exception); - - return fileMatch; - } - - private (FileMatch? fileMatch, Exception? exception) MatchDirectoryImpl(string path) - { - FileNameSpan span = FileNameSpan.FromDirectory(path, Part); - Match? match = null; - - if (Name is not null) - { - match = Name.Match(span, MatchPartOnly); - - if (match is null) - return default; - } - - DirectoryInfo? directoryInfo = null; - - if (Attributes != 0 - || EmptyOption != FileEmptyOption.None - || Properties is not null) - { - try - { - directoryInfo = new DirectoryInfo(path); - - if (Attributes != 0 - && (directoryInfo.Attributes & Attributes) != Attributes) - { - return default; - } - - if (Properties is not null) - { - if (Properties.CreationTimePredicate?.Invoke(directoryInfo.CreationTime) == false) - return default; - - if (Properties.ModifiedTimePredicate?.Invoke(directoryInfo.LastWriteTime) == false) - return default; - } - - if (EmptyOption == FileEmptyOption.Empty) - { - if (!IsEmptyDirectory(path)) - return default; - } - else if (EmptyOption == FileEmptyOption.NonEmpty) - { - if (IsEmptyDirectory(path)) - return default; - } - } - catch (Exception ex) when (ex is IOException - || ex is UnauthorizedAccessException) - { - return (null, ex); - } - } - - return (new FileMatch(span, match, directoryInfo, isDirectory: true), null); - } - - private MatchStatus IncludeDirectory(string path) - { - Debug.Assert(DirectoryFilters.Any()); - - foreach (NameFilter filter in DirectoryFilters) - { - if (!IsMatch(path, filter)) - return (filter.IsNegative) ? MatchStatus.FailFromNegative : MatchStatus.FailFromPositive; - } - - return MatchStatus.Success; - } - - public static bool IsMatch(string directoryPath, NameFilter filter) - { - return filter.Name.IsMatch(FileNameSpan.FromDirectory(directoryPath, filter.Part)); - } - - private void Report(string path, SearchProgressKind kind, bool isDirectory = false, Exception? exception = null) - { - SearchProgress?.Report(path, kind, isDirectory: isDirectory, exception); - } - - private static bool IsWellKnownException(Exception ex) - { - return ex is IOException - || ex is ArgumentException - || ex is UnauthorizedAccessException - || ex is SecurityException - || ex is NotSupportedException; - } - - [DebuggerDisplay("{DebuggerDisplay,nq}")] - private readonly struct Directory - { - public Directory(string path, MatchStatus status) - { - Path = path; - Status = status; - } - - public string Path { get; } - - public MatchStatus Status { get; } - - public bool IsSuccess => Status == MatchStatus.Success; - - public bool IsFail => Status == MatchStatus.FailFromPositive || Status == MatchStatus.FailFromNegative; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => Path; - } - - private enum MatchStatus - { - Unknown = 0, - Success = 1, - FailFromPositive = 2, - FailFromNegative = 3, - } -} diff --git a/src/FileSystem/FileSystem/FileSystemSearchOptions.cs b/src/FileSystem/FileSystem/FileSystemSearchOptions.cs deleted file mode 100644 index 6d534c3a..00000000 --- a/src/FileSystem/FileSystem/FileSystemSearchOptions.cs +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Diagnostics; -using System.Text; -using Orang.Text; - -namespace Orang.FileSystem; - -//TODO: FileSearchOptions, SearchOptions -[DebuggerDisplay("{DebuggerDisplay,nq}")] -public class FileSystemSearchOptions -{ - public static FileSystemSearchOptions Default { get; } = new(); - - public FileSystemSearchOptions( - SearchTarget searchTarget = SearchTarget.Files, - bool recurseSubdirectories = true, - bool ignoreInaccessible = true, - Encoding? defaultEncoding = null) - { - SearchTarget = searchTarget; - RecurseSubdirectories = recurseSubdirectories; - IgnoreInaccessible = ignoreInaccessible; - DefaultEncoding = defaultEncoding ?? EncodingHelpers.UTF8NoBom; - } - - public SearchTarget SearchTarget { get; } - - public bool RecurseSubdirectories { get; } - - public bool IgnoreInaccessible { get; } - - public Encoding DefaultEncoding { get; } - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => $"{SearchTarget} {nameof(RecurseSubdirectories)} = {RecurseSubdirectories}"; -} diff --git a/src/FileSystem/FileSystem/FileSystemHelpers.cs b/src/FileSystem/FileSystem/FileSystemUtilities.cs similarity index 85% rename from src/FileSystem/FileSystem/FileSystemHelpers.cs rename to src/FileSystem/FileSystem/FileSystemUtilities.cs index dadf7213..1769fef2 100644 --- a/src/FileSystem/FileSystem/FileSystemHelpers.cs +++ b/src/FileSystem/FileSystem/FileSystemUtilities.cs @@ -13,8 +13,10 @@ namespace Orang.FileSystem; -internal static class FileSystemHelpers +internal static class FileSystemUtilities { + internal static FileAttributes AllFileAttributes { get; } = GetAllFileAttributes(); + private static ImmutableHashSet? _invalidFileNameChars; private static readonly EnumerationOptions _enumerationOptionsNoRecurse = new() @@ -94,34 +96,34 @@ private static bool GetIsCaseSensitive() internal static bool FileEquals( string path1, string path2, - FileCompareOptions options, - FileAttributes ignoredAttributes = 0, + FileCompareProperties properties, + FileAttributes? compareAttributes = null, TimeSpan? allowedTimeDiff = null) { - if ((options & FileCompareOptions.ModifiedTime) != 0 + if ((properties & FileCompareProperties.ModifiedTime) != 0 && !LastWriteTimeUtcEquals(path1, path2, allowedTimeDiff)) { return false; } - if ((options & FileCompareOptions.Attributes) != 0 - && !AttributeEquals(path1, path2, ignoredAttributes)) + if ((properties & FileCompareProperties.Attributes) != 0 + && !AttributeEquals(path1, path2, compareAttributes)) { return false; } - if ((options & (FileCompareOptions.Size | FileCompareOptions.Content)) != 0) + if ((properties & (FileCompareProperties.Size | FileCompareProperties.Content)) != 0) { using (var fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read)) using (var fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read)) { - if ((options & FileCompareOptions.Size) != 0 + if ((properties & FileCompareProperties.Size) != 0 && fs1.Length != fs2.Length) { return false; } - if ((options & FileCompareOptions.Content) != 0 + if ((properties & FileCompareProperties.Content) != 0 && !StreamComparer.Default.ByteEquals(fs1, fs2)) { return false; @@ -132,73 +134,73 @@ internal static bool FileEquals( return true; } - internal static FileCompareOptions CompareFiles( + internal static FileCompareProperties CompareFiles( string path1, string path2, - FileCompareOptions options, - FileAttributes ignoredAttributes = 0, + FileCompareProperties properties, + FileAttributes? attributes = null, TimeSpan? allowedTimeDiff = null) { - if ((options & FileCompareOptions.ModifiedTime) != 0 + if ((properties & FileCompareProperties.ModifiedTime) != 0 && !LastWriteTimeUtcEquals(path1, path2, allowedTimeDiff)) { - return FileCompareOptions.ModifiedTime; + return FileCompareProperties.ModifiedTime; } - if ((options & FileCompareOptions.Attributes) != 0 - && !AttributeEquals(path1, path2, ignoredAttributes)) + if ((properties & FileCompareProperties.Attributes) != 0 + && !AttributeEquals(path1, path2, attributes)) { - if ((options & (FileCompareOptions.Size | FileCompareOptions.Content)) != 0) + if ((properties & (FileCompareProperties.Size | FileCompareProperties.Content)) != 0) { using (var fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read)) using (var fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read)) { - if ((options & FileCompareOptions.Size) != 0 + if ((properties & FileCompareProperties.Size) != 0 && fs1.Length != fs2.Length) { - return FileCompareOptions.Size; + return FileCompareProperties.Size; } - if ((options & FileCompareOptions.Content) != 0 + if ((properties & FileCompareProperties.Content) != 0 && !StreamComparer.Default.ByteEquals(fs1, fs2)) { - return FileCompareOptions.Content; + return FileCompareProperties.Content; } } } - return FileCompareOptions.Attributes; + return FileCompareProperties.Attributes; } - if ((options & (FileCompareOptions.Size | FileCompareOptions.Content)) != 0) + if ((properties & (FileCompareProperties.Size | FileCompareProperties.Content)) != 0) { using (var fs1 = new FileStream(path1, FileMode.Open, FileAccess.Read)) using (var fs2 = new FileStream(path2, FileMode.Open, FileAccess.Read)) { - if ((options & FileCompareOptions.Size) != 0 + if ((properties & FileCompareProperties.Size) != 0 && fs1.Length != fs2.Length) { - return FileCompareOptions.Size; + return FileCompareProperties.Size; } - if ((options & FileCompareOptions.Content) != 0 + if ((properties & FileCompareProperties.Content) != 0 && !StreamComparer.Default.ByteEquals(fs1, fs2)) { - return FileCompareOptions.Content; + return FileCompareProperties.Content; } } } - return FileCompareOptions.None; + return FileCompareProperties.None; } internal static bool AttributeEquals( string path1, string path2, - FileAttributes ignoredAttributes = 0) + FileAttributes? attributes = null) { - FileAttributes attr1 = File.GetAttributes(path1) & ~ignoredAttributes; - FileAttributes attr2 = File.GetAttributes(path2) & ~ignoredAttributes; + FileAttributes attr1 = File.GetAttributes(path1) & (attributes ?? AllFileAttributes); + FileAttributes attr2 = File.GetAttributes(path2) & (attributes ?? AllFileAttributes); return attr1 == attr2; } @@ -331,13 +333,14 @@ public static void DeleteFile(string filePath, bool contentOnly, bool includingB { if (contentOnly) { - FileAccess fileAccess = (includingBom) ? FileAccess.Write : FileAccess.ReadWrite; - - using (var stream = new FileStream(filePath, FileMode.Open, fileAccess)) + using (var stream = new FileStream( + filePath, + FileMode.Open, + (includingBom) ? FileAccess.Write : FileAccess.ReadWrite)) { int length = 0; - if (includingBom) + if (!includingBom) { Encoding? encoding = DetectEncoding(stream); @@ -439,9 +442,7 @@ public static long GetDirectorySize(string directoryPath) long size = 0; foreach (string filePath in EnumerateFiles(directoryPath, _enumerationOptionsRecurse)) - { size += new FileInfo(filePath).Length; - } return size; } @@ -583,4 +584,16 @@ public static string EnsureFullPath(string path) { return (Path.IsPathRooted(path)) ? path : Path.GetFullPath(path); } + + private static FileAttributes GetAllFileAttributes() + { + var attributes = (FileAttributes)0; + + foreach (FileAttributes value in Enum.GetValues(typeof(FileAttributes)).Cast()) + { + attributes |= value; + } + + return attributes; + } } diff --git a/src/FileSystem/FileSystem/Fluent/CopyOperation.cs b/src/FileSystem/FileSystem/Fluent/CopyOperation.cs new file mode 100644 index 00000000..11640d8a --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/CopyOperation.cs @@ -0,0 +1,32 @@ +// Copyright (c) Josef Pihrt. 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.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class CopyOperation +{ + internal CopyOperation(Search search, CopyOptions options) + { + Search = search ?? throw new ArgumentNullException(nameof(search)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Search Search { get; } + + public CopyOptions Options { get; } + + public IOperationResult Run(string directoryPath, string destinationPath, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (destinationPath is null) + throw new ArgumentNullException(nameof(destinationPath)); + + return Search.Copy(directoryPath, destinationPath, Options, cancellationToken); + } +} diff --git a/src/FileSystem/FileSystem/Fluent/CopyOperationBuilder.cs b/src/FileSystem/FileSystem/Fluent/CopyOperationBuilder.cs new file mode 100644 index 00000000..ad856bd5 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/CopyOperationBuilder.cs @@ -0,0 +1,111 @@ +// Copyright (c) Josef Pihrt. 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; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class CopyOperationBuilder +{ + private ConflictResolution _conflictResolution = ConflictResolution.Skip; + private bool _dryRun; + private Action? _logOperation; + private FileCompareProperties _comparedProperties; + private FileAttributes? _comparedAttributes; + private TimeSpan? _allowedTimeDiff; + private bool _flat; + private IDialogProvider? _dialogProvider; + private Func? _compareFile; + private Func? _compareDirectory; + + internal CopyOperationBuilder() + { + } + + public CopyOperationBuilder WithConflictResolution(ConflictResolution conflictResolution) + { + _conflictResolution = conflictResolution; + return this; + } + + public CopyOperationBuilder CompareSize() + { + _comparedProperties |= FileCompareProperties.Size; + return this; + } + + public CopyOperationBuilder CompareModifiedTime(TimeSpan? allowedTimeDiff = null) + { + _comparedProperties |= FileCompareProperties.ModifiedTime; + _allowedTimeDiff = allowedTimeDiff; + return this; + } + + public CopyOperationBuilder CompareAttributes(FileAttributes? attributes = null) + { + _comparedProperties |= FileCompareProperties.Attributes; + _comparedAttributes = attributes; + return this; + } + + public CopyOperationBuilder CompareContent() + { + _comparedProperties |= FileCompareProperties.Content; + return this; + } + + internal CopyOperationBuilder CompareFile(Func comparer) + { + _compareFile = comparer; + return this; + } + + internal CopyOperationBuilder CompareDirectory(Func comparer) + { + _compareDirectory = comparer; + return this; + } + + public CopyOperationBuilder Flat() + { + _flat = true; + return this; + } + + public CopyOperationBuilder DryRun() + { + _dryRun = true; + return this; + } + + public CopyOperationBuilder LogOperation(Action logOperation) + { + _logOperation = logOperation; + return this; + } + + public CopyOperationBuilder WithDialogProvider(IDialogProvider dialogProvider) + { + _dialogProvider = dialogProvider; + return this; + } + + internal CopyOptions CreateOptions() + { + return new CopyOptions() + { + ConflictResolution = _conflictResolution, + ComparedProperties = _comparedProperties, + ComparedAttributes = _comparedAttributes, + AllowedTimeDiff = _allowedTimeDiff, + Flat = _flat, + DryRun = _dryRun, + LogOperation = _logOperation, + DialogProvider = _dialogProvider, + CompareFile = _compareFile, + CompareDirectory = _compareDirectory, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/DeleteOperation.cs b/src/FileSystem/FileSystem/Fluent/DeleteOperation.cs new file mode 100644 index 00000000..47042259 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/DeleteOperation.cs @@ -0,0 +1,46 @@ +// Copyright (c) Josef Pihrt. 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 System.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class DeleteOperation +{ + internal DeleteOperation(Search search, DeleteOptions options) + { + Search = search ?? throw new ArgumentNullException(nameof(search)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Search Search { get; } + + public DeleteOptions Options { get; } + + public IOperationResult Run(string directoryPath, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + return Search.Delete(directoryPath, Options, cancellationToken); + } + + public IOperationResult Run(IEnumerable directoryPaths, CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + var telemetry = new SearchTelemetry(); + + foreach (string directoryPath in directoryPaths) + { + IOperationResult result = Search.Delete(directoryPath, Options, cancellationToken); + telemetry.Add(result.Telemetry); + } + + return new OperationResult(telemetry); + } +} diff --git a/src/FileSystem/FileSystem/Fluent/DeleteOperationBuilder.cs b/src/FileSystem/FileSystem/Fluent/DeleteOperationBuilder.cs new file mode 100644 index 00000000..2df19c4a --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/DeleteOperationBuilder.cs @@ -0,0 +1,54 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class DeleteOperationBuilder +{ + private bool _contentOnly; + private bool _includingBom; + private bool _dryRun; + private Action? _logOperation; + + internal DeleteOperationBuilder() + { + } + + public DeleteOperationBuilder ContentOnly() + { + _contentOnly = true; + return this; + } + + public DeleteOperationBuilder IncludingBom() + { + _includingBom = true; + return this; + } + + public DeleteOperationBuilder DryRun() + { + _dryRun = true; + return this; + } + + public DeleteOperationBuilder LogOperation(Action logOperation) + { + _logOperation = logOperation; + return this; + } + + internal DeleteOptions CreateOptions() + { + return new DeleteOptions() + { + ContentOnly = _contentOnly, + IncludingBom = _includingBom, + DryRun = _dryRun, + LogOperation = _logOperation + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/DirectoryMatcherBuilder.cs b/src/FileSystem/FileSystem/Fluent/DirectoryMatcherBuilder.cs new file mode 100644 index 00000000..733358fc --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/DirectoryMatcherBuilder.cs @@ -0,0 +1,81 @@ +// Copyright (c) Josef Pihrt. 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.Text.RegularExpressions; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class DirectoryMatcherBuilder +{ + private Matcher? _name; + private FileNamePart? _namePart; + private FileAttributes? _attributes; + private FileAttributes? _attributesToSkip; + private FileEmptyOption? _emptyOption; + + internal DirectoryMatcherBuilder() + { + } + + public DirectoryMatcherBuilder Name( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + _name = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public DirectoryMatcherBuilder NamePart(FileNamePart namePart) + { + _namePart = namePart; + + return this; + } + + public DirectoryMatcherBuilder WithAttributes(FileAttributes attributes) + { + _attributes = attributes; + + return this; + } + + public DirectoryMatcherBuilder WithoutAttributes(FileAttributes attributes) + { + _attributesToSkip = attributes; + + return this; + } + + public DirectoryMatcherBuilder Empty() + { + _emptyOption = FileEmptyOption.Empty; + + return this; + } + + public DirectoryMatcherBuilder NonEmpty() + { + _emptyOption = FileEmptyOption.NonEmpty; + + return this; + } + + internal DirectoryMatcher Build() + { + return new DirectoryMatcher() + { + Name = _name, + NamePart = _namePart ?? FileNamePart.Name, + WithAttributes = _attributes ?? 0, + WithoutAttributes = _attributesToSkip ?? 0, + EmptyOption = _emptyOption ?? FileEmptyOption.None, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/FileMatcherBuilder.cs b/src/FileSystem/FileSystem/Fluent/FileMatcherBuilder.cs new file mode 100644 index 00000000..e43e8375 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/FileMatcherBuilder.cs @@ -0,0 +1,149 @@ +// Copyright (c) Josef Pihrt. 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.Text.RegularExpressions; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class FileMatcherBuilder +{ + private Matcher? _name; + private FileNamePart? _namePart; + private Matcher? _content; + private static Matcher? _extension; + private FileAttributes? _withAttributes; + private FileAttributes? _withoutAttributes; + private FileEmptyOption? _emptyOption; + + internal FileMatcherBuilder() + { + } + + public FileMatcherBuilder Extension( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + _extension = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public FileMatcherBuilder WithExtensions(params string[] extensions) + { + _extension = GetExtensionMatcher(extensions, invert: false); + + return this; + } + + public FileMatcherBuilder WithoutExtensions(params string[] extensions) + { + _extension = GetExtensionMatcher(extensions, invert: true); + + return this; + } + + private static Matcher? GetExtensionMatcher(string[] extensions, bool invert) + { + if (extensions.Length == 0) + { + return null; + } + else if (extensions.Length == 1) + { + return new Matcher( + Pattern.Create( + extensions[0], + PatternOptions.Equals | PatternOptions.Literal), + (FileSystemUtilities.IsCaseSensitive) ? RegexOptions.None : RegexOptions.IgnoreCase, + invert: invert); + } + else + { + return new Matcher( + Pattern.Any( + extensions, + PatternOptions.Equals | PatternOptions.Literal), + (FileSystemUtilities.IsCaseSensitive) ? RegexOptions.None : RegexOptions.IgnoreCase, + invert: invert); + } + } + + public FileMatcherBuilder Name( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + _name = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public FileMatcherBuilder NamePart(FileNamePart namePart) + { + _namePart = namePart; + + return this; + } + + public FileMatcherBuilder Content( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + _content = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public FileMatcherBuilder WithAttributes(FileAttributes attributes) + { + _withAttributes = attributes; + + return this; + } + + public FileMatcherBuilder WithoutAttributes(FileAttributes attributes) + { + _withoutAttributes = attributes; + + return this; + } + + public FileMatcherBuilder Empty() + { + _emptyOption = FileEmptyOption.Empty; + + return this; + } + + public FileMatcherBuilder NonEmpty() + { + _emptyOption = FileEmptyOption.NonEmpty; + + return this; + } + + internal FileMatcher Build() + { + return new FileMatcher() + { + Name = _name, + NamePart = _namePart ?? FileNamePart.Name, + Extension = _extension, + Content = _content, + WithAttributes = _withAttributes ?? 0, + WithoutAttributes = _withoutAttributes ?? 0, + EmptyOption = _emptyOption ?? FileEmptyOption.None, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/MoveOperation.cs b/src/FileSystem/FileSystem/Fluent/MoveOperation.cs new file mode 100644 index 00000000..0a72eb2a --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/MoveOperation.cs @@ -0,0 +1,32 @@ +// Copyright (c) Josef Pihrt. 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.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class MoveOperation +{ + internal MoveOperation(Search search, CopyOptions options) + { + Search = search ?? throw new ArgumentNullException(nameof(search)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Search Search { get; } + + public CopyOptions Options { get; } + + public IOperationResult Run(string directoryPath, string destinationPath, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (destinationPath is null) + throw new ArgumentNullException(nameof(destinationPath)); + + return Search.Move(directoryPath, destinationPath, Options, cancellationToken); + } +} diff --git a/src/FileSystem/FileSystem/Fluent/MoveOperationBuilder.cs b/src/FileSystem/FileSystem/Fluent/MoveOperationBuilder.cs new file mode 100644 index 00000000..15c97ca4 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/MoveOperationBuilder.cs @@ -0,0 +1,111 @@ +// Copyright (c) Josef Pihrt. 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; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class MoveOperationBuilder +{ + private ConflictResolution _conflictResolution = ConflictResolution.Skip; + private bool _dryRun; + private bool _flat; + private Action? _logOperation; + private FileCompareProperties _comparedProperties; + private FileAttributes? _comparedAttributes; + private TimeSpan? _allowedTimeDiff; + private IDialogProvider? _dialogProvider; + private Func? _compareFile; + private Func? _compareDirectory; + + internal MoveOperationBuilder() + { + } + + public MoveOperationBuilder WithConflictResolution(ConflictResolution conflictResolution) + { + _conflictResolution = conflictResolution; + return this; + } + + public MoveOperationBuilder CompareSize() + { + _comparedProperties |= FileCompareProperties.Size; + return this; + } + + public MoveOperationBuilder CompareModifiedTime(TimeSpan? allowedTimeDiff = null) + { + _comparedProperties |= FileCompareProperties.ModifiedTime; + _allowedTimeDiff = allowedTimeDiff; + return this; + } + + public MoveOperationBuilder CompareAttributes(FileAttributes? attributes = null) + { + _comparedProperties |= FileCompareProperties.Attributes; + _comparedAttributes = attributes; + return this; + } + + public MoveOperationBuilder CompareContent() + { + _comparedProperties |= FileCompareProperties.Content; + return this; + } + + internal MoveOperationBuilder CompareFile(Func comparer) + { + _compareFile = comparer; + return this; + } + + internal MoveOperationBuilder CompareDirectory(Func comparer) + { + _compareDirectory = comparer; + return this; + } + + public MoveOperationBuilder Flat() + { + _flat = true; + return this; + } + + public MoveOperationBuilder DryRun() + { + _dryRun = true; + return this; + } + + public MoveOperationBuilder LogOperation(Action logOperation) + { + _logOperation = logOperation; + return this; + } + + public MoveOperationBuilder WithDialogProvider(IDialogProvider dialogProvider) + { + _dialogProvider = dialogProvider; + return this; + } + + internal CopyOptions CreateOptions() + { + return new CopyOptions() + { + ConflictResolution = _conflictResolution, + ComparedProperties = _comparedProperties, + ComparedAttributes = _comparedAttributes, + AllowedTimeDiff = _allowedTimeDiff, + Flat = _flat, + DryRun = _dryRun, + LogOperation = _logOperation, + DialogProvider = _dialogProvider, + CompareFile = _compareFile, + CompareDirectory = _compareDirectory, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/RenameOperation.cs b/src/FileSystem/FileSystem/Fluent/RenameOperation.cs new file mode 100644 index 00000000..6b18e7b6 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/RenameOperation.cs @@ -0,0 +1,77 @@ +// Copyright (c) Josef Pihrt. 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 System.Text.RegularExpressions; +using System.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class RenameOperation +{ + internal RenameOperation(Search search, RenameOptions options) + { + Search = search ?? throw new ArgumentNullException(nameof(search)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Search Search { get; } + + public RenameOptions Options { get; } + + public IOperationResult Run(string directoryPath, string? replacement = null, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + return Search.Rename(directoryPath, replacement, Options, cancellationToken); + } + + public IOperationResult Run(string directoryPath, MatchEvaluator matchEvaluator, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + return Search.Rename(directoryPath, matchEvaluator, Options, cancellationToken); + } + + public IOperationResult Run( + IEnumerable directoryPaths, + string? replacement = null, + CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + var telemetry = new SearchTelemetry(); + + foreach (string directoryPath in directoryPaths) + { + IOperationResult result = Search.Rename(directoryPath, replacement, Options, cancellationToken); + telemetry.Add(result.Telemetry); + } + + return new OperationResult(telemetry); + } + + public IOperationResult Run( + IEnumerable directoryPaths, + MatchEvaluator matchEvaluator, + CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + var telemetry = new SearchTelemetry(); + + foreach (string directoryPath in directoryPaths) + { + IOperationResult result = Search.Rename(directoryPath, matchEvaluator, Options, cancellationToken); + telemetry.Add(result.Telemetry); + } + + return new OperationResult(telemetry); + } +} diff --git a/src/FileSystem/FileSystem/Fluent/RenameOperationBuilder.cs b/src/FileSystem/FileSystem/Fluent/RenameOperationBuilder.cs new file mode 100644 index 00000000..039f9031 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/RenameOperationBuilder.cs @@ -0,0 +1,70 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class RenameOperationBuilder +{ + private ReplaceFunctions _functions; + private bool _cultureInvariant; + private bool _dryRun; + private Action? _logOperation; + private IDialogProvider? _dialogProvider; + private ConflictResolution _conflictResolution = ConflictResolution.Skip; + + internal RenameOperationBuilder() + { + } + + internal RenameOperationBuilder WithFunctions(ReplaceFunctions functions) + { + _functions = functions; + return this; + } + + public RenameOperationBuilder CultureInvariant() + { + _cultureInvariant = true; + return this; + } + + public RenameOperationBuilder DryRun() + { + _dryRun = true; + return this; + } + + public RenameOperationBuilder LogOperation(Action logOperation) + { + _logOperation = logOperation; + return this; + } + + public RenameOperationBuilder WithDialogProvider(IDialogProvider dialogProvider) + { + _dialogProvider = dialogProvider; + return this; + } + + public RenameOperationBuilder WithConflictResolution(ConflictResolution conflictResolution) + { + _conflictResolution = conflictResolution; + return this; + } + + internal RenameOptions CreateOptions() + { + return new RenameOptions() + { + ReplaceFunctions = _functions, + CultureInvariant = _cultureInvariant, + DryRun = _dryRun, + LogOperation = _logOperation, + DialogProvider = _dialogProvider, + ConflictResolution = _conflictResolution, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/ReplaceOperation.cs b/src/FileSystem/FileSystem/Fluent/ReplaceOperation.cs new file mode 100644 index 00000000..96409580 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/ReplaceOperation.cs @@ -0,0 +1,83 @@ +// Copyright (c) Josef Pihrt. 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 System.Text.RegularExpressions; +using System.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class ReplaceOperation +{ + internal ReplaceOperation(Search search, ReplaceOptions options) + { + Search = search ?? throw new ArgumentNullException(nameof(search)); + Options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public Search Search { get; } + + public ReplaceOptions Options { get; } + + public IOperationResult Run(string directoryPath, string? replacement = null, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + return Search.Replace(directoryPath, replacement, Options, cancellationToken); + } + + public IOperationResult Run(string directoryPath, MatchEvaluator matchEvaluator, CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (matchEvaluator is null) + throw new ArgumentNullException(nameof(matchEvaluator)); + + return Search.Replace(directoryPath, matchEvaluator, Options, cancellationToken); + } + + public IOperationResult Run( + IEnumerable directoryPaths, + string? replacement = null, + CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + var telemetry = new SearchTelemetry(); + + foreach (string directoryPath in directoryPaths) + { + IOperationResult result = Search.Replace(directoryPath, replacement, Options, cancellationToken); + telemetry.Add(result.Telemetry); + } + + return new OperationResult(telemetry); + } + + public IOperationResult Run( + IEnumerable directoryPaths, + MatchEvaluator matchEvaluator, + CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + if (matchEvaluator is null) + throw new ArgumentNullException(nameof(matchEvaluator)); + + var telemetry = new SearchTelemetry(); + + foreach (string directoryPath in directoryPaths) + { + IOperationResult result = Search.Replace(directoryPath, matchEvaluator, Options, cancellationToken); + telemetry.Add(result.Telemetry); + } + + return new OperationResult(telemetry); + } +} diff --git a/src/FileSystem/FileSystem/Fluent/ReplaceOperationBuilder.cs b/src/FileSystem/FileSystem/Fluent/ReplaceOperationBuilder.cs new file mode 100644 index 00000000..7aafd1f8 --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/ReplaceOperationBuilder.cs @@ -0,0 +1,54 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class ReplaceOperationBuilder +{ + private ReplaceFunctions _functions; + private bool _cultureInvariant; + private bool _dryRun; + private Action? _logOperation; + + internal ReplaceOperationBuilder() + { + } + + internal ReplaceOperationBuilder WithFunctions(ReplaceFunctions functions) + { + _functions = functions; + return this; + } + + public ReplaceOperationBuilder CultureInvariant() + { + _cultureInvariant = true; + return this; + } + + public ReplaceOperationBuilder DryRun() + { + _dryRun = true; + return this; + } + + public ReplaceOperationBuilder LogOperation(Action logOperation) + { + _logOperation = logOperation; + return this; + } + + internal ReplaceOptions CreateOptions() + { + return new ReplaceOptions() + { + ReplaceFunctions = _functions, + CultureInvariant = _cultureInvariant, + DryRun = _dryRun, + LogOperation = _logOperation, + }; + } +} diff --git a/src/FileSystem/FileSystem/Fluent/SearchBuilder.cs b/src/FileSystem/FileSystem/Fluent/SearchBuilder.cs new file mode 100644 index 00000000..3d7a47af --- /dev/null +++ b/src/FileSystem/FileSystem/Fluent/SearchBuilder.cs @@ -0,0 +1,262 @@ +// Copyright (c) Josef Pihrt. 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 System.Text; +using System.Text.RegularExpressions; +using System.Threading; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem.Fluent; + +public class SearchBuilder +{ + private FileMatcher? _file; + private DirectoryMatcher? _directory; + private DirectoryMatcher? _searchDirectory; + + private bool _topDirectoryOnly; + private Action? _logProgress; + private Encoding? _defaultEncoding; + + public SearchBuilder MatchFile(Action configure) + { + var builder = new FileMatcherBuilder(); + + configure(builder); + + _file = builder.Build(); + + return this; + } + + public SearchBuilder MatchDirectory(Action configure) + { + var builder = new DirectoryMatcherBuilder(); + + configure(builder); + + _directory = builder.Build(); + + return this; + } + + public SearchBuilder SearchDirectory(Action configure) + { + var builder = new DirectoryMatcherBuilder(); + + configure(builder); + + _searchDirectory = builder.Build(); + + return this; + } + + public SearchBuilder SkipDirectory( + string pattern, + RegexOptions options = RegexOptions.None, + object? group = null, + Func? predicate = null) + { + _searchDirectory = new DirectoryMatcher() + { + Name = new Matcher(pattern, options, invert: true, group, predicate) + }; + + return this; + } + + public SearchBuilder FileExtension( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + if (_file is null) + _file = new FileMatcher(); + + _file.Extension = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public SearchBuilder FileName( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + if (_file is null) + _file = new FileMatcher(); + + _file.Name = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public SearchBuilder FileContent( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + if (_file is null) + _file = new FileMatcher(); + + _file.Name = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public SearchBuilder DirectoryName( + string pattern, + RegexOptions options = RegexOptions.None, + bool invert = false, + object? group = null, + Func? predicate = null) + { + if (_directory is null) + _directory = new DirectoryMatcher(); + + _directory.Name = new Matcher(pattern, options, invert, group, predicate); + + return this; + } + + public SearchBuilder TopDirectoryOnly() + { + _topDirectoryOnly = true; + + return this; + } + + public SearchBuilder LogProgress(Action logProgress) + { + _logProgress = logProgress; + + return this; + } + + public SearchBuilder WithDefaultEncoding(Encoding encoding) + { + _defaultEncoding = encoding; + + return this; + } + + public Search ToSearch() + { + var options = new SearchOptions() + { + SearchDirectory = _searchDirectory, + LogProgress = _logProgress, + TopDirectoryOnly = _topDirectoryOnly, + DefaultEncoding = _defaultEncoding, + }; + + FileMatcher? fileMatcher = _file; + DirectoryMatcher? directoryMatcher = _directory; + + if (fileMatcher is not null) + { + return (directoryMatcher is not null) + ? new Search(fileMatcher, directoryMatcher, options) + : new Search(fileMatcher, options); + } + else if (directoryMatcher is not null) + { + return new Search(directoryMatcher, options); + } + else + { + throw new InvalidOperationException("File matcher or directory matcher must be specified."); + } + } + + public IEnumerable Matches( + string directoryPath, + CancellationToken cancellationToken = default) + { + return ToSearch().Matches(directoryPath, cancellationToken); + } + + public IEnumerable Matches( + IEnumerable directoryPaths, + CancellationToken cancellationToken = default) + { + if (directoryPaths is null) + throw new ArgumentNullException(nameof(directoryPaths)); + + return Matches(); + + IEnumerable Matches() + { + Search search = ToSearch(); + + foreach (string directoryPath in directoryPaths) + { + foreach (FileMatch fileMatch in search.Matches(directoryPath, cancellationToken)) + yield return fileMatch; + } + } + } + + public DeleteOperation Delete(Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var deleteBuilder = new DeleteOperationBuilder(); + configure(deleteBuilder); + + return new DeleteOperation(ToSearch(), deleteBuilder.CreateOptions()); + } + + public CopyOperation Copy(Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var copyBuilder = new CopyOperationBuilder(); + configure(copyBuilder); + + return new CopyOperation(ToSearch(), copyBuilder.CreateOptions()); + } + + public MoveOperation Move(Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var moveBuilder = new MoveOperationBuilder(); + configure(moveBuilder); + + return new MoveOperation(ToSearch(), moveBuilder.CreateOptions()); + } + + public RenameOperation Rename(Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var renameBuilder = new RenameOperationBuilder(); + configure(renameBuilder); + + return new RenameOperation(ToSearch(), renameBuilder.CreateOptions()); + } + + public ReplaceOperation Replace(Action configure) + { + if (configure is null) + throw new ArgumentNullException(nameof(configure)); + + var replaceBuilder = new ReplaceOperationBuilder(); + configure(replaceBuilder); + + return new ReplaceOperation(ToSearch(), replaceBuilder.CreateOptions()); + } +} diff --git a/src/FileSystem/FileSystem/IOperationResult.cs b/src/FileSystem/FileSystem/IOperationResult.cs new file mode 100644 index 00000000..abb16662 --- /dev/null +++ b/src/FileSystem/FileSystem/IOperationResult.cs @@ -0,0 +1,8 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Orang.FileSystem; + +public interface IOperationResult +{ + SearchTelemetry Telemetry { get; } +} diff --git a/src/FileSystem/FileSystem/NameFilter.cs b/src/FileSystem/FileSystem/NameFilter.cs deleted file mode 100644 index 40dee0f4..00000000 --- a/src/FileSystem/FileSystem/NameFilter.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright (c) Josef Pihrt. 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.Diagnostics; -using System.Runtime.InteropServices; -using System.Text.RegularExpressions; -using Orang.Text.RegularExpressions; - -namespace Orang.FileSystem; - -//TODO: NameFilter : Filter -[DebuggerDisplay("{DebuggerDisplay,nq}")] -public class NameFilter -{ - public NameFilter( - Filter name, - FileNamePart part = FileNamePart.Name) - { - Name = name ?? throw new ArgumentNullException(nameof(name)); - Part = part; - } - - public Filter Name { get; } - - public FileNamePart Part { get; } - - public bool IsNegative => Name.IsNegative; - - [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => $"{Part} {Name}"; - - internal static NameFilter CreateFromDirectoryPath(string path, bool isNegative = false) - { - string pattern = RegexEscape.Escape(path); - - var options = RegexOptions.ExplicitCapture; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - pattern = Regex.Replace(pattern, @"(/|\\\\)", @"(/|\\)", options); - - pattern = $@"\A{pattern}(?=(/"; - - if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) - { - pattern += @"|\\)"; - } - else - { - pattern += ")"; - } - - pattern += @"|\z)"; - - if (!FileSystemHelpers.IsCaseSensitive) - options |= RegexOptions.IgnoreCase; - - var filter = new Filter(pattern, options, isNegative: isNegative); - - return new NameFilter(filter, FileNamePart.FullName); - } -} diff --git a/src/FileSystem/FileSystem/OperationKind.cs b/src/FileSystem/FileSystem/OperationKind.cs index e589334b..cbb8a163 100644 --- a/src/FileSystem/FileSystem/OperationKind.cs +++ b/src/FileSystem/FileSystem/OperationKind.cs @@ -2,11 +2,12 @@ namespace Orang.FileSystem; -public enum OperationKind : byte +internal enum OperationKind : byte { - Replace = 0, - Rename = 1, - Delete = 2, - Copy = 3, - Move = 4, + None, + Copy, + Delete, + Move, + Rename, + Replace, } diff --git a/src/FileSystem/FileSystem/OperationProgress.cs b/src/FileSystem/FileSystem/OperationProgress.cs index 13907c82..0ba2ddea 100644 --- a/src/FileSystem/FileSystem/OperationProgress.cs +++ b/src/FileSystem/FileSystem/OperationProgress.cs @@ -8,27 +8,29 @@ namespace Orang.FileSystem; [DebuggerDisplay("{DebuggerDisplay,nq}")] public readonly struct OperationProgress { - internal OperationProgress(FileMatch fileMatch, OperationKind kind, Exception? exception = null) - : this(fileMatch, null, kind, exception) + internal OperationProgress(FileMatch match, OperationKind kind, Exception? exception = null) + : this(match, null, kind, exception) { } - internal OperationProgress(FileMatch fileMatch, string? newPath, OperationKind kind, Exception? exception = null) + internal OperationProgress(FileMatch match, string? newPath, OperationKind kind, Exception? exception = null) { - FileMatch = fileMatch; + Match = match; NewPath = newPath; Kind = kind; Exception = exception; } - public FileMatch FileMatch { get; } + public FileMatch Match { get; } + + public string Path => Match.Path; public string? NewPath { get; } - public OperationKind Kind { get; } + internal OperationKind Kind { get; } public Exception? Exception { get; } [DebuggerBrowsable(DebuggerBrowsableState.Never)] - private string DebuggerDisplay => (FileMatch is not null) ? $"{Kind} {FileMatch.Path}" : "Uninitialized"; + private string DebuggerDisplay => (Match is not null) ? $"{Kind} {Match.Path}" : "Uninitialized"; } diff --git a/src/FileSystem/FileSystem/OperationResult.cs b/src/FileSystem/FileSystem/OperationResult.cs new file mode 100644 index 00000000..51eeff5f --- /dev/null +++ b/src/FileSystem/FileSystem/OperationResult.cs @@ -0,0 +1,13 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Orang.FileSystem; + +internal class OperationResult : IOperationResult +{ + public OperationResult(SearchTelemetry telemetry) + { + Telemetry = telemetry; + } + + public SearchTelemetry Telemetry { get; } +} diff --git a/src/FileSystem/FileSystem/RenameOptions.cs b/src/FileSystem/FileSystem/RenameOptions.cs index 8d88baab..21f51a27 100644 --- a/src/FileSystem/FileSystem/RenameOptions.cs +++ b/src/FileSystem/FileSystem/RenameOptions.cs @@ -1,30 +1,22 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System.Text.RegularExpressions; +using System; namespace Orang.FileSystem; -public class RenameOptions : ReplaceOptions +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +public class RenameOptions { - public RenameOptions( - string replacement, - ReplaceFunctions functions = ReplaceFunctions.None, - bool cultureInvariant = false, - ConflictResolution conflictResolution = ConflictResolution.Skip) - : base(replacement, functions, cultureInvariant) - { - ConflictResolution = conflictResolution; - } - - public RenameOptions( - MatchEvaluator matchEvaluator, - ReplaceFunctions functions = ReplaceFunctions.None, - bool cultureInvariant = false, - ConflictResolution conflictResolution = ConflictResolution.Skip) - : base(matchEvaluator, functions, cultureInvariant) - { - ConflictResolution = conflictResolution; - } - - public ConflictResolution ConflictResolution { get; } + internal ReplaceFunctions ReplaceFunctions { get; set; } + + public bool CultureInvariant { get; set; } + + public bool DryRun { get; set; } + + public Action? LogOperation { get; set; } + + public IDialogProvider? DialogProvider { get; set; } + + public ConflictResolution ConflictResolution { get; set; } = ConflictResolution.Skip; } diff --git a/src/FileSystem/FileSystem/ReplaceHelpers.cs b/src/FileSystem/FileSystem/ReplaceHelpers.cs deleted file mode 100644 index f2bb5617..00000000 --- a/src/FileSystem/FileSystem/ReplaceHelpers.cs +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (c) Josef Pihrt. 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 System.Text; -using System.Text.RegularExpressions; -using System.Threading; -using Orang.Text.RegularExpressions; - -namespace Orang.FileSystem; - -internal static class ReplaceHelpers -{ - public static List GetReplaceItems( - Match match, - ReplaceOptions replaceOptions, - CancellationToken cancellationToken = default) - { - List matches = GetMatches(match); - - int offset = 0; - List replaceItems = ListCache.GetInstance(); - - if (replaceItems.Capacity < matches.Count) - replaceItems.Capacity = matches.Count; - - foreach (Match match2 in matches) - { - string value = replaceOptions.Replace(match2); - - replaceItems.Add(new ReplaceItem(match2, value, match2.Index + offset)); - - offset += value.Length - match2.Length; - - cancellationToken.ThrowIfCancellationRequested(); - } - - ListCache.Free(matches); - - return replaceItems; - - static List GetMatches(Match match) - { - List matches = ListCache.GetInstance(); - - do - { - matches.Add(match); - - match = match.NextMatch(); - } - while (match.Success); - - if (matches.Count > 1 - && matches[0].Index > matches[1].Index) - { - matches.Reverse(); - } - - return matches; - } - } - - public static (List replaceItems, MaxReason maxReason) GetReplaceItems( - Match match, - ReplaceOptions replaceOptions, - int count = 0, - Func? predicate = null, - CancellationToken cancellationToken = default) - { - List captures = ListCache.GetInstance(); - - MaxReason maxReason = CaptureFactory.GetCaptures( - ref captures, - match, - count: count, - predicate: predicate, - cancellationToken: cancellationToken); - - int offset = 0; - List replaceItems = ListCache.GetInstance(); - - if (replaceItems.Capacity < captures.Count) - replaceItems.Capacity = captures.Count; - - foreach (Match match2 in captures) - { - string value = replaceOptions.Replace(match2); - - replaceItems.Add(new ReplaceItem(match2, value, match2.Index + offset)); - - offset += value.Length - match2.Length; - - cancellationToken.ThrowIfCancellationRequested(); - } - - ListCache.Free(captures); - - return (replaceItems, maxReason); - } - - public static string GetNewName(FileMatch fileMatch, List replaceItems) - { - StringBuilder sb = StringBuilderCache.GetInstance(); - - string path = fileMatch.Path; - FileNameSpan span = fileMatch.NameSpan; - - int lastPos = span.Start; - - foreach (ReplaceItem item in replaceItems) - { - Match match = item.Match; - - sb.Append(path, lastPos, span.Start + match.Index - lastPos); - sb.Append(item.Value); - - lastPos = span.Start + match.Index + match.Length; - } - - sb.Append(path, lastPos, path.Length - lastPos); - - return StringBuilderCache.GetStringAndFree(sb); - } -} diff --git a/src/FileSystem/FileSystem/ReplaceOptions.cs b/src/FileSystem/FileSystem/ReplaceOptions.cs new file mode 100644 index 00000000..6d8cd71c --- /dev/null +++ b/src/FileSystem/FileSystem/ReplaceOptions.cs @@ -0,0 +1,18 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem; + +public class ReplaceOptions +{ + internal ReplaceFunctions ReplaceFunctions { get; set; } + + public bool CultureInvariant { get; set; } + + public bool DryRun { get; set; } + + public Action? LogOperation { get; set; } +} diff --git a/src/FileSystem/FileSystem/ReplaceResult.cs b/src/FileSystem/FileSystem/ReplaceResult.cs new file mode 100644 index 00000000..da277869 --- /dev/null +++ b/src/FileSystem/FileSystem/ReplaceResult.cs @@ -0,0 +1,24 @@ +// Copyright (c) Josef Pihrt. 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 Orang.Text.RegularExpressions; + +namespace Orang.FileSystem; + +internal class ReplaceResult +{ + public ReplaceResult(List items, MaxReason maxReason, string newName) + { + Items = items; + MaxReason = maxReason; + NewName = newName; + } + + public List Items { get; } + + public MaxReason MaxReason { get; } + + public string NewName { get; } + + public int Count => Items.Count; +} diff --git a/src/FileSystem/FileSystem/Search.cs b/src/FileSystem/FileSystem/Search.cs new file mode 100644 index 00000000..ba858fd7 --- /dev/null +++ b/src/FileSystem/FileSystem/Search.cs @@ -0,0 +1,356 @@ +// Copyright (c) Josef Pihrt. 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 System.IO; +using System.Text.RegularExpressions; +using System.Threading; +using Orang.FileSystem.Commands; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem; + +public class Search +{ + public Search( + FileMatcher fileMatcher, + SearchOptions? options = null) + { + FileMatcher = fileMatcher ?? throw new ArgumentNullException(nameof(fileMatcher)); + Options = options ?? new SearchOptions(); + } + + public Search( + DirectoryMatcher directoryMatcher, + SearchOptions? options = null) + { + DirectoryMatcher = directoryMatcher ?? throw new ArgumentNullException(nameof(directoryMatcher)); + Options = options ?? new SearchOptions(); + } + + public Search( + FileMatcher fileMatcher, + DirectoryMatcher directoryMatcher, + SearchOptions? options = null) + { + FileMatcher = fileMatcher ?? throw new ArgumentNullException(nameof(fileMatcher)); + DirectoryMatcher = directoryMatcher ?? throw new ArgumentNullException(nameof(directoryMatcher)); + Options = options ?? new SearchOptions(); + } + + public FileMatcher? FileMatcher { get; } + + public DirectoryMatcher? DirectoryMatcher { get; } + + public SearchOptions Options { get; } + + public IEnumerable Matches( + string directoryPath, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + var state = new SearchState(FileMatcher, DirectoryMatcher) + { + IncludeDirectory = Options.IncludeDirectoryPredicate, + ExcludeDirectory = Options.ExcludeDirectoryPredicate, + LogProgress = Options.LogProgress, + RecurseSubdirectories = !Options.TopDirectoryOnly, + DefaultEncoding = Options.DefaultEncoding, + IgnoreInaccessible = Options.IgnoreInaccessible, + }; + + return state.Find(directoryPath, cancellationToken); + } + + public IOperationResult Copy( + string directoryPath, + string destinationPath, + CopyOptions? options = null, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (destinationPath is null) + throw new ArgumentNullException(nameof(destinationPath)); + + if (DirectoryMatcher is not null) + throw new InvalidOperationException("Directory matcher cannot be specified when executing 'copy' operation."); + + options ??= new CopyOptions(); + + VerifyCopyMoveArguments(directoryPath, destinationPath, options); + + var command = new CopyCommand() + { + DestinationPath = destinationPath, + CopyOptions = options, + MaxMatchingFiles = 0, + MaxMatchesInFile = 0, + MaxTotalMatches = 0, + ConflictResolution = options.ConflictResolution, + CancellationToken = cancellationToken, + }; + + command.Execute(directoryPath, this, cancellationToken); + + return new OperationResult(command.Telemetry); + } + + public IOperationResult Delete( + string directoryPath, + DeleteOptions? options = null, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + options ??= new DeleteOptions(); + + var command = new DeleteCommand() + { + DeleteOptions = options, + LogOperation = options.LogOperation, + DryRun = options.DryRun, + MaxMatchingFiles = 0, + CancellationToken = cancellationToken, + }; + + command.Execute(directoryPath, this, cancellationToken); + + return new OperationResult(command.Telemetry); + } + + public IOperationResult Move( + string directoryPath, + string destinationPath, + CopyOptions? options = null, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (destinationPath is null) + throw new ArgumentNullException(nameof(destinationPath)); + + if (DirectoryMatcher is not null) + throw new InvalidOperationException("Directory matcher cannot be specified when executing 'move' operation."); + + options ??= new CopyOptions(); + + VerifyCopyMoveArguments(directoryPath, destinationPath, options); + + var command = new MoveCommand() + { + DestinationPath = destinationPath, + CopyOptions = options, + LogOperation = options.LogOperation, + DryRun = options.DryRun, + DialogProvider = options.DialogProvider, + MaxMatchingFiles = 0, + MaxMatchesInFile = 0, + MaxTotalMatches = 0, + ConflictResolution = options.ConflictResolution, + CancellationToken = cancellationToken, + }; + + command.Execute(directoryPath, this, cancellationToken); + + return new OperationResult(command.Telemetry); + } + + public IOperationResult Rename( + string directoryPath, + string? replacement = null, + RenameOptions? options = null, + CancellationToken cancellationToken = default) + { + options ??= new RenameOptions(); + + var replacer = new Replacer(replacement, options?.ReplaceFunctions ?? ReplaceFunctions.None, options?.CultureInvariant ?? false); + + return Rename(directoryPath, replacer, options, cancellationToken); + } + + public IOperationResult Rename( + string directoryPath, + MatchEvaluator matchEvaluator, + RenameOptions? options = null, + CancellationToken cancellationToken = default) + { + options ??= new RenameOptions(); + + var replacer = new Replacer(matchEvaluator, options?.ReplaceFunctions ?? ReplaceFunctions.None, options?.CultureInvariant ?? false); + + return Rename(directoryPath, replacer, options, cancellationToken); + } + + private IOperationResult Rename( + string directoryPath, + Replacer replacer, + RenameOptions? options = null, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + options ??= new RenameOptions(); + + FileMatcher? fileMatcher = FileMatcher; + + if (fileMatcher is not null) + { + if (fileMatcher.NamePart == FileNamePart.FullName) + throw new InvalidOperationException($"Invalid file name part '{nameof(FileNamePart.FullName)}'."); + + if (fileMatcher.Name is null) + throw new InvalidOperationException("Name matcher is not defined."); + + if (fileMatcher.Name.Invert) + throw new InvalidOperationException("Name matcher cannot be negative."); + } + + DirectoryMatcher? directoryMatcher = DirectoryMatcher; + + if (directoryMatcher is not null) + { + if (directoryMatcher.NamePart == FileNamePart.FullName + || directoryMatcher.NamePart == FileNamePart.Extension) + { + throw new InvalidOperationException($"Invalid file name part '{directoryMatcher.NamePart}'."); + } + + if (directoryMatcher.Name is null) + throw new InvalidOperationException("Name matcher is not defined."); + + if (directoryMatcher.Name.Invert) + throw new InvalidOperationException("Name matcher cannot be negative."); + } + + VerifyConflictResolution(options.ConflictResolution, options.DialogProvider); + + var command = new RenameCommand() + { + FileMatcher = FileMatcher?.Name, + DirectoryMatcher = DirectoryMatcher?.Name, + RenameOptions = options, + Replacer = replacer, + LogOperation = options.LogOperation, + DryRun = options.DryRun, + DialogProvider = options.DialogProvider, + MaxMatchingFiles = 0, + ConflictResolution = options.ConflictResolution, + CancellationToken = cancellationToken, + }; + + command.Execute(directoryPath, this, cancellationToken); + + return new OperationResult(command.Telemetry); + } + + public IOperationResult Replace( + string directoryPath, + string? replacement = null, + ReplaceOptions? options = null, + CancellationToken cancellationToken = default) + { + var replacer = new Replacer(replacement, options?.ReplaceFunctions ?? ReplaceFunctions.None, options?.CultureInvariant ?? false); + + return Replace(directoryPath, replacer, options, cancellationToken); + } + + public IOperationResult Replace( + string directoryPath, + MatchEvaluator matchEvaluator, + ReplaceOptions? options = null, + CancellationToken cancellationToken = default) + { + var replacer = new Replacer(matchEvaluator, options?.ReplaceFunctions ?? ReplaceFunctions.None, options?.CultureInvariant ?? false); + + return Replace(directoryPath, replacer, options, cancellationToken); + } + + private IOperationResult Replace( + string directoryPath, + Replacer replacer, + ReplaceOptions? options = null, + CancellationToken cancellationToken = default) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (FileMatcher is null) + throw new InvalidOperationException($"'{nameof(FileMatcher)}' is null."); + + FileMatcher matcher = FileMatcher; + + if (matcher.Content is null) + throw new InvalidOperationException("Content matcher is not defined."); + + if (matcher.Content.Invert) + throw new InvalidOperationException("Content matcher cannot be negative."); + + options ??= new ReplaceOptions(); + + var command = new ReplaceCommand() + { + ContentFilter = matcher.Content, + ReplaceOptions = options, + Replacer = replacer, + LogOperation = options.LogOperation, + DryRun = options.DryRun, + MaxMatchingFiles = 0, + MaxMatchesInFile = 0, + MaxTotalMatches = 0, + CancellationToken = cancellationToken, + }; + + command.Execute(directoryPath, this, cancellationToken); + + return new OperationResult(command.Telemetry); + } + + private static void VerifyConflictResolution( + ConflictResolution conflictResolution, + IDialogProvider? dialogProvider) + { + if (conflictResolution == ConflictResolution.Ask + && dialogProvider is null) + { + throw new ArgumentNullException( + nameof(dialogProvider), + $"'{nameof(dialogProvider)}' cannot be null when {nameof(ConflictResolution)} " + + $"is set to {nameof(ConflictResolution.Ask)}."); + } + } + + private static void VerifyCopyMoveArguments( + string directoryPath, + string destinationPath, + CopyOptions copyOptions) + { + if (directoryPath is null) + throw new ArgumentNullException(nameof(directoryPath)); + + if (destinationPath is null) + throw new ArgumentNullException(nameof(destinationPath)); + + if (!Directory.Exists(directoryPath)) + throw new DirectoryNotFoundException($"Directory not found: {directoryPath}"); + + if (!Directory.Exists(destinationPath)) + throw new DirectoryNotFoundException($"Directory not found: {destinationPath}"); + + if (FileSystemUtilities.IsSubdirectory(destinationPath, directoryPath)) + { + throw new ArgumentException( + "Source directory cannot be subdirectory of a destination directory.", + nameof(directoryPath)); + } + + VerifyConflictResolution(copyOptions.ConflictResolution, copyOptions.DialogProvider); + } +} diff --git a/src/FileSystem/FileSystem/SearchOptions.cs b/src/FileSystem/FileSystem/SearchOptions.cs new file mode 100644 index 00000000..99ae5fc6 --- /dev/null +++ b/src/FileSystem/FileSystem/SearchOptions.cs @@ -0,0 +1,49 @@ +// Copyright (c) Josef Pihrt. 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.Text; + +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +namespace Orang.FileSystem; + +public class SearchOptions +{ + public DirectoryMatcher? SearchDirectory { get; set; } + + internal Func? IncludeDirectoryPredicate + { + get + { + if (SearchDirectory is not null + && SearchDirectory.Name?.Invert != true) + { + return path => SearchDirectory.IsMatch(path); + } + + return null; + } + } + + internal Func? ExcludeDirectoryPredicate + { + get + { + if (SearchDirectory is not null + && SearchDirectory.Name?.Invert != false) + { + return path => !SearchDirectory.IsMatch(path); + } + + return null; + } + } + + public Action? LogProgress { get; set; } + + public Encoding? DefaultEncoding { get; set; } + + public bool TopDirectoryOnly { get; set; } + + internal bool IgnoreInaccessible { get; set; } = true; +} diff --git a/src/FileSystem/FileSystem/SearchState.cs b/src/FileSystem/FileSystem/SearchState.cs new file mode 100644 index 00000000..d1b5a524 --- /dev/null +++ b/src/FileSystem/FileSystem/SearchState.cs @@ -0,0 +1,317 @@ +// Copyright (c) Josef Pihrt. 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 System.Diagnostics; +using System.IO; +using System.Security; +using System.Text; +using System.Threading; +using static Orang.FileSystem.FileSystemUtilities; + +#pragma warning disable RCS1223 + +namespace Orang.FileSystem; + +internal class SearchState +{ + public SearchState(FileMatcher? matcher, DirectoryMatcher? directoryMatcher) + { + Debug.Assert(matcher is not null || directoryMatcher is not null); + + FileMatcher = matcher; + DirectoryMatcher = directoryMatcher; + } + + public FileMatcher? FileMatcher { get; } + + public DirectoryMatcher? DirectoryMatcher { get; } + + public Func? IncludeDirectory { get; set; } + + public Func? ExcludeDirectory { get; set; } + + public Action? LogProgress { get; set; } + + public bool SupportsEnumeration { get; set; } = true; + + public bool CanRecurseMatch { get; set; } = true; + + public bool RecurseSubdirectories { get; set; } = true; + + public bool IgnoreInaccessible { get; set; } = true; + + public Encoding? DefaultEncoding { get; set; } + + public SearchTelemetry? Telemetry { get; set; } + + public IEnumerable Find( + string directoryPath, + CancellationToken cancellationToken = default) + { + return Find(directoryPath, default(INotifyDirectoryChanged), cancellationToken); + } + + internal IEnumerable Find( + string directoryPath, + INotifyDirectoryChanged? notifyDirectoryChanged = null, + CancellationToken cancellationToken = default) + { + var enumerationOptions = new EnumerationOptions() + { + AttributesToSkip = FileMatcher?.WithoutAttributes ?? 0, + IgnoreInaccessible = IgnoreInaccessible, + MatchCasing = MatchCasing.PlatformDefault, + MatchType = MatchType.Simple, + RecurseSubdirectories = false, + ReturnSpecialDirectories = false + }; + + var directories = new Queue(); + Queue? subdirectories = (RecurseSubdirectories) ? new Queue() : null; + + string? currentDirectory = null; + + if (notifyDirectoryChanged is not null) + { + notifyDirectoryChanged.DirectoryChanged + += (object sender, DirectoryChangedEventArgs e) => currentDirectory = e.NewName; + } + + MatchStatus matchStatus = (IncludeDirectory is not null || ExcludeDirectory is not null) + ? MatchStatus.Unknown + : MatchStatus.Success; + + if (IncludeDirectory is not null) + { + matchStatus = (IncludeDirectory(directoryPath)) + ? MatchStatus.Success + : MatchStatus.FailFromPositive; + } + + var directory = new Directory(directoryPath, matchStatus); + + while (true) + { + Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true); + + if (FileMatcher is not null + && !directory.IsFail) + { + IEnumerator fi = null!; + + try + { + fi = (SupportsEnumeration) + ? EnumerateFiles(directory.Path, enumerationOptions).GetEnumerator() + : ((IEnumerable)GetFiles(directory.Path, enumerationOptions)).GetEnumerator(); + } + catch (Exception ex) when (IsWellKnownException(ex)) + { + Debug.Fail(ex.ToString()); + Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true, ex); + } + + if (fi is not null) + { + using (fi) + { + while (fi.MoveNext()) + { + FileMatch? match = MatchFile(fi.Current); + + if (match is not null) + yield return match; + + cancellationToken.ThrowIfCancellationRequested(); + } + } + } + } + + IEnumerator di = null!; + + try + { + di = (SupportsEnumeration) + ? EnumerateDirectories(directory.Path, enumerationOptions).GetEnumerator() + : ((IEnumerable)GetDirectories(directory.Path, enumerationOptions)).GetEnumerator(); + } + catch (Exception ex) when (IsWellKnownException(ex)) + { + Debug.Fail(ex.ToString()); + Report(directory.Path, SearchProgressKind.SearchDirectory, isDirectory: true, ex); + } + + if (di is not null) + { + using (di) + { + while (di.MoveNext()) + { + currentDirectory = di.Current; + + matchStatus = (ExcludeDirectory is null && directory.IsSuccess) + ? MatchStatus.Success + : MatchStatus.Unknown; + + if (!directory.IsFail + && DirectoryMatcher is not null + && DirectoryMatcher?.NamePart != FileNamePart.Extension) + { + if (matchStatus == MatchStatus.Unknown + && (IncludeDirectory is not null || ExcludeDirectory is not null)) + { + matchStatus = IncludeOrExcludeDirectory(currentDirectory); + } + + if (matchStatus == MatchStatus.Success + || matchStatus == MatchStatus.Unknown) + { + FileMatch? match = MatchDirectory(currentDirectory); + + if (match is not null) + { + yield return match; + + if (!CanRecurseMatch) + currentDirectory = null; + } + } + } + + if (currentDirectory is not null + && RecurseSubdirectories) + { + if (matchStatus == MatchStatus.Unknown + && (IncludeDirectory is not null || ExcludeDirectory is not null)) + { + matchStatus = IncludeOrExcludeDirectory(currentDirectory); + } + + if (matchStatus != MatchStatus.FailFromNegative) + subdirectories!.Enqueue(new Directory(currentDirectory, matchStatus)); + } + + cancellationToken.ThrowIfCancellationRequested(); + } + } + + if (RecurseSubdirectories) + { + while (subdirectories!.Count > 0) + directories.Enqueue(subdirectories.Dequeue()); + } + } + + if (directories.Count > 0) + { + directory = directories.Dequeue(); + } + else + { + break; + } + } + } + + public FileMatch? MatchFile(string path) + { + (FileMatch? fileMatch, Exception? exception) = FileMatcher!.Match(path, DefaultEncoding); + + Report(path, SearchProgressKind.File, exception: exception); + + return fileMatch; + } + + public FileMatch? MatchDirectory(string path) + { + (FileMatch? fileMatch, Exception? exception) = DirectoryMatcher!.Match(path); + + Report(path, SearchProgressKind.Directory, isDirectory: true, exception); + + return fileMatch; + } + + private MatchStatus IncludeOrExcludeDirectory(string path) + { + Debug.Assert(IncludeDirectory is not null || ExcludeDirectory is not null); + + if (ExcludeDirectory?.Invoke(path) == true) + return MatchStatus.FailFromNegative; + + if (IncludeDirectory?.Invoke(path) == false) + return MatchStatus.FailFromPositive; + + return MatchStatus.Success; + } + + private void Report(string path, SearchProgressKind kind, bool isDirectory = false, Exception? exception = null) + { + LogProgress?.Invoke(new SearchProgress(path, kind, isDirectory: isDirectory, exception)); + + if (Telemetry is not null) + { + switch (kind) + { + case SearchProgressKind.SearchDirectory: + { + Telemetry.SearchedDirectoryCount++; + break; + } + case SearchProgressKind.Directory: + { + Telemetry.DirectoryCount++; + break; + } + case SearchProgressKind.File: + { + Telemetry.FileCount++; + break; + } + default: + { + throw new InvalidOperationException($"Unknown enum value '{kind}'."); + } + } + } + } + + private static bool IsWellKnownException(Exception ex) + { + return ex is IOException + || ex is ArgumentException + || ex is UnauthorizedAccessException + || ex is SecurityException + || ex is NotSupportedException; + } + + [DebuggerDisplay("{DebuggerDisplay,nq}")] + private readonly struct Directory + { + public Directory(string path, MatchStatus status) + { + Path = path; + Status = status; + } + + public string Path { get; } + + public MatchStatus Status { get; } + + public bool IsSuccess => Status == MatchStatus.Success; + + public bool IsFail => Status == MatchStatus.FailFromPositive || Status == MatchStatus.FailFromNegative; + + [DebuggerBrowsable(DebuggerBrowsableState.Never)] + private string DebuggerDisplay => Path; + } + + private enum MatchStatus + { + Unknown = 0, + Success = 1, + FailFromPositive = 2, + FailFromNegative = 3, + } +} diff --git a/src/FileSystem/FileSystem/SearchTarget.cs b/src/FileSystem/FileSystem/SearchTarget.cs index a861b7b5..b1e77767 100644 --- a/src/FileSystem/FileSystem/SearchTarget.cs +++ b/src/FileSystem/FileSystem/SearchTarget.cs @@ -2,7 +2,7 @@ namespace Orang.FileSystem; -public enum SearchTarget +internal enum SearchTarget { Files = 0, Directories = 1, diff --git a/src/FileSystem/FileSystem/SearchTelemetry.cs b/src/FileSystem/FileSystem/SearchTelemetry.cs index 4e620476..6ef5b2ea 100644 --- a/src/FileSystem/FileSystem/SearchTelemetry.cs +++ b/src/FileSystem/FileSystem/SearchTelemetry.cs @@ -1,14 +1,23 @@ // Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +#pragma warning disable RCS1223 // Mark publicly visible type with DebuggerDisplay attribute. + +using System; + namespace Orang.FileSystem; -//TODO: MatchingLineCount -internal class SearchTelemetry +public sealed class SearchTelemetry { internal SearchTelemetry() { } + public int SearchedDirectoryCount { get; internal set; } + + public int FileCount { get; internal set; } + + public int DirectoryCount { get; internal set; } + public int MatchingFileCount { get; internal set; } public int MatchingDirectoryCount { get; internal set; } @@ -46,4 +55,40 @@ internal void IncrementProcessedCount(bool isDirectory) ProcessedFileCount++; } } + + internal int MatchingLineCount { get; set; } + + internal TimeSpan Elapsed { get; set; } + + internal long FilesTotalSize { get; set; } + + internal int UpdatedCount { get; set; } + + internal int AddedCount { get; set; } + + internal int RenamedCount { get; set; } + + internal int DeletedCount { get; set; } + + public SearchTelemetry Add(SearchTelemetry other) + { + SearchedDirectoryCount += other.SearchedDirectoryCount; + FileCount += other.FileCount; + DirectoryCount += other.DirectoryCount; + MatchingFileCount += other.MatchingFileCount; + MatchingDirectoryCount += other.MatchingDirectoryCount; + ProcessedFileCount += other.ProcessedFileCount; + ProcessedDirectoryCount += other.ProcessedDirectoryCount; + MatchCount += other.MatchCount; + ProcessedMatchCount += other.ProcessedMatchCount; + MatchingLineCount += other.MatchingLineCount; + Elapsed += other.Elapsed; + FilesTotalSize += other.FilesTotalSize; + UpdatedCount += other.UpdatedCount; + AddedCount += other.AddedCount; + RenamedCount += other.RenamedCount; + DeletedCount += other.DeletedCount; + + return this; + } } diff --git a/src/FileSystem/FileSystemExtensions.cs b/src/FileSystem/FileSystemExtensions.cs deleted file mode 100644 index 68ce95fc..00000000 --- a/src/FileSystem/FileSystemExtensions.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Josef Pihrt. 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.Text.RegularExpressions; -using Orang.FileSystem; - -namespace Orang; - -internal static class FileSystemExtensions -{ - public static void Report( - this IProgress progress, - string path, - SearchProgressKind kind, - bool isDirectory = false, - Exception? exception = null) - { - progress.Report(new SearchProgress(path, kind, isDirectory, exception)); - } - - public static void Report( - this IProgress progress, - FileMatch fileMatch, - string? newPath, - OperationKind kind, - Exception? exception = null) - { - progress.Report(new OperationProgress(fileMatch, newPath, kind, exception)); - } - - public static Match? Match(this Filter filter, in FileNameSpan name) - { - return filter.Match(filter.Regex.Match(name.Path, name.Start, name.Length)); - } - - public static Match? Match(this Filter filter, in FileNameSpan name, bool matchPartOnly) - { - return (matchPartOnly) - ? filter.Match(name.ToString()) - : filter.Match(name); - } - - public static bool IsMatch(this Filter filter, in FileNameSpan name) - { - return Match(filter, name) is not null; - } -} diff --git a/src/FileSystem/IsExternalInit.cs b/src/FileSystem/IsExternalInit.cs new file mode 100644 index 00000000..8f30c982 --- /dev/null +++ b/src/FileSystem/IsExternalInit.cs @@ -0,0 +1,10 @@ +// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace System.Runtime.CompilerServices; + +using System.ComponentModel; + +[EditorBrowsable(EditorBrowsableState.Never)] +internal static class IsExternalInit +{ +} diff --git a/src/FileSystem/Operations/CommonCopyOperation.cs b/src/FileSystem/Operations/CommonCopyOperation.cs deleted file mode 100644 index c5f09f5d..00000000 --- a/src/FileSystem/Operations/CommonCopyOperation.cs +++ /dev/null @@ -1,193 +0,0 @@ -// Copyright (c) Josef Pihrt. 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.Diagnostics; -using System.IO; -using Orang.FileSystem; - -namespace Orang.Operations; - -internal abstract class CommonCopyOperation : CommonFindOperation -{ - protected CommonCopyOperation() - { - } - - public string DestinationPath { get; set; } = null!; - - public IDialogProvider? DialogProvider { get; set; } - - public CopyOptions CopyOptions { get; set; } = null!; - - public ConflictResolution ConflictResolution { get; set; } - - protected abstract void ExecuteOperation(string sourcePath, string destinationPath); - - protected override void ExecuteMatch( - FileMatch fileMatch, - string directoryPath) - { - string sourcePath = fileMatch.Path; - string destinationPath; - - if (fileMatch.IsDirectory - || (directoryPath is not null && !CopyOptions.Flat)) - { - Debug.Assert(sourcePath.StartsWith(directoryPath, FileSystemHelpers.Comparison)); - - string relativePath = sourcePath.Substring(directoryPath.Length + 1); - - destinationPath = Path.Combine(DestinationPath, relativePath); - } - else - { - string fileName = Path.GetFileName(sourcePath); - - destinationPath = Path.Combine(DestinationPath, fileName); - } - - try - { - ExecuteOperation(fileMatch, destinationPath, fileMatch.IsDirectory); - } - catch (Exception ex) when (ex is IOException - || ex is UnauthorizedAccessException) - { - Report(fileMatch, destinationPath, ex); - } - } - - protected virtual void ExecuteOperation(FileMatch fileMatch, string destinationPath, bool isDirectory) - { - string sourcePath = fileMatch.Path; - bool fileExists = File.Exists(destinationPath); - bool directoryExists = !fileExists && Directory.Exists(destinationPath); - var ask = false; - - if (isDirectory) - { - if (fileExists) - { - ask = true; - } - else if (directoryExists) - { - if (File.GetAttributes(sourcePath) == File.GetAttributes(destinationPath)) - return; - - ask = true; - } - } - else if (fileExists) - { - if (CopyOptions.CompareOptions != FileCompareOptions.None - && FileSystemHelpers.FileEquals(sourcePath, destinationPath, CopyOptions.CompareOptions)) - { - return; - } - - ask = true; - } - else if (directoryExists) - { - ask = true; - } - - if (ask - && ConflictResolution == ConflictResolution.Skip) - { - return; - } - - if (!isDirectory - && fileExists - && ConflictResolution == ConflictResolution.Suffix) - { - destinationPath = FileSystemHelpers.CreateNewFilePath(destinationPath); - } - - if (ask - && ConflictResolution == ConflictResolution.Ask) - { - DialogResult dialogResult = DialogProvider!.GetResult( - new OperationProgress(fileMatch, destinationPath, OperationKind)); - - switch (dialogResult) - { - case DialogResult.Yes: - { - break; - } - case DialogResult.YesToAll: - { - ConflictResolution = ConflictResolution.Overwrite; - break; - } - case DialogResult.No: - case DialogResult.None: - { - return; - } - case DialogResult.NoToAll: - { - ConflictResolution = ConflictResolution.Skip; - return; - } - case DialogResult.Cancel: - { - throw new OperationCanceledException(); - } - default: - { - throw new InvalidOperationException($"Unknown enum value '{dialogResult}'."); - } - } - } - - Report(fileMatch, destinationPath); - - if (isDirectory) - { - if (directoryExists) - { - if (!DryRun) - FileSystemHelpers.UpdateAttributes(sourcePath, destinationPath); - } - else - { - if (fileExists - && !DryRun) - { - File.Delete(destinationPath); - } - - if (!DryRun) - Directory.CreateDirectory(destinationPath); - } - - Telemetry.ProcessedDirectoryCount++; - } - else - { - if (fileExists) - { - if (!DryRun) - File.Delete(destinationPath); - } - else if (directoryExists) - { - if (!DryRun) - Directory.Delete(destinationPath, recursive: true); - } - else if (!DryRun) - { - Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)); - } - - if (!DryRun) - ExecuteOperation(sourcePath, destinationPath); - - Telemetry.ProcessedFileCount++; - } - } -} diff --git a/src/FileSystem/Operations/FileSystemOperation.cs b/src/FileSystem/Operations/FileSystemOperation.cs deleted file mode 100644 index fd5b5b02..00000000 --- a/src/FileSystem/Operations/FileSystemOperation.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) Josef Pihrt. 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 System.IO; -using System.Threading; -using Orang.FileSystem; - -namespace Orang.Operations; - -internal abstract class FileSystemOperation -{ - protected FileSystemOperation() - { - Telemetry = new SearchTelemetry(); - } - - public FileSystemSearch Search { get; set; } = null!; - - public SearchTelemetry Telemetry { get; } - - public TerminationReason TerminationReason { get; set; } - - public int MaxMatchingFiles { get; set; } - - public IProgress? Progress { get; set; } - - public bool DryRun { get; set; } - - public CancellationToken CancellationToken { get; set; } - - protected Filter? ContentFilter => Search.Filter.Content; - - protected Filter? NameFilter => Search.Filter.Name; - - public abstract OperationKind OperationKind { get; } - - protected abstract void ExecuteMatch(FileMatch fileMatch, string directoryPath); - - public void Execute(string directoryPath) - { - directoryPath = FileSystemHelpers.EnsureFullPath(directoryPath); - - if (!Directory.Exists(directoryPath)) - throw new DirectoryNotFoundException($"Directory not found: {directoryPath}"); - - ExecuteDirectory(directoryPath); - } - - protected virtual void ExecuteDirectory(string directoryPath) - { - foreach (FileMatch fileMatch in GetMatches(directoryPath)) - { - Telemetry.IncrementMatchingCount(fileMatch.IsDirectory); - - ExecuteMatch(fileMatch, directoryPath); - - if (MaxMatchingFiles == Telemetry.MatchingFileDirectoryCount) - { - TerminationReason = TerminationReason.MaxReached; - break; - } - } - } - - protected IEnumerable GetMatches(string directoryPath) - { - return Search.Find( - directoryPath: directoryPath, - notifyDirectoryChanged: this as INotifyDirectoryChanged, - cancellationToken: CancellationToken); - } - - protected void Report(FileMatch fileMatch, Exception? exception = null) - { - Report(fileMatch, newPath: null, exception); - } - - protected void Report(FileMatch fileMatch, string? newPath, Exception? exception = null) - { - Progress?.Report(fileMatch, newPath, OperationKind, exception); - } -} diff --git a/src/FileSystem/Properties/AssemblyInfo.cs b/src/FileSystem/Properties/AssemblyInfo.cs deleted file mode 100644 index 3894ee46..00000000 --- a/src/FileSystem/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -#pragma warning disable RCS0056 - -[assembly: InternalsVisibleTo("Orang, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] diff --git a/src/FileSystem/ReplaceFunctions.cs b/src/FileSystem/ReplaceFunctions.cs index 6a1d3eb5..20265516 100644 --- a/src/FileSystem/ReplaceFunctions.cs +++ b/src/FileSystem/ReplaceFunctions.cs @@ -5,7 +5,7 @@ namespace Orang; [Flags] -public enum ReplaceFunctions +internal enum ReplaceFunctions { None = 0, TrimStart = 1, diff --git a/src/FileSystem/ReplaceOptions.cs b/src/FileSystem/Replacer.cs similarity index 94% rename from src/FileSystem/ReplaceOptions.cs rename to src/FileSystem/Replacer.cs index 2900adcc..ceb484bb 100644 --- a/src/FileSystem/ReplaceOptions.cs +++ b/src/FileSystem/Replacer.cs @@ -9,11 +9,11 @@ namespace Orang; [DebuggerDisplay("{DebuggerDisplay,nq}")] -public class ReplaceOptions : IReplacer +internal class Replacer : IReplacer { - public static ReplaceOptions Empty { get; } = new(""); + public static Replacer Empty { get; } = new(""); - public ReplaceOptions( + public Replacer( string? replacement, ReplaceFunctions functions = ReplaceFunctions.None, bool cultureInvariant = false) @@ -23,7 +23,7 @@ public ReplaceOptions( CultureInvariant = cultureInvariant; } - public ReplaceOptions( + public Replacer( MatchEvaluator matchEvaluator, ReplaceFunctions functions = ReplaceFunctions.None, bool cultureInvariant = false) diff --git a/src/FileSystem/docs/NuGetReadme.md b/src/FileSystem/docs/NuGetReadme.md new file mode 100644 index 00000000..54b77392 --- /dev/null +++ b/src/FileSystem/docs/NuGetReadme.md @@ -0,0 +1,76 @@ +# Orang.FileSystem + +Search, replace, rename and delete files and its content using the power of \.NET regular expressions\. + +## Installation + +```sh +dotnet add package orang.filesystem +``` + +## Documentation + +See [reference documentation](https://josefpihrt.github.io/docs/orang/ref) for further information. + +### Usage + +Clean .NET project temporary folders: + +#### Fluent API + +```cs +using System; +using System.Threading; +using Orang; +using Orang.FileSystem; +using Orang.FileSystem.Fluent; + +IOperationResult result = new SearchBuilder() + .MatchDirectory(d => d + .Name(Pattern.Any("bin", "obj", PatternOptions.Equals)) + .NonEmpty()) + .SkipDirectory(Pattern.Any(".git", ".vs", "node_modules", PatternOptions.Equals | PatternOptions.Literal)) + .Delete(d => d + .ContentOnly() + .DryRun() + .LogOperation(o => Console.WriteLine(o.Path))) + .Run("", CancellationToken.None); + +Console.WriteLine(result.Telemetry.MatchingDirectoryCount); +``` + +#### Classic API + +```cs +using System; +using System.Threading; +using Orang; +using Orang.FileSystem; + +var search = new Search( + new DirectoryMatcher() + { + Name = new Matcher(@"\A(bin|obj)\z"), + EmptyOption = FileEmptyOption.NonEmpty, + }, + new SearchOptions() + { + SearchDirectory = new DirectoryMatcher() + { + Name = new Matcher(@"\A(\.git|\.vs|node_modules)\z", invert: true) + } + }); + +IOperationResult result = search.Delete( + "", + new DeleteOptions() + { + ContentOnly = true, + DryRun = true, + LogOperation = o => Console.WriteLine(o.Path), + }, + CancellationToken.None); + +Console.WriteLine(result.Telemetry.MatchingDirectoryCount); +``` + diff --git a/src/Orang.sln b/src/Orang.sln index 500302a1..f8717e1e 100644 --- a/src/Orang.sln +++ b/src/Orang.sln @@ -12,7 +12,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine", "CommandLine\CommandLine.csproj", "{5FC70E60-F77A-4D4F-8484-6E432351CD1F}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "Core\Core.csproj", "{AD61C9B8-AF0F-420E-85A9-10E8B5AF446C}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{AD61C9B8-AF0F-420E-85A9-10E8B5AF446C}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "EvaluatorTest", "EvaluatorTest\EvaluatorTest.csproj", "{69050EE0-4163-404B-828B-7EF6C8E20847}" EndProject @@ -20,26 +20,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine.Core", "Command EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DocumentationGenerator", "DocumentationGenerator\DocumentationGenerator.csproj", "{0404415B-E221-4A83-99BC-CD211167CE6F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{3A93F055-6756-4074-8F4C-E8858963604C}" - ProjectSection(SolutionItems) = preProject - ..\docs\cli\copy-command.md = ..\docs\cli\copy-command.md - ..\docs\cli\delete-command.md = ..\docs\cli\delete-command.md - ..\docs\cli\escape-command.md = ..\docs\cli\escape-command.md - ..\docs\cli\find-command.md = ..\docs\cli\find-command.md - ..\docs\cli\help-command.md = ..\docs\cli\help-command.md - ..\docs\cli\HowTo.md = ..\docs\cli\HowTo.md - ..\docs\cli\list-patterns-command.md = ..\docs\cli\list-patterns-command.md - ..\docs\cli\match-command.md = ..\docs\cli\match-command.md - ..\docs\cli\move-command.md = ..\docs\cli\move-command.md - ..\docs\cli\OptionValues.md = ..\docs\cli\OptionValues.md - ..\docs\cli\README.md = ..\docs\cli\README.md - ..\docs\cli\rename-command.md = ..\docs\cli\rename-command.md - ..\docs\cli\replace-command.md = ..\docs\cli\replace-command.md - ..\docs\cli\spellcheck-command.md = ..\docs\cli\spellcheck-command.md - ..\docs\cli\split-command.md = ..\docs\cli\split-command.md - ..\docs\cli\sync-command.md = ..\docs\cli\sync-command.md - EndProjectSection -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{76ED7535-91B5-429C-A0FB-FDD8079B1568}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CommandLine.Tests", "Tests\CommandLine.Tests\CommandLine.Tests.csproj", "{CB518C03-1C39-41A2-B289-E655FE648D9A}" @@ -87,6 +67,7 @@ Global {23F2141B-690E-48DB-B570-AD9C960D6C68}.Release|Any CPU.ActiveCfg = Release|Any CPU {23F2141B-690E-48DB-B570-AD9C960D6C68}.Release|Any CPU.Build.0 = Release|Any CPU {CB89202C-9472-49CF-BBF6-1317028BFB08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CB89202C-9472-49CF-BBF6-1317028BFB08}.Debug|Any CPU.Build.0 = Debug|Any CPU {CB89202C-9472-49CF-BBF6-1317028BFB08}.Release|Any CPU.ActiveCfg = Release|Any CPU {76056DDB-D962-4A9A-9EC2-B5FD0A311081}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {76056DDB-D962-4A9A-9EC2-B5FD0A311081}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -99,7 +80,6 @@ Global GlobalSection(NestedProjects) = preSolution {69050EE0-4163-404B-828B-7EF6C8E20847} = {76ED7535-91B5-429C-A0FB-FDD8079B1568} {0404415B-E221-4A83-99BC-CD211167CE6F} = {76ED7535-91B5-429C-A0FB-FDD8079B1568} - {3A93F055-6756-4074-8F4C-E8858963604C} = {8B17331B-D942-4A0C-82AA-23515F4B46C4} {CB518C03-1C39-41A2-B289-E655FE648D9A} = {858E2CFC-11AB-4A87-91A7-C2F433AB891A} {CB89202C-9472-49CF-BBF6-1317028BFB08} = {76ED7535-91B5-429C-A0FB-FDD8079B1568} EndGlobalSection diff --git a/src/Spelling/Properties/AssemblyInfo.cs b/src/Spelling/Properties/AssemblyInfo.cs deleted file mode 100644 index 3894ee46..00000000 --- a/src/Spelling/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Josef Pihrt. All rights reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System.Runtime.CompilerServices; - -#pragma warning disable RCS0056 - -[assembly: InternalsVisibleTo("Orang, PublicKey=00240000048000009400000006020000002400005253413100040000010001009ff202171ab25d708192b490c52c1a373c74a2849c734fd9f545bfedc92b61d4e10d356cd26213ef6d96af669a9b570cd6277d590c338cfc00ccc9a15d6ad5b08ac3a8a09db3eae536d653f4acb9c7e992162129b67b4bc72c08af7d67a48ecde99c53a5d2cd44b1e8179368f6db2ec7665061e3ef4029703df4b49952bd0de4")] diff --git a/src/Spelling/Spelling.csproj b/src/Spelling/Spelling.csproj index 61de26bc..89845371 100644 --- a/src/Spelling/Spelling.csproj +++ b/src/Spelling/Spelling.csproj @@ -15,7 +15,13 @@ - + + + + + + <_Parameter1>Orang, PublicKey=$(OrangPublicKey) + diff --git a/tools/generate_ref_docs.ps1 b/tools/generate_ref_docs.ps1 new file mode 100644 index 00000000..2158e30a --- /dev/null +++ b/tools/generate_ref_docs.ps1 @@ -0,0 +1,26 @@ +$roslynatorExe="../../roslynator/src/CommandLine/bin/Debug/net7.0/Roslynator" + +dotnet restore "../../Roslynator/src/CommandLine.sln" /p:Configuration=Debug -v m +dotnet build "../../Roslynator/src/CommandLine.sln" --no-restore /p:Configuration=Debug /v:m /m + +& $roslynatorExe generate-doc "../src/Orang.sln" ` + --properties "Configuration=Release" ` + --projects "Common" "FileSystem" ` + --heading ".NET API Reference" ` + -o "build/ref" ` + --host docusaurus ` + --group-by-common-namespace ` + --ignored-common-parts content ` + --ignored-root-parts all ` + --max-derived-types 10 + +& $roslynatorExe generate-doc-root "../src/Orang.sln" ` + --properties "Configuration=Release" ` + --projects "Common" "FileSystem" ` + -o "build/ref.md" ` + --host docusaurus ` + --heading ".NET API Reference" ` + --ignored-parts content ` + --root-directory-url "ref" + +Write-Host "DONE" diff --git a/tools/orang_filesystem_api.txt b/tools/orang_filesystem_api.txt new file mode 100644 index 00000000..83fe1e26 --- /dev/null +++ b/tools/orang_filesystem_api.txt @@ -0,0 +1,478 @@ + +namespace Orang + + public interface IDialogProvider + + DialogResult GetResult(TMessage message); + + public enum DialogResult + + None = 0, + + Yes = 1, + + YesToAll = 2, + + No = 3, + + NoToAll = 4, + + Cancel = 5, + + [Flags] + public enum ReplaceFunctions + + None = 0, + + TrimStart = 1, + + TrimEnd = 2, + + Trim = TrimStart | TrimEnd, + + ToLower = 4, + + ToUpper = 8, + +namespace Orang.FileSystem + + public class CopyOptions + + public CopyOptions(); + + public TimeSpan? AllowedTimeDiff { get; set; } + + public Func? CompareDirectory { get; set; } + + public Func? CompareFile { get; set; } + + public FileAttributes? ComparedAttributes { get; set; } + + public FileCompareProperties ComparedProperties { get; set; } + + public ConflictResolution ConflictResolution { get; set; } + + public IDialogProvider? DialogProvider { get; set; } + + public bool DryRun { get; set; } + + public bool Flat { get; set; } + + public Action? LogOperation { get; set; } + + public class DeleteOptions + + public DeleteOptions(); + + public bool ContentOnly { get; set; } + + public bool DryRun { get; set; } + + public bool IncludingBom { get; set; } + + public Action? LogOperation { get; set; } + + public class DirectoryMatcher + + public DirectoryMatcher(); + + public FileEmptyOption EmptyOption { get; set; } + + public Func? MatchDirectoryInfo { get; set; } + + public Matcher? Name { get; set; } + + public FileNamePart NamePart { get; set; } + + public FileAttributes WithAttributes { get; set; } + + public FileAttributes WithoutAttributes { get; set; } + + public class DirectoryMatcherBuilder + + public DirectoryMatcherBuilder EmptyOnly(); + + public DirectoryMatcherBuilder Match(Func predicate); + + public DirectoryMatcherBuilder Name(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public DirectoryMatcherBuilder NamePart(FileNamePart namePart); + + public DirectoryMatcherBuilder NonEmptyOnly(); + + public DirectoryMatcherBuilder WithAttributes(FileAttributes attributes); + + public DirectoryMatcherBuilder WithoutAttributes(FileAttributes attributes); + + public class FileMatch + + public Match? Content { get; } + + public bool IsDirectory { get; } + + public Match? Name { get; } + + public FileNameSpan NameSpan { get; } + + public string Path { get; } + + public class FileMatcher + + public FileMatcher(); + + public Matcher? Content { get; set; } + + public FileEmptyOption EmptyOption { get; set; } + + public Matcher? Extension { get; set; } + + public Func? MatchFileInfo { get; set; } + + public Matcher? Name { get; set; } + + public FileNamePart NamePart { get; set; } + + public FileAttributes WithAttributes { get; set; } + + public FileAttributes WithoutAttributes { get; set; } + + public class FileMatcherBuilder + + public FileMatcherBuilder Content(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public FileMatcherBuilder EmptyOnly(); + + public FileMatcherBuilder Extension(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public FileMatcherBuilder Match(Func predicate); + + public FileMatcherBuilder Name(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public FileMatcherBuilder NamePart(FileNamePart namePart); + + public FileMatcherBuilder NonEmptyOnly(); + + public FileMatcherBuilder WithAttributes(FileAttributes attributes); + + public FileMatcherBuilder WithExtensions(params string[] extensions); + + public FileMatcherBuilder WithoutAttributes(FileAttributes attributes); + + public FileMatcherBuilder WithoutExtensions(params string[] extensions); + + public class RenameOptions + + public RenameOptions(); + + public ConflictResolution ConflictResolution { get; set; } + + public bool CultureInvariant { get; set; } + + public IDialogProvider? DialogProvider { get; set; } + + public bool DryRun { get; set; } + + public Action? LogOperation { get; set; } + + public ReplaceFunctions ReplaceFunctions { get; set; } + + public class ReplaceOptions + + public ReplaceOptions(); + + public bool CultureInvariant { get; set; } + + public bool DryRun { get; set; } + + public Action? LogOperation { get; set; } + + public ReplaceFunctions ReplaceFunctions { get; set; } + + public class Search + + public Search(DirectoryMatcher directoryMatcher, SearchOptions? options = null); + + public Search(FileMatcher fileMatcher, SearchOptions? options = null); + + public Search(FileMatcher fileMatcher, DirectoryMatcher directoryMatcher, SearchOptions? options = null); + + public DirectoryMatcher? DirectoryMatcher { get; } + + public FileMatcher? FileMatcher { get; } + + public SearchOptions Options { get; } + + public IOperationResult Copy(string directoryPath, string destinationPath, CopyOptions? options = null, CancellationToken cancellationToken = default); + + public IOperationResult Delete(string directoryPath, DeleteOptions? options = null, CancellationToken cancellationToken = default); + + public IEnumerable Matches(string directoryPath, CancellationToken cancellationToken = default); + + public IOperationResult Move(string directoryPath, string destinationPath, CopyOptions? options = null, CancellationToken cancellationToken = default); + + public IOperationResult Rename(string directoryPath, MatchEvaluator matchEvaluator, RenameOptions? options = null, CancellationToken cancellationToken = default); + + public IOperationResult Rename(string directoryPath, string replacement, RenameOptions? options = null, CancellationToken cancellationToken = default); + + public IOperationResult Replace(string directoryPath, MatchEvaluator matchEvaluator, ReplaceOptions? options = null, CancellationToken cancellationToken = default); + + public IOperationResult Replace(string directoryPath, string replacement, ReplaceOptions? options = null, CancellationToken cancellationToken = default); + + public class SearchBuilder + + public Search Build(); + + public CopyOperation Copy(string directoryPath, string destinationPath); + + public static SearchBuilder Create(); + + public DeleteOperation Delete(string directoryPath); + + public SearchBuilder DirectoryName(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public SearchBuilder FileContent(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public SearchBuilder FileExtension(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public SearchBuilder FileName(string pattern, RegexOptions options = None, bool invert = false, object? group = null, Func? predicate = null); + + public SearchBuilder LogProgress(Action logProgress); + + public SearchBuilder MatchDirectory(Action configureMatcher); + + public SearchBuilder MatchFile(Action configureMatcher); + + public MoveOperation Move(string directoryPath, string destinationPath); + + public RenameOperation Rename(string directoryPath, MatchEvaluator matchEvaluator); + + public RenameOperation Rename(string directoryPath, string replacement); + + public ReplaceOperation Replace(string directoryPath, string destinationPath); + + public ReplaceOperation Replace(string directoryPath, MatchEvaluator matchEvaluator); + + public SearchBuilder SearchDirectory(Action configureMatcher); + + public SearchBuilder SkipDirectory(string pattern, RegexOptions options = None, object? group = null, Func? predicate = null); + + public SearchBuilder TopDirectoryOnly(); + + public SearchBuilder WithDefaultEncoding(Encoding encoding); + + public class SearchOptions + + public SearchOptions(); + + public Encoding? DefaultEncoding { get; set; } + + public Action? LogProgress { get; set; } + + public DirectoryMatcher? SearchDirectory { get; set; } + + public bool TopDirectoryOnly { get; set; } + + public sealed class SearchTelemetry + + public int DirectoryCount { get; internal set; } + + public int FileCount { get; internal set; } + + public int MatchCount { get; internal set; } + + public int MatchingDirectoryCount { get; internal set; } + + public int MatchingFileCount { get; internal set; } + + public int ProcessedDirectoryCount { get; internal set; } + + public int ProcessedFileCount { get; internal set; } + + public int ProcessedMatchCount { get; internal set; } + + public int SearchedDirectoryCount { get; internal set; } + + public readonly struct ConflictInfo + + public string DestinationPath { get; } + + public string SourcePath { get; } + + public readonly struct FileNameSpan + + public int Length { get; } + + public FileNamePart Part { get; } + + public string Path { get; } + + public int Start { get; } + + public override string ToString(); + + public readonly struct OperationProgress + + public Exception? Exception { get; } + + public FileMatch Match { get; } + + public string? NewPath { get; } + + public string Path { get; } + + public readonly struct SearchProgress + + public Exception? Exception { get; } + + public bool IsDirectory { get; } + + public SearchProgressKind Kind { get; } + + public string Path { get; } + + public interface IOperationResult + + SearchTelemetry Telemetry { get; } + + public enum ConflictResolution + + Overwrite = 0, + + Skip = 1, + + Ask = 2, + + Suffix = 3, + + [Flags] + public enum FileCompareProperties + + None = 0, + + Size = 1, + + ModifiedTime = 2, + + Attributes = 4, + + Content = 8, + + public enum FileEmptyOption + + None = 0, + + Empty = 1, + + NonEmpty = 2, + + public enum FileNamePart + + Name = 0, + + NameWithoutExtension = 1, + + Extension = 2, + + FullName = 3, + + public enum SearchProgressKind + + SearchDirectory = 0, + + Directory = 1, + + File = 2, + +namespace Orang.FileSystem.FileSystem.Operations + + public class CopyOperation + + public CopyOperation CompareAttributes(FileAttributes? attributes = null); + + public CopyOperation CompareContent(); + + public CopyOperation CompareDirectory(Func comparer); + + public CopyOperation CompareFile(Func comparer); + + public CopyOperation CompareModifiedTime(TimeSpan? allowedTimeDiff = null); + + public CopyOperation CompareSize(); + + public CopyOperation DryRun(); + + public IOperationResult Execute(CancellationToken cancellationToken = default); + + public CopyOperation Flat(); + + public CopyOperation LogOperation(Action logOperation); + + public CopyOperation WithConflictResolution(ConflictResolution conflictResolution); + + public CopyOperation WithDialogProvider(IDialogProvider dialogProvider); + + public class DeleteOperation + + public DeleteOperation ContentOnly(); + + public DeleteOperation DryRun(); + + public IOperationResult Execute(CancellationToken cancellationToken = default); + + public DeleteOperation IncludingBom(); + + public DeleteOperation LogOperation(Action logOperation); + + public class MoveOperation + + public MoveOperation CompareAttributes(FileAttributes? attributes = null); + + public MoveOperation CompareContent(); + + public MoveOperation CompareDirectory(Func comparer); + + public MoveOperation CompareFile(Func comparer); + + public MoveOperation CompareModifiedTime(TimeSpan? allowedTimeDiff = null); + + public MoveOperation CompareSize(); + + public MoveOperation DryRun(); + + public IOperationResult Execute(CancellationToken cancellationToken = default); + + public MoveOperation Flat(); + + public MoveOperation LogOperation(Action logOperation); + + public MoveOperation WithConflictResolution(ConflictResolution conflictResolution); + + public MoveOperation WithDialogProvider(IDialogProvider dialogProvider); + + public class RenameOperation + + public RenameOperation CultureInvariant(); + + public RenameOperation DryRun(); + + public IOperationResult Execute(CancellationToken cancellationToken = default); + + public RenameOperation LogOperation(Action logOperation); + + public RenameOperation WithConflictResolution(ConflictResolution conflictResolution); + + public RenameOperation WithDialogProvider(IDialogProvider dialogProvider); + + public RenameOperation WithFunctions(ReplaceFunctions functions); + + public class ReplaceOperation + + public ReplaceOperation CultureInvariant(); + + public ReplaceOperation DryRun(); + + public IOperationResult Execute(CancellationToken cancellationToken = default); + + public ReplaceOperation LogOperation(Action logOperation); + + public ReplaceOperation WithFunctions(ReplaceFunctions functions);