Skip to content

Commit

Permalink
Merge #3889 Parse quoted strings for ckan prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
HebaruSan committed Aug 23, 2023
2 parents 12fbb72 + 6eb1e4d commit b916779
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 164 deletions.
20 changes: 10 additions & 10 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
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
31 changes: 15 additions & 16 deletions Core/Meta.cs
Original file line number Diff line number Diff line change
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
Loading

0 comments on commit b916779

Please sign in to comment.