Skip to content

Commit

Permalink
Update
Browse files Browse the repository at this point in the history
  • Loading branch information
nd1012 committed Feb 23, 2024
1 parent 9f0a903 commit ee842f0
Show file tree
Hide file tree
Showing 11 changed files with 207 additions and 41 deletions.
29 changes: 24 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,24 @@ A flag is inidicated with a `-[flag]`, having no value following.
A value may be quoted using single or double quotes. If quoted, the value
needs to be escaped for JSON decoding. A backslash needs double escaping.

#### Supported CLR types
#### Supported argument types

Per default these CLR types can be parsed:
Per default these CLR types can be parsed from the CLI argument list:

- `bool`: Flag argument
- `string`: Simple key/value argument
- `string[]`: Simple key/value list argument
- `string`: Simple string (key/)value argument
- `string[]`: Simple string (key/)value list argument

All other CLR types need to be given as JSON encoded values.
All other CLR types need to be given as JSON encoded values, or you use a
custom argument parser - example for float values:

```cs
CliApi.CustomArgumentParsers[typeof(float)] = (name, type, arg, attr) => float.Parse(arg);
```

This custom parser will now be used for `float` argument types. If you want to
use JSON decoding instead, set the `ParseJson` property value of the `CliApi`
attribute of the property or method parameter to `true`.

#### Keyless parameters

Expand Down Expand Up @@ -396,6 +405,16 @@ The `CliApi.GeneralHeader` and `CliApi.HelpHeader` properties store a header,
which will be displayed in general, or if help is being displayed (if there's
a general header, the help header will never be displayed).

## Running as a dotnet tool

Since there's no way to determine if the process is running as dotnet tool,
the CLI command would need to be specified in order to get correct usage
examples from the CLI help API:

```cs
CliApi.CommandLine = "dotnet tool yourapp";
```

## Best practice

You use this library, 'cause it matches your requirements (which
Expand Down
24 changes: 18 additions & 6 deletions src/wan24-CLI Demo/DemoApi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,23 @@ public static int Echo(
[CliApi]
[DisplayText("Message")]
[Description("The message to display (Spectre.Console markup is supported)")]
string message
string message,
int exitCode = 123
)
{
AnsiConsole.MarkupLine(message);
return 123;
return exitCode;
}

[CliApi("echo2")]
[DisplayText("Echo a message")]
[Description("The output to STDOUT is the given message (Spectre.Console markup is supported), exit code will be 456")]
[StdOut("Message")]
[ExitCode(456, "Default exit code")]
public static int Echo2([CliApi] Echo2Arguments args)
public static int Echo2([CliApi] Echo2Arguments args, int exitCode = 456)
{
AnsiConsole.MarkupLine(args.Message);
return 456;
return exitCode;
}

[CliApi("sum")]
Expand All @@ -56,9 +57,9 @@ string[] integers
[Description("The given integers will be summarized, the result will be the exit code")]
[StdOut("Result")]
public static int Sum2(
[CliApi(ParseJson = true, Example = "\"[ 1, 2, 3, ... ]\"")]
[CliApi(ParseJson = true, Example = "1 2 3 ...")]
[DisplayText("Numbers to summarize")]
[Description("Define 1..n integer values as JSON array to summarize")]
[Description("Define 1..n integer values to summarize")]
int[] integers
)
=> integers.Sum();
Expand All @@ -74,6 +75,17 @@ int[] integers
[Description("This API method will throw an exception")]
public static void Error() => throw new InvalidProgramException("Error API method called");

[CliApi("custom")]
[DisplayText("Custom type")]
[Description("Demonstrate the usage of a custom argument type parser")]
public static void CustomType(
[CliApi(Example = "float")]
[DisplayText("Number")]
[Description("Float number to output to the console")]
float number
)
=> Console.WriteLine(number.ToString());

