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..478d655b39 100644 --- a/Core/Meta.cs +++ b/Core/Meta.cs @@ -23,32 +23,34 @@ 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); - + return $"v{version.UpToCharacters(shortDelimiters)}"; break; case VersionFormat.Normal: - if (plusIndex >= 0) - version = version.Substring(0, plusIndex); - + return $"v{version.UpToCharacter('+')}"; break; case VersionFormat.Full: + return $"v{version}"; break; 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 - } - } -}