Skip to content

nd1012/wan24-CLI

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

63 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

wan24-CLI

This library contains some CLI app helpers. It uses the Spectre.Console library for console output.

Spectre.Console contains some of the functionality this library provides, but in a different style, which may not meet some developers needs. The goal of this library is to provide a tool for (even more) rapid CLI app API development.

Usage

Create a CLI API type:

[CliApi]
public class YourCliApi
{
	public YourCliApi() { }

	[CliApi, Required]
	public string Value { get; set; } = null!;

	[CliApi]
	public void YourApiMethod()
	{
		Console.WriteLine(Value);
	}
}

In your CLI apps startup:

await wan24.Core.Bootstrap.Async();
Translation.Current = Translation.Dummy;// Or initialize with your own translations
return await CliApi.RunAsync(args);

Usage of your CLI API:

dotnet app.dll --value 'Hello world!'

CLI argument types

Key/value pairs

A key/value pair is indicated with a --[key], followed by its value.

Key with multiple values

A key may allow a value list, which follows the key. If a 2nd value starts with a dash, it needs to be prepended with another key indicator - example:

dotnet app.dll --key value1 value2 --key -value3

Flags

A flag is inidicated with a -[flag], having no value following.

Value quoting

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 argument types

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

  • bool: Flag argument
  • string: Simple string (key/)value argument
  • string[]: Simple string (key/)value list argument
  • FileStream: Simple string (key/)value argument using CliApiFileStream attribute
  • FileStream[]: Simple string (key/)value argument using CliApiFileStream attribute

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

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.

The CliApiAttribute allows overriding the CanParseArgument property and the ParseArgument method to implement a custom argument value type parsing within the attribute directly.

FileStream argument value type using CliApiFileStreamAttribute

A property or a method parameter type may be FileStream or FileStream[], if the CliApiFileStream attribute was used. The attribute allows to set how to create the filestream using wan24.Core.FsHelper.CreateFileStream.

The CliApiFileStreamAttribute contains a custom argument value parser which may also be overridden, if required.

Keyless parameters

Example:

dotnet app.dll -flag value1 --key value2 - value3 value4

-flag is a flag, while value1 is a keyless value, and value2 will be stored to the key key. value3 and value4 are appended to the keyless values list of the parsed CliArguments instance. The single dash is used to separate them from the value list for key.

Another example:

dotnet app.dll value1 value2 value3 ... -flag --key valueN

All arguments before -flag are handled as keyless values.

For binding keyless values to a CLI argument property or parameter, the CliApi attribute constructor which takes a keyless value offset needs to be used (more about that in the following chapters).

STDIN and STDOUT

Uing the StdIn and StdOut attributes you can add STDIN/-OUT usage informations to the method help.

CLI argument binding

As API type property

This is the recommended way for the argument binding:

[CliApi, Required]
public string Value { get; set; } = null!;

This binding will set the value of the argument --value to the property before invoking the requested API method.

Don't worry about the = null! initialization of the property: The API instance will be validated. If --value wasn't given, an usage help will be displayed.

As API method parameter

[CliApi]
public void YourApiMethod([CliApi] string value)
{
	...
}

This binding will set the value of the argument --value to the parameter, when invoking the requested API method.

As argument object

The argument object:

public class YourApiMethodArguments : ICliArguments
{
	[CliApi, Required]
	public string Value { get; set; } = null!;
}

By implementing the empty ICliArguments interface, an argument type won't be JSON parsed, but used as argument object.

Don't worry about the = null! initialization of the property: The API instance will be validated. If --value wasn't given, an usage help will be displayed.

The API method which will consume the argument object:

[CliApi]
public void YourApiMethod([CliApi] YourApiMethodArguments args)
{
	...
}

Binding keyless arguments

Example API type:

[CliApi]
public class YourCliApi
{
	public YourCliApi() { }

	[CliApi(0)]
	public string KeyLessValue1 { get ; set; } = null!;

	[CliApi(1)]
	public string[] KeyLessValues2 { get; set; } = null!;

	[CliApi]
	public string NamedValue { get; set; } = null!;

	[CliApi]
	public void YourApiMethod()
	{
		....
	}
}

Example CLI app call:

dotnet app.dll keyLessValue1 --namedValue 'Named value' keyLessValue2a keyLessValue2b

Now the properties of YourCliApi have these values:

  • KeyLessValue1: "keyLessValue1"
  • NamedValue: "Named value"
  • KeyLessValues2: string[] { "keyLessValue2a", "keyLessValue2b"}

A keyless value list must always be taken from the last possible offset.

Multiple API types and multiple methods within an API type

Serving multiple APIs:

await CliApi.RunAsync(args, default, typeof(YourCliApi1), typeof(YourCliApi2), ...);

