mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
@@ -26,7 +26,7 @@ namespace CliFx.Tests
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_HelpAndVersion_RunAsync()
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Smoke()
|
||||
{
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
@@ -53,6 +53,21 @@ namespace CliFx.Tests
|
||||
new string[0]
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"--help"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"--version"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(NamedCommand)},
|
||||
new[] {"cmd", "-h"}
|
||||
@@ -72,6 +87,21 @@ namespace CliFx.Tests
|
||||
new[] {typeof(FaultyCommand3)},
|
||||
new[] {"faulty3", "-h"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(DefaultCommand)},
|
||||
new[] {"[preview]"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand1)},
|
||||
new[] {"faulty1", "[preview]"}
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {typeof(FaultyCommand1)},
|
||||
new[] {"faulty1", "[preview]", "-o", "value"}
|
||||
);
|
||||
}
|
||||
|
||||
private static IEnumerable<TestCaseData> GetTestCases_RunAsync_Negative()
|
||||
@@ -126,8 +156,8 @@ namespace CliFx.Tests
|
||||
}
|
||||
|
||||
[Test]
|
||||
[TestCaseSource(nameof(GetTestCases_HelpAndVersion_RunAsync))]
|
||||
public async Task RunAsync_HelpAndVersion_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||
[TestCaseSource(nameof(GetTestCases_RunAsync_Smoke))]
|
||||
public async Task RunAsync_Smoke_Test(IReadOnlyList<Type> commandTypes, IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
// Arrange
|
||||
using (var stdout = new StringWriter())
|
||||
|
||||
@@ -165,6 +165,40 @@ namespace CliFx.Tests.Services
|
||||
new CommandOptionInput("option", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]"},
|
||||
new CommandInput(null,
|
||||
new[] {"debug"},
|
||||
new CommandOptionInput[0])
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]", "[preview]"},
|
||||
new CommandInput(null,
|
||||
new[] {"debug", "preview"},
|
||||
new CommandOptionInput[0])
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"[debug]", "[preview]", "-o", "value"},
|
||||
new CommandInput(null,
|
||||
new[] {"debug", "preview"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
|
||||
yield return new TestCaseData(
|
||||
new[] {"command", "[debug]", "[preview]", "-o", "value"},
|
||||
new CommandInput("command",
|
||||
new[] {"debug", "preview"},
|
||||
new[]
|
||||
{
|
||||
new CommandOptionInput("o", "value")
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Exceptions;
|
||||
@@ -49,12 +50,48 @@ namespace CliFx
|
||||
|
||||
try
|
||||
{
|
||||
// Get schemas for all available command types
|
||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||
|
||||
// Parse command input from arguments
|
||||
var commandInput = _commandInputParser.ParseCommandInput(commandLineArguments);
|
||||
|
||||
// Wait for debugger to be attached if debug mode is requested
|
||||
if (_configuration.IsDebugModeAllowed && commandInput.IsDebugModeRequested())
|
||||
{
|
||||
// Whoever comes up with an idea on how to cover this in tests is a genius
|
||||
|
||||
_console.WithForegroundColor(ConsoleColor.Green,
|
||||
() => _console.Output.WriteLine($"Attach debugger to PID {Process.GetCurrentProcess().Id} to continue."));
|
||||
|
||||
while (!Debugger.IsAttached)
|
||||
await Task.Delay(100);
|
||||
}
|
||||
|
||||
// Show parse results if preview mode is requested
|
||||
if (_configuration.IsPreviewModeAllowed && commandInput.IsPreviewModeRequested())
|
||||
{
|
||||
_console.Output.WriteLine($"Command name: {commandInput.CommandName}");
|
||||
_console.Output.WriteLine();
|
||||
|
||||
_console.Output.WriteLine("Directives:");
|
||||
foreach (var directive in commandInput.Directives)
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.WriteLine(directive);
|
||||
}
|
||||
_console.Output.WriteLine();
|
||||
|
||||
_console.Output.WriteLine("Options:");
|
||||
foreach (var option in commandInput.Options)
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.WriteLine(option);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Get schemas for all available command types
|
||||
var availableCommandSchemas = _commandSchemaResolver.GetCommandSchemas(_configuration.CommandTypes);
|
||||
|
||||
// Find command schema matching the name specified in the input
|
||||
var targetCommandSchema = availableCommandSchemas.FindByName(commandInput.CommandName);
|
||||
|
||||
@@ -94,7 +131,7 @@ namespace CliFx
|
||||
return isError ? -1 : 0;
|
||||
}
|
||||
|
||||
// Show version if it was requested without specifying a command
|
||||
// Show version if it was requested and command wasn't specified
|
||||
if (commandInput.IsVersionRequested() && !commandInput.IsCommandSpecified())
|
||||
{
|
||||
_console.Output.WriteLine(_metadata.VersionText);
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace CliFx
|
||||
{
|
||||
private readonly HashSet<Type> _commandTypes = new HashSet<Type>();
|
||||
|
||||
private bool _isDebugModeAllowed = true;
|
||||
private bool _isPreviewModeAllowed = true;
|
||||
private string _title;
|
||||
private string _executableName;
|
||||
private string _versionText;
|
||||
@@ -50,6 +52,20 @@ namespace CliFx
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AllowDebugMode(bool isAllowed = true)
|
||||
{
|
||||
_isDebugModeAllowed = isAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true)
|
||||
{
|
||||
_isPreviewModeAllowed = isAllowed;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ICliApplicationBuilder UseTitle(string title)
|
||||
{
|
||||
@@ -148,7 +164,7 @@ namespace CliFx
|
||||
|
||||
// Project parameters to expected types
|
||||
var metadata = new ApplicationMetadata(_title, _executableName, _versionText, _description);
|
||||
var configuration = new ApplicationConfiguration(_commandTypes.ToArray());
|
||||
var configuration = new ApplicationConfiguration(_commandTypes.ToArray(), _isDebugModeAllowed, _isPreviewModeAllowed);
|
||||
|
||||
return new CliApplication(metadata, configuration,
|
||||
_console, new CommandInputParser(), new CommandSchemaResolver(),
|
||||
|
||||
@@ -19,6 +19,16 @@ namespace CliFx
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AddCommandsFrom(Assembly commandAssembly);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether debug mode (enabled with [debug] directive) is allowed in the application.
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AllowDebugMode(bool isAllowed = true);
|
||||
|
||||
/// <summary>
|
||||
/// Specifies whether preview mode (enabled with [preview] directive) is allowed in the application.
|
||||
/// </summary>
|
||||
ICliApplicationBuilder AllowPreviewMode(bool isAllowed = true);
|
||||
|
||||
/// <summary>
|
||||
/// Sets application title, which appears in the help text.
|
||||
/// </summary>
|
||||
|
||||
@@ -10,16 +10,29 @@ namespace CliFx.Models
|
||||
public class ApplicationConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Command types defined in the application.
|
||||
/// Command types defined in this application.
|
||||
/// </summary>
|
||||
public IReadOnlyList<Type> CommandTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether debug mode is allowed in this application.
|
||||
/// </summary>
|
||||
public bool IsDebugModeAllowed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether preview mode is allowed in this application.
|
||||
/// </summary>
|
||||
public bool IsPreviewModeAllowed { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="ApplicationConfiguration"/>.
|
||||
/// </summary>
|
||||
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes)
|
||||
public ApplicationConfiguration(IReadOnlyList<Type> commandTypes,
|
||||
bool isDebugModeAllowed, bool isPreviewModeAllowed)
|
||||
{
|
||||
CommandTypes = commandTypes.GuardNotNull(nameof(commandTypes));
|
||||
IsDebugModeAllowed = isDebugModeAllowed;
|
||||
IsPreviewModeAllowed = isPreviewModeAllowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,11 @@ namespace CliFx.Models
|
||||
/// </summary>
|
||||
public string CommandName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specified directives.
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> Directives { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Specified options.
|
||||
/// </summary>
|
||||
@@ -23,12 +28,21 @@ namespace CliFx.Models
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options)
|
||||
public CommandInput(string commandName, IReadOnlyList<string> directives, IReadOnlyList<CommandOptionInput> options)
|
||||
{
|
||||
CommandName = commandName; // can be null
|
||||
Directives = directives.GuardNotNull(nameof(directives));
|
||||
Options = options.GuardNotNull(nameof(options));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string commandName, IReadOnlyList<CommandOptionInput> options)
|
||||
: this(commandName, EmptyDirectives, options)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
@@ -41,15 +55,7 @@ namespace CliFx.Models
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput(string commandName)
|
||||
: this(commandName, new CommandOptionInput[0])
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes an instance of <see cref="CommandInput"/>.
|
||||
/// </summary>
|
||||
public CommandInput()
|
||||
: this(null, new CommandOptionInput[0])
|
||||
: this(commandName, EmptyOptions)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -73,9 +79,12 @@ namespace CliFx.Models
|
||||
|
||||
public partial class CommandInput
|
||||
{
|
||||
private static readonly IReadOnlyList<string> EmptyDirectives = new string[0];
|
||||
private static readonly IReadOnlyList<CommandOptionInput> EmptyOptions = new CommandOptionInput[0];
|
||||
|
||||
/// <summary>
|
||||
/// Empty input.
|
||||
/// </summary>
|
||||
public static CommandInput Empty { get; } = new CommandInput();
|
||||
public static CommandInput Empty { get; } = new CommandInput(EmptyOptions);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ namespace CliFx.Models
|
||||
/// <summary>
|
||||
/// Parsed option from command line input.
|
||||
/// </summary>
|
||||
public class CommandOptionInput
|
||||
public partial class CommandOptionInput
|
||||
{
|
||||
/// <summary>
|
||||
/// Specified option alias.
|
||||
@@ -40,7 +40,7 @@ namespace CliFx.Models
|
||||
/// Initializes an instance of <see cref="CommandOptionInput"/>.
|
||||
/// </summary>
|
||||
public CommandOptionInput(string alias)
|
||||
: this(alias, new string[0])
|
||||
: this(alias, EmptyValues)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -70,4 +70,9 @@ namespace CliFx.Models
|
||||
return buffer.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public partial class CommandOptionInput
|
||||
{
|
||||
private static readonly IReadOnlyList<string> EmptyValues = new string[0];
|
||||
}
|
||||
}
|
||||
@@ -108,6 +108,24 @@ namespace CliFx.Models
|
||||
return !commandInput.CommandName.IsNullOrWhiteSpace();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether debug mode was requested in the input.
|
||||
/// </summary>
|
||||
public static bool IsDebugModeRequested(this CommandInput commandInput)
|
||||
{
|
||||
commandInput.GuardNotNull(nameof(commandInput));
|
||||
return commandInput.Directives.Contains("debug", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether preview mode was requested in the input.
|
||||
/// </summary>
|
||||
public static bool IsPreviewModeRequested(this CommandInput commandInput)
|
||||
{
|
||||
commandInput.GuardNotNull(nameof(commandInput));
|
||||
return commandInput.Directives.Contains("preview", StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets whether help was requested in the input.
|
||||
/// </summary>
|
||||
@@ -116,7 +134,6 @@ namespace CliFx.Models
|
||||
commandInput.GuardNotNull(nameof(commandInput));
|
||||
|
||||
var firstOption = commandInput.Options.FirstOrDefault();
|
||||
|
||||
return firstOption != null && CommandOptionSchema.HelpOption.MatchesAlias(firstOption.Alias);
|
||||
}
|
||||
|
||||
@@ -128,7 +145,6 @@ namespace CliFx.Models
|
||||
commandInput.GuardNotNull(nameof(commandInput));
|
||||
|
||||
var firstOption = commandInput.Options.FirstOrDefault();
|
||||
|
||||
return firstOption != null && CommandOptionSchema.VersionOption.MatchesAlias(firstOption.Alias);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,10 @@ namespace CliFx.Services
|
||||
commandLineArguments.GuardNotNull(nameof(commandLineArguments));
|
||||
|
||||
var commandNameBuilder = new StringBuilder();
|
||||
var directives = new List<string>();
|
||||
var optionsDic = new Dictionary<string, List<string>>();
|
||||
|
||||
// Option aliases and values are parsed in pairs so we need to keep track of last alias
|
||||
var lastOptionAlias = "";
|
||||
|
||||
foreach (var commandLineArgument in commandLineArguments)
|
||||
@@ -34,7 +36,7 @@ namespace CliFx.Services
|
||||
optionsDic[lastOptionAlias] = new List<string>();
|
||||
}
|
||||
|
||||
// Encountered short option name or multiple thereof
|
||||
// Encountered short option name or multiple short option names
|
||||
else if (commandLineArgument.StartsWith("-", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Handle stacked options
|
||||
@@ -48,11 +50,22 @@ namespace CliFx.Services
|
||||
}
|
||||
}
|
||||
|
||||
// Encountered command name or part thereof
|
||||
// Encountered directive or (part of) command name
|
||||
else if (lastOptionAlias.IsNullOrWhiteSpace())
|
||||
{
|
||||
commandNameBuilder.AppendIfNotEmpty(' ');
|
||||
commandNameBuilder.Append(commandLineArgument);
|
||||
if (commandLineArgument.StartsWith("[", StringComparison.OrdinalIgnoreCase) &&
|
||||
commandLineArgument.EndsWith("]", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Extract directive
|
||||
var directive = commandLineArgument.Substring(1, commandLineArgument.Length - 2);
|
||||
|
||||
directives.Add(directive);
|
||||
}
|
||||
else
|
||||
{
|
||||
commandNameBuilder.AppendIfNotEmpty(' ');
|
||||
commandNameBuilder.Append(commandLineArgument);
|
||||
}
|
||||
}
|
||||
|
||||
// Encountered option value
|
||||
@@ -65,7 +78,7 @@ namespace CliFx.Services
|
||||
var commandName = commandNameBuilder.Length > 0 ? commandNameBuilder.ToString() : null;
|
||||
var options = optionsDic.Select(p => new CommandOptionInput(p.Key, p.Value)).ToArray();
|
||||
|
||||
return new CommandInput(commandName, options);
|
||||
return new CommandInput(commandName, directives, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
24
Readme.md
24
Readme.md
@@ -21,13 +21,12 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
|
||||
|
||||
- Complete application framework, not just an argument parser
|
||||
- Requires minimal amount of code to get started
|
||||
- Resolves commands using attributes
|
||||
- Resolves commands and options using attributes
|
||||
- Handles options of various types, including custom types
|
||||
- Supports multi-level command hierarchies
|
||||
- Generates contextual help text
|
||||
- Prints errors and routes exit codes on exceptions
|
||||
- Highly testable and easy to customize
|
||||
- Contains utilties such as progress reporting
|
||||
- Highly testable and easy to debug
|
||||
- Targets .NET Framework 4.5+ and .NET Standard 2.0+
|
||||
- No external dependencies
|
||||
|
||||
@@ -36,7 +35,6 @@ _CliFx is to command line interfaces what ASP.NET Core is to web applications._
|
||||
- Positional arguments (anonymous options)
|
||||
- Auto-completion support
|
||||
- Environment variables
|
||||
- Runtime directives
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -360,6 +358,24 @@ public async Task ConcatCommand_Test()
|
||||
}
|
||||
```
|
||||
|
||||
### Debug and preview mode
|
||||
|
||||
When troubleshooting issues, you may find it useful to run your app in debug or preview mode. To do it, simply pass the corresponding directive to your app along with other command line arguments, e.g.: `myapp [debug] user add -n "John Doe" -e john.doe@example.com`
|
||||
|
||||
If your application is ran in debug mode (`[debug]` directive), it will wait for debugger to be attached before proceeding. This is useful for debugging apps that were ran outside of your IDE.
|
||||
|
||||
If preview mode is specified (`[preview]` directive), the app will print consumed command line arguments as they were parsed. This is useful when troubleshooting issues related to option parsing.
|
||||
|
||||
You can also disallow these directives, e.g. when running in production, by calling `AllowDebugMode` and `AllowPreviewMode` methods on `CliApplicationBuilder`.
|
||||
|
||||
```c#
|
||||
var app = new CliApplicationBuilder()
|
||||
.AddCommandsFromThisAssembly()
|
||||
.AllowDebugMode(true) // allow debug mode
|
||||
.AllowPreviewMode(false) // disallow preview mode
|
||||
.Build();
|
||||
```
|
||||
|
||||
## Benchmarks
|
||||
|
||||
CliFx has the smallest performance overhead compared to other command line parsers and frameworks.
|
||||
|
||||
Reference in New Issue
Block a user