From 2a8999b0b2780e2705541803e41159661fe08728 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 22 Aug 2023 12:32:56 -0500 Subject: [PATCH 1/5] Parse quoted strings for `ckan prompt` --- Cmdline/Action/Prompt.cs | 149 ++++++++++++++++++++++++--------------- Cmdline/Options.cs | 1 + Core/Meta.cs | 31 ++++---- Core/Meta.cs.in | 70 ------------------ 4 files changed, 107 insertions(+), 144 deletions(-) delete mode 100644 Core/Meta.cs.in diff --git a/Cmdline/Action/Prompt.cs b/Cmdline/Action/Prompt.cs index b9cd803636..d3f287cb6f 100644 --- a/Cmdline/Action/Prompt.cs +++ b/Cmdline/Action/Prompt.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Linq; using System.Collections.Generic; +using System.Text.RegularExpressions; using CommandLine; using CommandLine.Text; @@ -52,7 +53,8 @@ public int RunCommand(object raw_options) { // Parse input as if it was a normal command line, // but with a persistent GameInstanceManager object. - int cmdExitCode = MainClass.Execute(manager, opts, command.Split(' ')); + int cmdExitCode = MainClass.Execute(manager, opts, + ParseTextField(command)); // Clear the command if no exception was thrown if (headless && cmdExitCode != Exit.OK) { @@ -70,7 +72,27 @@ public int RunCommand(object raw_options) return Exit.OK; } - private string ReadLineWithCompletion(bool headless) + /// + /// Split string on spaces, unless they are between quotes. + /// Inspired by https://stackoverflow.com/a/14655145/2422988 + /// + /// The string to parse + /// Array split by strings, with quoted parts joined together + private static string[] ParseTextField(string input) + => quotePattern.Matches(input) + .Cast() + .Select(m => m.Value) + .ToArray(); + + /// + /// Look for non-quotes surrounded by quotes, or non-space-or-quotes, or end preceded by space. + /// No attempt to allow escaped quotes within quotes. + /// Inspired by https://stackoverflow.com/a/14655145/2422988 + /// + private static readonly Regex quotePattern = new Regex( + @"(?<="")[^""]*(?="")|[^ ""]+|(?<= )$", RegexOptions.Compiled); + + private static string ReadLineWithCompletion(bool headless) { try { @@ -87,7 +109,7 @@ private string ReadLineWithCompletion(bool headless) private string[] GetSuggestions(string text, int index) { - string[] pieces = text.Split(new char[] { ' ' }); + string[] pieces = ParseTextField(text); TypeInfo ti = typeof(Actions).GetTypeInfo(); List extras = new List { exitCommand, "help" }; foreach (string piece in pieces.Take(pieces.Length - 1)) @@ -103,88 +125,99 @@ private string[] GetSuggestions(string text, int index) extras.Clear(); } var lastPiece = pieces.LastOrDefault() ?? ""; - return lastPiece.StartsWith("--") ? GetOptions(ti, lastPiece.Substring(2)) - : HasVerbs(ti) ? GetVerbs(ti, lastPiece, extras) - : WantsAvailIdentifiers(ti) ? GetAvailIdentifiers(lastPiece) - : WantsInstIdentifiers(ti) ? GetInstIdentifiers(lastPiece) - : WantsGameInstances(ti) ? GetGameInstances(lastPiece) - : null; + return lastPiece.StartsWith("--") ? GetLongOptions(ti, lastPiece.Substring(2)) + : lastPiece.StartsWith("-") ? GetShortOptions(ti, lastPiece.Substring(1)) + : HasVerbs(ti) ? GetVerbs(ti, lastPiece, extras) + : WantsAvailIdentifiers(ti) ? GetAvailIdentifiers(lastPiece) + : WantsInstIdentifiers(ti) ? GetInstIdentifiers(lastPiece) + : WantsGameInstances(ti) ? GetGameInstances(lastPiece) + : null; } - private string[] GetOptions(TypeInfo ti, string prefix) - { - return ti.DeclaredProperties - .Select(p => p.GetCustomAttribute()?.LongName) + private static string[] GetLongOptions(TypeInfo ti, string prefix) + => AllBaseTypes(ti.AsType()) + .SelectMany(t => t.GetTypeInfo().DeclaredProperties) + .Select(p => p.GetCustomAttribute()?.LongName + ?? p.GetCustomAttribute()?.LongName + ?? p.GetCustomAttribute()?.LongName) .Where(o => o != null && o.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) .OrderBy(o => o) .Select(o => $"--{o}") .ToArray(); - } - - private bool HasVerbs(TypeInfo ti) - { - return ti.DeclaredProperties - .Any(p => p.GetCustomAttribute() != null); - } - private string[] GetVerbs(TypeInfo ti, string prefix, IEnumerable extras) - { - return ti.DeclaredProperties - .Select(p => p.GetCustomAttribute()?.LongName) - .Where(v => v != null) - .Concat(extras) - .Where(v => v.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) - .OrderBy(v => v) + private static string[] GetShortOptions(TypeInfo ti, string prefix) + => AllBaseTypes(ti.AsType()) + .SelectMany(t => t.GetTypeInfo().DeclaredProperties) + .Select(p => p.GetCustomAttribute()?.ShortName + ?? p.GetCustomAttribute()?.ShortName + ?? p.GetCustomAttribute()?.ShortName) + .Where(o => o != null && $"{o}".StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(o => o) + .Select(o => $"-{o}") .ToArray(); - } - private bool WantsAvailIdentifiers(TypeInfo ti) + private static IEnumerable AllBaseTypes(Type start) { - return ti.DeclaredProperties - .Any(p => p.GetCustomAttribute() != null); + for (Type t = start; t != null; t = t.BaseType) + { + yield return t; + } } + private static bool HasVerbs(TypeInfo ti) + => ti.DeclaredProperties + .Any(p => p.GetCustomAttribute() != null); + + private static string[] GetVerbs(TypeInfo ti, string prefix, IEnumerable extras) + => ti.DeclaredProperties + .Select(p => p.GetCustomAttribute()?.LongName) + .Where(v => v != null) + .Concat(extras) + .Where(v => v.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + .OrderBy(v => v) + .ToArray(); + + private static bool WantsAvailIdentifiers(TypeInfo ti) + => ti.DeclaredProperties + .Any(p => p.GetCustomAttribute() != null); + private string[] GetAvailIdentifiers(string prefix) { CKAN.GameInstance inst = MainClass.GetGameInstance(manager); - return RegistryManager.Instance(inst).registry - .CompatibleModules(inst.VersionCriteria()) - .Where(m => !m.IsDLC) - .Select(m => m.identifier) - .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) - .ToArray(); + return RegistryManager.Instance(inst) + .registry + .CompatibleModules(inst.VersionCriteria()) + .Where(m => !m.IsDLC) + .Select(m => m.identifier) + .Where(ident => ident.StartsWith(prefix, + StringComparison.InvariantCultureIgnoreCase)) + .ToArray(); } - private bool WantsInstIdentifiers(TypeInfo ti) - { - return ti.DeclaredProperties - .Any(p => p.GetCustomAttribute() != null); - } + private static bool WantsInstIdentifiers(TypeInfo ti) + => ti.DeclaredProperties + .Any(p => p.GetCustomAttribute() != null); private string[] GetInstIdentifiers(string prefix) { CKAN.GameInstance inst = MainClass.GetGameInstance(manager); var registry = RegistryManager.Instance(inst).registry; return registry.Installed(false, false) - .Select(kvp => kvp.Key) - .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase) - && !registry.GetInstalledVersion(ident).IsDLC) - .ToArray(); + .Select(kvp => kvp.Key) + .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase) + && !registry.GetInstalledVersion(ident).IsDLC) + .ToArray(); } - private bool WantsGameInstances(TypeInfo ti) - { - return ti.DeclaredProperties - .Any(p => p.GetCustomAttribute() != null); - } + private static bool WantsGameInstances(TypeInfo ti) + => ti.DeclaredProperties + .Any(p => p.GetCustomAttribute() != null); private string[] GetGameInstances(string prefix) - { - return manager.Instances - .Select(kvp => kvp.Key) - .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) - .ToArray(); - } + => manager.Instances + .Select(kvp => kvp.Key) + .Where(ident => ident.StartsWith(prefix, StringComparison.InvariantCultureIgnoreCase)) + .ToArray(); private readonly GameInstanceManager manager; private const string exitCommand = "exit"; diff --git a/Cmdline/Options.cs b/Cmdline/Options.cs index a7b3164e2a..11e89f0a52 100644 --- a/Cmdline/Options.cs +++ b/Cmdline/Options.cs @@ -4,6 +4,7 @@ using System.Reflection; using System.Collections.Generic; using System.Text.RegularExpressions; + using log4net; using log4net.Core; using CommandLine; diff --git a/Core/Meta.cs b/Core/Meta.cs index c4d8a620d8..dac54e12bb 100644 --- a/Core/Meta.cs +++ b/Core/Meta.cs @@ -23,32 +23,31 @@ public static string GetVersion(VersionFormat format = VersionFormat.Normal) .GetAssemblyAttribute() .InformationalVersion; - var dashIndex = version.IndexOf('-'); - var plusIndex = version.IndexOf('+'); - switch (format) { case VersionFormat.Short: - if (dashIndex >= 0) - version = version.Substring(0, dashIndex); - else if (plusIndex >= 0) - version = version.Substring(0, plusIndex); - - break; + return $"v{version.UpToCharacters(shortDelimiters)}"; case VersionFormat.Normal: - if (plusIndex >= 0) - version = version.Substring(0, plusIndex); - - break; + return $"v{version.UpToCharacter('+')}"; case VersionFormat.Full: - break; + return $"v{version}"; default: throw new ArgumentOutOfRangeException(nameof(format), format, null); } - - return "v" + version; } + private static readonly char[] shortDelimiters = new char[] { '-', '+' }; + + private static string UpToCharacter(this string orig, char what) + => orig.UpToIndex(orig.IndexOf(what)); + + private static string UpToCharacters(this string orig, char[] what) + => orig.UpToIndex(orig.IndexOfAny(what)); + + private static string UpToIndex(this string orig, int index) + => index == -1 ? orig + : orig.Substring(0, index); + private static T GetAssemblyAttribute(this Assembly assembly) => (T)assembly.GetCustomAttributes(typeof(T), false) .First(); diff --git a/Core/Meta.cs.in b/Core/Meta.cs.in deleted file mode 100644 index 26a4da002a..0000000000 --- a/Core/Meta.cs.in +++ /dev/null @@ -1,70 +0,0 @@ -using System.Text.RegularExpressions; - -namespace CKAN -{ - public static class Meta - { - public readonly static string Development = "development"; - - // Do *not* change the following line, BUILD_VERSION is - // replaced by our build system with our actual version. - - private readonly static string BUILD_VERSION = <%version%>; - - /// - /// Returns the version of the CKAN.dll used, complete with git info - /// and other decorations as filled in by our build system. - /// Eg: v1.3.5-12-g055d7c3 (beta) or "development (unstable)" - /// - public static string Version() - { - string version = BuildVersion(); - - #if (STABLE) - version += " (stable)"; - #else - version += " (beta)"; - #endif - - return version; - } - - /// - /// Returns only the build info, with no decorations, or "development" if - /// unknown. - /// - public static string BuildVersion() - { - return BUILD_VERSION ?? Development; - } - - /// - /// Returns just our release number (eg: 1.0.3), or null for a dev build. - /// - public static Version ReleaseNumber() - { - string build_version = BuildVersion(); - - if (build_version == Development) - { - return null; - } - - string short_version = Regex.Match(build_version, @"^(.*)-\d+-.*$").Result("$1"); - - return new Version(short_version); - } - - /// - /// Returns true if this is a 'stable' build, false otherwise. - /// - public static bool IsStable() - { - #if (STABLE) - return true; - #else - return false; - #endif - } - } -} From 053a085ea831f2de7fdcd7d83dd7fd44b3069b41 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 22 Aug 2023 22:44:44 -0500 Subject: [PATCH 2/5] Increment version of actions/cache --- .github/workflows/build.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0f457cc9d4..144bfdf900 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -32,22 +32,22 @@ jobs: - name: Install runtime dependencies run: apt-get install -y xvfb - name: Restore cache for _build/tools - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/tools key: build-tools-${{ hashFiles('build', 'build.ps1', 'build.cake') }} - name: Restore cache for _build/cake - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/cake key: build-cake-${{ hashFiles('build.cake') }} - name: Restore cache for _build/lib/nuget - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/lib/nuget key: nuget-oldref-modules-${{ hashFiles('**/packages.config') }}-${{ hashFiles('**/*.csproj') }} - name: Restore cache for ~/.nuget/packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.nuget/packages key: nuget-packref-modules-${{ hashFiles('**/packages.config') }}-${{ hashFiles('**/*.csproj') }} @@ -104,22 +104,22 @@ jobs: with: dotnet-version: '5.0.x' - name: Restore cache for _build/tools - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/tools key: build-tools-${{ hashFiles('build', 'build.ps1', 'build.cake') }} - name: Restore cache for _build/cake - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/cake key: build-cake-${{ hashFiles('build.cake') }} - name: Restore cache for _build/lib/nuget - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: _build/lib/nuget key: nuget-oldref-modules-${{ hashFiles('**/packages.config') }}-${{ hashFiles('**/*.csproj') }} - name: Restore cache for ~/.nuget/packages - uses: actions/cache@v1 + uses: actions/cache@v3 with: path: ~/.nuget/packages key: nuget-packref-modules-${{ hashFiles('**/packages.config') }}-${{ hashFiles('**/*.csproj') }} From 500a18308eb19d970ee4e1b40010fc26caca2027 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 22 Aug 2023 22:49:37 -0500 Subject: [PATCH 3/5] Increment version of actions/setup-dotnet --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 144bfdf900..f6d3c99242 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -100,7 +100,7 @@ jobs: steps: - uses: actions/checkout@v3 - name: Setup .NET Core - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v3 with: dotnet-version: '5.0.x' - name: Restore cache for _build/tools From fb956cc0aca0e1bce8ddeefe40738cbb346f5061 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 22 Aug 2023 22:56:23 -0500 Subject: [PATCH 4/5] Remove duplicate resources --- Core/Properties/Resources.fr-FR.resx | 3 --- Core/Properties/Resources.it-IT.resx | 3 --- Core/Properties/Resources.pl-PL.resx | 3 --- Core/Properties/Resources.resx | 1 - 4 files changed, 10 deletions(-) diff --git a/Core/Properties/Resources.fr-FR.resx b/Core/Properties/Resources.fr-FR.resx index 60bef93254..53cd781054 100644 --- a/Core/Properties/Resources.fr-FR.resx +++ b/Core/Properties/Resources.fr-FR.resx @@ -641,9 +641,6 @@ Libérez de l'espace sur cet appareil ou modifiez vos paramètres pour utiliser {0} {1} ({2}, {3} restant) - - * Installation : {0} {1} ({2}, {3} restant) - * Mise à jour : {0} {1} vers {2} ({3}, {4} restant) diff --git a/Core/Properties/Resources.it-IT.resx b/Core/Properties/Resources.it-IT.resx index 2d98b7c075..dac4472307 100644 --- a/Core/Properties/Resources.it-IT.resx +++ b/Core/Properties/Resources.it-IT.resx @@ -630,9 +630,6 @@ Libera spazio su quel dispositivo o modifica le impostazioni per utilizzare un'a {0} {1} ({2}, {3} rimanenti) - - * Installa: {0} {1} ({2}, {3} rimanenti) - * Aggiorna: {0} {1} a {2} ({3}, {4} rimanenti) diff --git a/Core/Properties/Resources.pl-PL.resx b/Core/Properties/Resources.pl-PL.resx index 8f76375558..acf3f82d2f 100644 --- a/Core/Properties/Resources.pl-PL.resx +++ b/Core/Properties/Resources.pl-PL.resx @@ -630,9 +630,6 @@ Zwolnij miejsce na tym urządzeniu lub zmień ustawienia, aby użyć innej lokal {0} {1} ({2}, {3} pozostało) - - * Instalacja: {0} {1} ({2}, {3} pozostało) - * Aktualizacja: {0} {1} do {2} ({3}, {4} pozostało) diff --git a/Core/Properties/Resources.resx b/Core/Properties/Resources.resx index f2d1af10af..210054617e 100644 --- a/Core/Properties/Resources.resx +++ b/Core/Properties/Resources.resx @@ -304,7 +304,6 @@ Free up space on that device or change your settings to use another location. {0} {1} (cached) {0} {1} ({2}, {3}) {0} {1} ({2}, {3} remaining) - * Install: {0} {1} ({2}, {3} remaining) * Upgrade: {0} {1} to {2} ({3}, {4} remaining) installed-{0} From 6eb1e4d12e019e9783af89175bb9834aeb4dc689 Mon Sep 17 00:00:00 2001 From: Paul Hebble Date: Tue, 22 Aug 2023 22:56:34 -0500 Subject: [PATCH 5/5] Increment version of actions/upload-artifact --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f6d3c99242..5a5d29c54b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -69,7 +69,7 @@ jobs: if: matrix.configuration == 'release' && ( matrix.mono == '6.8' || matrix.mono == 'latest' ) - name: Upload ckan.exe artifact - uses: actions/upload-artifact@v2 + uses: actions/upload-artifact@v3 with: name: ckan.exe path: _build/repack/Release/ckan.exe