The first keyless argument needs to be the API name (f.e. YourApiType). Keyless argument bindings within your APIs still begin with 0.

When serving multiple API methods within an API type, the second keyless argument needs to be the API method name. If you serve only one API type, the name of the API method will be taken from the first keyless argument. Keyless argument bindings within your APIs still begin with 0.

Custom API, method and argument names

You can give a custom API/method/argument name to the CliApi attribute constructor.

JSON parsed arguments

Set the ParseJson property value of the CliApi attribute for an argument to true to enable JSON parsing of the given value.

NOTE: JSON parsing must be enabled for numeric types, for example!

API documentation

API documentation will be generated automatic and be displayed on wrong usage. To add details, you can use the DisplayText (for titles) and Description attributes. If an argument isn't a flag, you can add an example value to display to the Example property of the CliApi attribute.

You may also specify a static string property which returns the help text for an API/method/argument by setting the properties namespace and name to the HelpTextProperty property of the CliApi attribute. The text contents will be parsed, and Spectre.Console markup is supported also. You may use these variables:

  • %{CommandLine}: The command line used to call the CLI in general
  • %{API}: The current API name
  • %{Method}: The current API method name

The wan24-Core string parser is being used for this.

Another option for help details is the HelpMethod property of the CliApi attribute, which may contain a public static API element help method name (see CliHelpApi.DetailHelp-Delegate). This method will then be executed, if help details for an element have been requested.

If your API methods return an exit code, you can add documentation for them using the ExitCode attribute on the method.

Help output uses the Spectre.Console markup syntax for printing rich output to an ANSI console. The default colors used can be customized in the static CliApiInfo properties.

All help output can be localized. For a full localization, you can parse the wan24-CLI source code with wan24-I8NTools to get a common PO file.

For intercepting errors there are multiple ways:

ICliApiErrorHandler

If your API type implements the ICliApiErrorHandler interface, errors during processing will be handled by your API type.

In case your method can't handle the error, you can forward the error handling to the default error processing by calling CliApi.DisplayHelpAsync and setting the value of the parameter useApi to false.

ICliApiHelpProvider

If your API type implements the ICliApiHelpProvider interface, wrong usage can be handled by your API type.

NOTE: If your API type implements ICliApiErrorHandler, too, the help provider will only be called, if there was no exception.

In case your method can't display the context help, you can forward the help handling to the default help display processing by calling CliApi.DisplayHelpAsync and setting the value of the parameter useApi to false.

ICliApiHelper

If you create a type which implements the ICliApiHelper interface, you can set an instance as global help provider to the CliApi.Helper property for a customized help output in any case.

The used type may implement ICliApiHelper and/or ICliApiHelpProvider, too.

NOTE: Interfaces implemented in an API type will be used in the first place! The CliApi.Helper instance will only be used, if the API type wasn't determined yet, or it doesn't implement error handling / help providing. In case your CLI app serves a CliHelpApi type, too, it'll be used before calling CliApi.Helper.

CAUTION: Do not call CliApi.DisplayHelpAsync from a ICliApiHelper instance, it may cause an endless loop!

CliHelpApi

Serve the CliHelpApi API type for serving help for APIs/methods/arguments:

# Display a list of possible API names
dotnet app.dll help (-details)

# Display API details
dotnet app.dll help --api [apiName] (-details)

# Display API method details
dotnet app.dll help --api [apiName] --method [methodName] (-details)

The optional -details flag will force the help API to output more available informations.

TIP: Serve the CliHelpApi as the first (and default) API to display the help on any general wrong usage.

Console I/O

If you'd like to support dynamic console input/output formats, you can use the ConsoleIoHelper.GetInput/SendOutput(Async) method for that. They support console input from

  • arguments
  • environment variables
  • user input (also password)
  • STDIN

in the input formats

  • raw binary
  • UTF-8
  • base64
  • hexadecimal
  • byte encoded

and can encode console output to those formats also. Using these helper methods you can support dynamic I/O formats and sources using a single line of code.

Example:

[CliApi]
[StdIn("/path/to/inputFile")]
[StdOut("/path/to/outputFile")]
public int YourApiMethod(
	[CliApi] string? input = null,
	[CliApi] string? output = null,
	[CliApi(ParseJson = true)] ConsoleIoHelper.Format inFormat = ConsoleIoHelper.Format.Base64,
	[CliApi(ParseJson = true)] ConsoleIoHelper.Format outFormat = ConsoleIoHelper.Format.Base64,
	[CliApi] bool useStdIn = false
	)
{
	Stream inputStream = ConsoleIoHelper.GetInput(input, useStdIn, "Input: ", format: inFormat);
	// Now inputStream contains the input data from any source, 
	// converted from UTF-8, base64, hex or byte encoded to raw binary
	return ConsoleIoHelper.SendOutput(outputStream, output, format: outFormat, exitCode: 0);
}

