diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0f457cc9d4..5a5d29c54b 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') }}
@@ -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
@@ -100,26 +100,26 @@ 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
- 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') }}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7764f2f7f6..f0e516287b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -27,6 +27,7 @@ All notable changes to this project will be documented in this file.
- [Netkan] Fix Netkan swinfo transformer null list error (#3869 by: HebaruSan)
- [Tooling] Deduce primary branch name in merge script (#3884 by: HebaruSan; reviewed: techman83)
+- [CLI] Parse quoted strings for `ckan prompt` (#3889 by: HebaruSan; reviewed: techman83)
## v1.33.2 (Laplace)
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
- }
- }
-}
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}