public sealed record class Echo2Arguments : ICliArguments
{
[CliApi]
Expand Down
2 changes: 2 additions & 0 deletions src/wan24-CLI Demo/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
if (File.Exists(fn)) File.Delete(fn);
#endif
await Bootstrap.Async().DynamicContext();
CliConfig.Apply(new(args));
#if DEBUG
Logging.Logger = await FileLogger.CreateAsync(fn, LogLevel.Trace).DynamicContext();
#endif
Translation.Current = Translation.Dummy;
CliApi.HelpHeader = "[white]wan24-CLI Demo API help header[/]";
CliApi.CustomArgumentParsers[typeof(float)] = (name, type, arg, attr) => float.Parse(arg);
return await CliApi.RunAsync(args, default, typeof(CliHelpApi), typeof(DemoApi)).DynamicContext();
2 changes: 1 addition & 1 deletion src/wan24-CLI Demo/Properties/launchSettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"profiles": {
"wan24-CLI Demo": {
"commandName": "Project",
"commandLineArgs": "--api demo --method sum2 -Details"
"commandLineArgs": "--api demo --method custom -Details"
}
}
}
75 changes: 71 additions & 4 deletions src/wan24-CLI/CliApi.Internals.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@
using static wan24.Core.Logging;
using static wan24.Core.Logger;

//TODO Allow custom argument type interpreting

