Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse quoted strings for ckan prompt #3889

Merged
merged 5 commits into from Aug 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
20 changes: 10 additions & 10 deletions .github/workflows/build.yml
Expand Up @@ -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') }}
Expand All @@ -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
Expand Down Expand Up @@ -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') }}
Expand Down
149 changes: 91 additions & 58 deletions Cmdline/Action/Prompt.cs
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
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
31 changes: 15 additions & 16 deletions Core/Meta.cs
Expand Up @@ -23,32 +23,31 @@ 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);

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