Input from STDIN, output to STDOUT:

dotnet app.dll YourApiMethod -useStdIn < /path/to/inputFile > /path/to/outputFile

Input from a file, output to STDOUT:

dotnet app.dll YourApiMethod --input /path/to/inputFile --inFormat "Binary | File" > /path/to/outputFile

Input from the user (UTF-8 encoded string), output to STDOUT:

dotnet app.dll YourApiMethod --inFormat String > /path/to/outputFile

Input from an environment variable (UTF-8 encoded string), output to STDOUT:

dotnet app.dll YourApiMethod --input VARIABLE_NAME --inFormat "String | Environment" > /path/to/outputFile

Input from STDIN, output base64 encoded to STDOUT:

dotnet app.dll YourApiMethod -useStdIn --outFormat Base64 < /path/to/inputFile > /path/to/outputFile

Input from STDIN, output base64 encoded to file:

dotnet app.dll YourApiMethod -useStdIn --output /path/to/outputFile --outFormat Base64 < /path/to/inputFile

These I/O formats are supported:

  • Binary: raw binary (always used for input files)
  • String: UTF-8 encoded
  • Base64: base64 encoded
  • Hex: hexadecimal encoded
  • ByteEncoded: byte encoded

Input may come from

  • STDIN
  • CLI argument
  • envionment variable
  • file

Output may go to

  • STDOUT
  • file

The ConsoleIoHelper.GetInput method also allows to display a secret user input prompt (see available parameters).

Color profiles

The ConsoleColorProfile allows to provide multiple color profiles which can be configured

  • using CLI arguments (wan24.Core.CliConfig)
  • in a JSON configuration file (CliAppConfig)
  • from code

Example for setting a color profile from CLI arguments:

dotnet app.dll ... --wan24.CLI.ConsoleColorProfile.ApplyRegistered PROFILE

Example for setting a color profile in a JSON configuration:

{
	...
	"ColorProfile": "PROFILE",
	...
}

Example for defining color profiles in a JSON configuration:

{
	...
	"ColorProfiles": [
		{
			"Name": "PROFLE",
			"BackGroundColor": "black",
			...
		},
		...
	]
	...
}

NOTE: Single colors can be overridden in the JSON structure by using the color properties at the root level.

Color profiles willbe applied to the static CliApiInfo color properties.

Localization

wan24-CLI uses the wan24-Core localization helpers. If you want to localize your CLI API help, you can include the wan24-CLI source code and match the keyword source _("...") to the keyword extraction configuration.

All help texts defined as API/method/argument attributes will be translated before they're going to be displayed.

Processing multiple API method calls within one process

Example:

dotnet app.dll --key value - --key value2 - --key value3

A single dash is used to separate API call arguments for one API method call.

How to process the three API method calls:

await CliApi.RunMultiAsync(args);

There are some limitations:

  1. API method calls will be processed sequential (not in parallel)
  2. The first API method which fails or returns an exit code !=0 will break the processing loop
  3. API calls without any argument aren't supported and will be ignored
  4. Since a single dash is used as argument separator, it can't be used as argument for your APIs

Dash and double dash handling

A single dash is a nameless flag, while a double dash is a nameless key which requires a value to follow.

Double name apperance handling

A repeated flag will be ignored, while a repeated key which required a value creates a value list. An API (method/arguments) name should be unique - otherwise the APIs (methods/arguments) would overwrite each other.

CLI API reflection

FrozenDictionary<string, CliApiInfo> apiInfos = 
	CliApiInfo.Create(typeof(YourCliApi), typeof(CliHelpApi), ...);

The CliApiInfosExtensions have some useful helper methods. Using the ReflectionExtensions and CliApiContext methods you may also reflect .NET reflection info objects or a CLI API context instance for CLI API object detail informations.

Header output

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:

CliApi.CommandLine = "dotnet tool run yourapp";

Best practice

You use this library, 'cause it matches your requirements (which Spectre.Console alone does not in some cases). You can work work the .NET Console methods, but since this library references Spectre.Console you could enrich your CLI app with formatted console output easily, if your app runs within an ANSI console.

Tips:

  • Create one API type for methods which work with one entity and may share argument definitions
  • Use API type properties for defining CLI arguments
  • Use ICliArguments object parameters for encapsulating API method arguments within an arguments type, if using API type properties is not an option
  • Use data annotations for ensuring valid arguments (the ObjectValidation NuGet package is being used for deep object validations, if CLI arguments are being stored in properties)