namespace wan24.CLI
{
// Internals
Expand Down Expand Up @@ -90,6 +88,32 @@ internal static (bool Parsed, object? Value) ParseArgument(string name, Type typ
keyLessArgOffset = ca.KeyLessArguments.Count - keyLessOffset;
return (true, res);
}
else if ((FindTypeParser(type) ?? (type.IsArray ? FindTypeParser(type.GetElementType()!) : null)) is ParseType_Delegate parser)
{
// Custom parser
if (!hasValue)
{
if (Debug) WriteDebug($"Custom parsed {type}");
return (false, null);
}
if (!type.IsArray)
{
if (Debug) WriteDebug($"Simple custom parsed {type}");
string value = ca.KeyLessArguments[keyLessOffset + keyLessArgOffset];
keyLessArgOffset++;
return (true, parser(name, type, value, attr));
}
else
{
type = type.GetElementType()!;
if (Debug) WriteDebug($"Array of custom parsed {type}");
string[] values = ca.KeyLessArguments.Skip(keyLessOffset + keyLessArgOffset).ToArray();
keyLessArgOffset = ca.KeyLessArguments.Count - keyLessOffset;
Array arr = Array.CreateInstance(type, values.Length);
for (int i = 0, len = values.Length; i < len; arr.SetValue(parser($"{name}[{i}]", type, values[i], attr), i), i++) ;
return (true, arr);
}
}
else if (type.IsArray)
{
// Array of JSON parsed values
Expand Down Expand Up @@ -128,7 +152,7 @@ internal static (bool Parsed, object? Value) ParseArgument(string name, Type typ
if (existingName is null) return (false, null);
if (ca.IsBoolean(existingName)) throw new CliArgException($"Argument is not a flag (value required)", existingName);
if (ca.All(existingName).Count != 1)
throw new CliArgException($"Only a single value is allowed ({ca.All(existingName).Count} value(e) is/are given)", existingName);
throw new CliArgException($"Only a single value is allowed ({ca.All(existingName).Count} values are given)", existingName);
return (true, ca.Single(existingName));
}
else if (type.IsArray && type.GetElementType() == typeof(string))
Expand All @@ -139,6 +163,32 @@ internal static (bool Parsed, object? Value) ParseArgument(string name, Type typ
if (ca.IsBoolean(existingName)) throw new CliArgException($"Argument is not a flag (value required)", existingName);
return (true, ca.All(existingName).ToArray());
}
else if ((FindTypeParser(type) ?? (type.IsArray ? FindTypeParser(type.GetElementType()!) : null)) is ParseType_Delegate parser)
{
// Custom parser
if (existingName is null)
{
if (Debug) WriteDebug($"Custom parsed {type}");
return (false, null);
}
if (ca.IsBoolean(existingName)) throw new CliArgException($"Argument is not a flag (value required)", existingName);
if (!type.IsArray)
{
if (Debug) WriteDebug($"Simple custom parsed {type}");
if (ca.All(existingName).Count != 1)
throw new CliArgException($"Only a single value is allowed ({ca.All(existingName).Count} values are given)", existingName);
return (true, parser(existingName, type, ca.Single(existingName), attr));
}
else
{
type = type.GetElementType()!;
if (Debug) WriteDebug($"Array of custom parsed {type}");
ReadOnlyCollection<string> values = ca.All(existingName);
Array res = Array.CreateInstance(type, values.Count);
for (int i = 0, len = values.Count; i < len; res.SetValue(parser($"{existingName}[{i}]", type, values[i], attr), i), i++) ;
return (true, res);
}
}
else if (type.IsArray)
{
// Array of JSON parsed values
Expand All @@ -157,7 +207,7 @@ internal static (bool Parsed, object? Value) ParseArgument(string name, Type typ
if (existingName is null) return (false, null);
if (ca.IsBoolean(existingName)) throw new CliArgException($"Argument is not a flag (value required)", existingName);
if (ca.All(existingName).Count != 1)
throw new CliArgException($"Only a single value is allowed ({ca.All(existingName).Count} value(e) is/are given)", existingName);
throw new CliArgException($"Only a single value is allowed ({ca.All(existingName).Count} values are given)", existingName);
return (true, ParseArgumentJsonValue(existingName, type, ca.Single(existingName), attr));
}
}
Expand All @@ -175,6 +225,23 @@ internal static (bool Parsed, object? Value) ParseArgument(string name, Type typ
? JsonHelper.DecodeObject(type, arg)
: throw new InvalidProgramException($"JSON parsing needs to be enabled for argument \"{name}\"");

/// <summary>
/// Find a custom parser for a type
/// </summary>
/// <param name="type">Type</param>
/// <returns>Parser</returns>
internal static ParseType_Delegate? FindTypeParser(Type type)
{
Type? gtd = type.IsGenericType ? type.GetGenericTypeDefinition() : null;
foreach(KeyValuePair<Type, ParseType_Delegate> kvp in CustomArgumentParsers)
{
if (kvp.Key == type) return kvp.Value;
if (type.IsAssignableFrom(type)) return kvp.Value;
if (gtd is not null && kvp.Key.IsGenericType && gtd == kvp.Key.GetGenericTypeDefinition()) return kvp.Value;
}
return null;
}

/// <summary>
/// Find all exported CLI APIs
/// </summary>
Expand Down
32 changes: 30 additions & 2 deletions src/wan24-CLI/CliApi.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Spectre.Console;
using System.Collections.Frozen;
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using wan24.Core;
using static wan24.Core.Logging;
using wan24.ObjectValidation;
using static wan24.Core.Logger;
using static wan24.Core.Logging;
using static wan24.Core.TranslationHelper;

namespace wan24.CLI
Expand Down Expand Up @@ -77,6 +79,17 @@ static CliApi()
/// </summary>
public static string CommandLine { get; set; }

/// <summary>
/// Custom argument type parser delegates
/// </summary>
public static Dictionary<Type, ParseType_Delegate> CustomArgumentParsers { get; } = [];

/// <summary>
/// Display full exceptions in error messages?
/// </summary>
[CliConfig]
public static bool DisplayFullExceptions { get; set; }

/// <summary>
/// Run the CLI API
/// </summary>
Expand Down Expand Up @@ -352,6 +365,7 @@ public static async Task<int> RunAsync(CliArguments ca, string[]? args = null, C
try
{
object? value;
IEnumerable<System.ComponentModel.DataAnnotations.ValidationResult> results;
foreach (ParameterInfo pi in mi.GetParameters())
if (pi.GetCustomAttributeCached<CliApiAttribute>() is not CliApiAttribute attr)
{
Expand All @@ -374,7 +388,11 @@ public static async Task<int> RunAsync(CliArguments ca, string[]? args = null, C
{
// Parsed argument
if (Trace) WriteTrace($"Using parsed argument {pi.Name} ({pi.ParameterType})");
param.Add(ParseArgument(pi.Name!, pi.ParameterType, ca, attr, keyLessOffset, ref keyLessArgOffset).Value);
value = ParseArgument(pi.Name!, pi.ParameterType, ca, attr, keyLessOffset, ref keyLessArgOffset).Value;
results = value.ValidateValue(pi.GetCustomAttributesCached<ValidationAttribute>());
if (results.Any())
throw new CliArgException($"Parsed argument validation failed: {results.First().ErrorMessage}", pi.Name, new ObjectValidationException(results));
param.Add(value);
}
}
catch (Exception ex)
Expand Down Expand Up @@ -548,5 +566,15 @@ public static void DisplayHelpHeader()
AnsiConsole.MarkupLine(_(HelpHeader));
Console.WriteLine();
}

/// <summary>
/// Delegate for a custom argument type parser (needs to throw on error)
/// </summary>
/// <param name="name">Argument name</param>
/// <param name="type">Type</param>
/// <param name="arg">Argument value</param>
/// <param name="attr">Attribute</param>
/// <returns>Parsed object</returns>
public delegate object ParseType_Delegate(string name, Type type, string arg, CliApiAttribute attr);
}
}
20 changes: 14 additions & 6 deletions src/wan24-CLI/CliApiHelper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Spectre.Console;
using System.Diagnostics.Contracts;
using wan24.Core;
using static wan24.Core.TranslationHelper;

namespace wan24.CLI
Expand Down Expand Up @@ -29,15 +30,22 @@ public virtual int DisplayHelp(CliApiContext context)
{
CliApi.StdErr.MarkupLine(argException is not null
? $"[white on red]{_("Invalid argument")} \"{argException.ParamName.EscapeMarkup()}\": {context.Exception.Message.EscapeMarkup()}[/]"
: $"[white on red]{_("An exception has been catched")}: {context.Exception.ToString().EscapeMarkup()}[/]");
: $"[white on red]{_("An exception has been catched")}: {(CliApi.DisplayFullExceptions ? context.Exception.ToString().EscapeMarkup() : context.Exception.Message.EscapeMarkup())}[/]");
CliApi.StdErr.WriteLine();
}
CliHelpApi help = new()
CliHelpApi help = CliApi.ExportedApis.Values.Where(a => typeof(CliHelpApi).IsAssignableFrom(a.Type)).FirstOrDefault()?.Type is Type apiHelpType
? apiHelpType.ConstructAuto() as CliHelpApi ?? throw new InvalidProgramException($"Failed to instance API help from {apiHelpType}")
: new();
try
{
ApiName = context.API?.GetType().GetCliApiName(),
ApiMethodName = context.Method?.GetCliApiMethodName()
};
help.Help();
help.ApiName = context.API?.GetType().GetCliApiName();
help.ApiMethodName = context.Method?.GetCliApiMethodName();
help.Help();
}
finally
{
help.TryDispose();
}
return 1;
}
}
Expand Down
5 changes: 5 additions & 0 deletions src/wan24-CLI/CliApiInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ public CliApiInfo(Type api, NullabilityInfoContext? nic = null)
/// </summary>
public static Color BackGroundColor { get; set; } = Color.Black;

/// <summary>
/// Highlight color
/// </summary>
public static Color HighlightColor { get; set; } = Color.White;

/// <summary>
/// Required color
/// </summary>
Expand Down
Loading

0 comments on commit ee842f0

Please sign in to comment.