Skip to content

Commit

Permalink
Parse quoted strings for ckan prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Aug 22, 2023
1 parent 12fbb72 commit e068b54
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 71 deletions.
149 changes: 91 additions & 58 deletions Cmdline/Action/Prompt.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Reflection;
using System.Linq;
using System.Collections.Generic;
using System.Text.RegularExpressions;

using CommandLine;
using CommandLine.Text;
Expand Down Expand Up @@ -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)
{
Expand All @@ -70,7 +72,27 @@ public int RunCommand(object raw_options)
return Exit.OK;
}

private string ReadLineWithCompletion(bool headless)
/// <summary>
/// Split string on spaces, unless they are between quotes.
/// Inspired by https://stackoverflow.com/a/14655145/2422988
/// </summary>
/// <param name="input">The string to parse</param>
/// <returns>Array split by strings, with quoted parts joined together</returns>
private static string[] ParseTextField(string input)
=> quotePattern.Matches(input)
.Cast<Match>()
.Select(m => m.Value)
.ToArray();

/// <summary>
/// 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
/// </summary>
private static readonly Regex quotePattern = new Regex(
@"(?<="")[^""]*(?="")|[^ ""]+|(?<= )$", RegexOptions.Compiled);

private static string ReadLineWithCompletion(bool headless)
{
try
{
Expand All @@ -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<string> extras = new List<string> { exitCommand, "help" };
foreach (string piece in pieces.Take(pieces.Length - 1))
Expand All @@ -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<OptionAttribute>()?.LongName)
private static string[] GetLongOptions(TypeInfo ti, string prefix)
=> AllBaseTypes(ti.AsType())
.SelectMany(t => t.GetTypeInfo().DeclaredProperties)
.Select(p => p.GetCustomAttribute<OptionAttribute>()?.LongName
?? p.GetCustomAttribute<OptionArrayAttribute>()?.LongName
?? p.GetCustomAttribute<OptionListAttribute>()?.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<VerbOptionAttribute>() != null);
}

private string[] GetVerbs(TypeInfo ti, string prefix, IEnumerable<string> extras)
{
return ti.DeclaredProperties
.Select(p => p.GetCustomAttribute<VerbOptionAttribute>()?.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<OptionAttribute>()?.ShortName
?? p.GetCustomAttribute<OptionArrayAttribute>()?.ShortName
?? p.GetCustomAttribute<OptionListAttribute>()?.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<Type> AllBaseTypes(Type start)
{
return ti.DeclaredProperties
.Any(p => p.GetCustomAttribute<AvailableIdentifiersAttribute>() != 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<VerbOptionAttribute>() != null);

private static string[] GetVerbs(TypeInfo ti, string prefix, IEnumerable<string> extras)
=> ti.DeclaredProperties
.Select(p => p.GetCustomAttribute<VerbOptionAttribute>()?.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<AvailableIdentifiersAttribute>() != 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<InstalledIdentifiersAttribute>() != null);
}
private static bool WantsInstIdentifiers(TypeInfo ti)
=> ti.DeclaredProperties
.Any(p => p.GetCustomAttribute<InstalledIdentifiersAttribute>() != 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<GameInstancesAttribute>() != null);
}
private static bool WantsGameInstances(TypeInfo ti)
=> ti.DeclaredProperties
.Any(p => p.GetCustomAttribute<GameInstancesAttribute>() != 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";
Expand Down
1 change: 1 addition & 0 deletions Cmdline/Options.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Reflection;
using System.Collections.Generic;
using System.Text.RegularExpressions;

using log4net;
using log4net.Core;
using CommandLine;
Expand Down
28 changes: 15 additions & 13 deletions Core/Meta.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,32 +23,34 @@ public static string GetVersion(VersionFormat format = VersionFormat.Normal)
.GetAssemblyAttribute<AssemblyInformationalVersionAttribute>()
.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;

Check warning on line 30 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 30 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 30 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected

Check warning on line 30 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected
case VersionFormat.Normal:
if (plusIndex >= 0)
version = version.Substring(0, plusIndex);

return $"v{version.UpToCharacter('+')}";
break;

Check warning on line 33 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 33 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 33 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected

Check warning on line 33 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected
case VersionFormat.Full:
return $"v{version}";
break;

Check warning on line 36 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 36 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Debug_NetCore)

Unreachable code detected

Check warning on line 36 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected

Check warning on line 36 in Core/Meta.cs

View workflow job for this annotation

GitHub Actions / build_NetCore (Release_NetCore)

Unreachable code detected
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<T>(this Assembly assembly)
=> (T)assembly.GetCustomAttributes(typeof(T), false)
.First();
Expand Down

0 comments on commit e068b54

Please sign in to comment.