mirror of
https://github.com/Tyrrrz/CliFx.git
synced 2025-10-25 15:19:17 +00:00
Add console abstraction, remove CommandContext
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Benchmarks.Commands
|
||||
{
|
||||
@@ -16,6 +16,6 @@ namespace CliFx.Benchmarks.Commands
|
||||
[CommandOption("bool", 'b')]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
@@ -15,7 +14,7 @@ namespace CliFx.Tests.Dummy.Commands
|
||||
[CommandOption('e', Description = "Whether the greeting should be exclaimed.")]
|
||||
public bool IsExclaimed { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
@@ -24,7 +23,7 @@ namespace CliFx.Tests.Dummy.Commands
|
||||
if (IsExclaimed)
|
||||
buffer.Append('!');
|
||||
|
||||
context.Output.WriteLine(buffer.ToString());
|
||||
console.Output.WriteLine(buffer.ToString());
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
@@ -15,10 +14,10 @@ namespace CliFx.Tests.Dummy.Commands
|
||||
[CommandOption("base", 'b', Description = "Logarithm base.")]
|
||||
public double Base { get; set; } = 10;
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var result = Math.Log(Value, Base);
|
||||
context.Output.WriteLine(result);
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Tests.Dummy.Commands
|
||||
@@ -13,10 +12,10 @@ namespace CliFx.Tests.Dummy.Commands
|
||||
[CommandOption("values", 'v', IsRequired = true, Description = "Input values.")]
|
||||
public IReadOnlyList<double> Values { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
public Task ExecuteAsync(IConsole console)
|
||||
{
|
||||
var result = Values.Sum();
|
||||
context.Output.WriteLine(result);
|
||||
console.Output.WriteLine(result);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Exceptions;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
|
||||
@@ -14,19 +14,19 @@ namespace CliFx.Tests
|
||||
[Command]
|
||||
private class TestDefaultCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command("command")]
|
||||
private class TestNamedCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => Task.CompletedTask;
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Command("faulty command")]
|
||||
private class TestFaultyCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => Task.FromException(new CommandErrorException(-1337));
|
||||
public Task ExecuteAsync(IConsole console) => Task.FromException(new CommandErrorException(-1337));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
using FluentAssertions;
|
||||
using NUnit.Framework;
|
||||
@@ -14,7 +13,7 @@ namespace CliFx.Tests
|
||||
[Command]
|
||||
private class TestCommand : ICommand
|
||||
{
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +31,9 @@ namespace CliFx.Tests
|
||||
{
|
||||
// Arrange
|
||||
var factory = new CommandFactory();
|
||||
var schema = new CommandSchemaResolver().GetCommandSchema(commandType);
|
||||
|
||||
// Act
|
||||
var command = factory.CreateCommand(schema);
|
||||
var command = factory.CreateCommand(commandType);
|
||||
|
||||
// Assert
|
||||
command.Should().BeOfType(commandType);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Attributes;
|
||||
@@ -27,7 +26,7 @@ namespace CliFx.Tests
|
||||
[CommandOption("bool", 'b', GroupName = "other-group")]
|
||||
public bool BoolOption { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ namespace CliFx.Tests
|
||||
[CommandOption("option-c", Description = "Option C description")]
|
||||
public bool OptionC { get; set; }
|
||||
|
||||
public Task ExecuteAsync(CommandContext context) => throw new NotImplementedException();
|
||||
public Task ExecuteAsync(IConsole console) => Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,29 +15,33 @@ namespace CliFx
|
||||
{
|
||||
private readonly ApplicationMetadata _applicationMetadata;
|
||||
private readonly IReadOnlyList<Type> _commandTypes;
|
||||
|
||||
private readonly IConsole _console;
|
||||
private readonly ICommandInputParser _commandInputParser;
|
||||
private readonly ICommandSchemaResolver _commandSchemaResolver;
|
||||
private readonly ICommandFactory _commandFactory;
|
||||
private readonly ICommandInitializer _commandInitializer;
|
||||
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
|
||||
private readonly ICommandHelpTextRenderer _commandHelpTextRenderer;
|
||||
|
||||
public CliApplication(ApplicationMetadata applicationMetadata, IReadOnlyList<Type> commandTypes,
|
||||
ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
|
||||
ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextBuilder commandHelpTextBuilder)
|
||||
IConsole console, ICommandInputParser commandInputParser, ICommandSchemaResolver commandSchemaResolver,
|
||||
ICommandFactory commandFactory, ICommandInitializer commandInitializer, ICommandHelpTextRenderer commandHelpTextRenderer)
|
||||
{
|
||||
_applicationMetadata = applicationMetadata;
|
||||
_commandTypes = commandTypes;
|
||||
|
||||
_console = console;
|
||||
_commandInputParser = commandInputParser;
|
||||
_commandSchemaResolver = commandSchemaResolver;
|
||||
_commandFactory = commandFactory;
|
||||
_commandInitializer = commandInitializer;
|
||||
_commandHelpTextBuilder = commandHelpTextBuilder;
|
||||
_commandHelpTextRenderer = commandHelpTextRenderer;
|
||||
}
|
||||
|
||||
public CliApplication(ApplicationMetadata applicationMetadata, IReadOnlyList<Type> commandTypes)
|
||||
: this(applicationMetadata, commandTypes,
|
||||
new CommandInputParser(), new CommandSchemaResolver(), new CommandFactory(),
|
||||
new CommandInitializer(), new CommandHelpTextBuilder())
|
||||
new SystemConsole(), new CommandInputParser(), new CommandSchemaResolver(),
|
||||
new CommandFactory(), new CommandInitializer(), new CommandHelpTextRenderer())
|
||||
{
|
||||
}
|
||||
|
||||
@@ -75,9 +79,6 @@ namespace CliFx
|
||||
|
||||
public async Task<int> RunAsync(IReadOnlyList<string> commandLineArguments)
|
||||
{
|
||||
var stdOut = ConsoleWriter.GetStandardOutput();
|
||||
var stdErr = ConsoleWriter.GetStandardError();
|
||||
|
||||
try
|
||||
{
|
||||
var commandInput = _commandInputParser.ParseInput(commandLineArguments);
|
||||
@@ -88,65 +89,60 @@ namespace CliFx
|
||||
// Fail if there are no commands defined
|
||||
if (!availableCommandSchemas.Any())
|
||||
{
|
||||
stdErr.WriteLine("There are no commands defined in this application.");
|
||||
_console.WithColor(ConsoleColor.Red,
|
||||
c => c.Error.WriteLine("There are no commands defined in this application."));
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Fail if specified a command which is not defined
|
||||
if (!commandInput.CommandName.IsNullOrWhiteSpace() && matchingCommandSchema == null)
|
||||
{
|
||||
stdErr.WriteLine($"Specified command [{commandInput.CommandName}] is not defined.");
|
||||
return -1;
|
||||
}
|
||||
_console.WithColor(ConsoleColor.Red,
|
||||
c => c.Error.WriteLine($"Specified command [{commandInput.CommandName}] is not defined."));
|
||||
|
||||
// Use a stub if command was not specified but there is no default command defined
|
||||
if (matchingCommandSchema == null)
|
||||
{
|
||||
matchingCommandSchema = _commandSchemaResolver.GetCommandSchema(typeof(StubDefaultCommand));
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Show version if it was requested without specifying a command
|
||||
if (IsVersionRequested(commandInput) && commandInput.CommandName.IsNullOrWhiteSpace())
|
||||
{
|
||||
stdOut.WriteLine(_applicationMetadata.VersionText);
|
||||
_console.Output.WriteLine(_applicationMetadata.VersionText);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Show help if it was requested
|
||||
if (IsHelpRequested(commandInput))
|
||||
{
|
||||
var helpText = _commandHelpTextBuilder.Build(_applicationMetadata, availableCommandSchemas, matchingCommandSchema);
|
||||
stdOut.WriteLine(helpText);
|
||||
_commandHelpTextRenderer.RenderHelpText(_applicationMetadata, availableCommandSchemas, matchingCommandSchema);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Show help if command wasn't specified but a default command isn't defined
|
||||
if (commandInput.CommandName.IsNullOrWhiteSpace() && matchingCommandSchema == null)
|
||||
{
|
||||
_commandHelpTextRenderer.RenderHelpText(_applicationMetadata, availableCommandSchemas);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Create an instance of the command
|
||||
var command = matchingCommandSchema.Type == typeof(StubDefaultCommand)
|
||||
? new StubDefaultCommand(_commandHelpTextBuilder)
|
||||
: _commandFactory.CreateCommand(matchingCommandSchema);
|
||||
var command = _commandFactory.CreateCommand(matchingCommandSchema.Type);
|
||||
|
||||
// Populate command with options according to its schema
|
||||
_commandInitializer.InitializeCommand(command, matchingCommandSchema, commandInput);
|
||||
|
||||
// Create context and execute command
|
||||
var commandContext = new CommandContext(_applicationMetadata,
|
||||
availableCommandSchemas, matchingCommandSchema,
|
||||
commandInput, stdOut, stdErr);
|
||||
|
||||
await command.ExecuteAsync(commandContext);
|
||||
await command.ExecuteAsync(_console);
|
||||
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stdErr.WriteLine(ex.ToString());
|
||||
_console.WithColor(ConsoleColor.Red, c => c.Error.WriteLine(ex));
|
||||
|
||||
return ex is CommandErrorException errorException ? errorException.ExitCode : -1;
|
||||
}
|
||||
finally
|
||||
{
|
||||
stdOut.Dispose();
|
||||
stdErr.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +153,7 @@ namespace CliFx
|
||||
// Entry assembly is null in tests
|
||||
var entryAssembly = Assembly.GetEntryAssembly();
|
||||
|
||||
var title = entryAssembly?.GetName().FullName ?? "App";
|
||||
var title = entryAssembly?.GetName().Name ?? "App";
|
||||
var executableName = Path.GetFileNameWithoutExtension(entryAssembly?.Location) ?? "app";
|
||||
var versionText = entryAssembly?.GetName().Version.ToString() ?? "1.0";
|
||||
|
||||
@@ -174,26 +170,5 @@ namespace CliFx
|
||||
|
||||
return entryAssembly.ExportedTypes.Where(t => t.Implements(typeof(ICommand))).ToArray();
|
||||
}
|
||||
|
||||
private sealed class StubDefaultCommand : ICommand
|
||||
{
|
||||
private readonly ICommandHelpTextBuilder _commandHelpTextBuilder;
|
||||
|
||||
public StubDefaultCommand(ICommandHelpTextBuilder commandHelpTextBuilder)
|
||||
{
|
||||
_commandHelpTextBuilder = commandHelpTextBuilder;
|
||||
}
|
||||
|
||||
public Task ExecuteAsync(CommandContext context)
|
||||
{
|
||||
var helpText = _commandHelpTextBuilder.Build(context.ApplicationMetadata,
|
||||
context.AvailableCommandSchemas,
|
||||
context.MatchingCommandSchema);
|
||||
|
||||
context.Output.WriteLine(helpText);
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
using System.Threading.Tasks;
|
||||
using CliFx.Models;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx
|
||||
{
|
||||
public interface ICommand
|
||||
{
|
||||
Task ExecuteAsync(CommandContext context);
|
||||
Task ExecuteAsync(IConsole console);
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
using System.Collections.Generic;
|
||||
using CliFx.Services;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public class CommandContext
|
||||
{
|
||||
public ApplicationMetadata ApplicationMetadata { get; }
|
||||
|
||||
public IReadOnlyList<CommandSchema> AvailableCommandSchemas { get; }
|
||||
|
||||
public CommandSchema MatchingCommandSchema { get; }
|
||||
|
||||
public CommandInput CommandInput { get; }
|
||||
|
||||
public IConsoleWriter Output { get; }
|
||||
|
||||
public IConsoleWriter Error { get; }
|
||||
|
||||
public CommandContext(ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema,
|
||||
CommandInput commandInput, IConsoleWriter output, IConsoleWriter error)
|
||||
{
|
||||
ApplicationMetadata = applicationMetadata;
|
||||
AvailableCommandSchemas = availableCommandSchemas;
|
||||
MatchingCommandSchema = matchingCommandSchema;
|
||||
CommandInput = commandInput;
|
||||
Output = output;
|
||||
Error = error;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
|
||||
@@ -37,12 +36,15 @@ namespace CliFx.Models
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
if (!CommandName.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(CommandName);
|
||||
buffer.Append(' ');
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append(option);
|
||||
buffer.Append(' ');
|
||||
}
|
||||
|
||||
return buffer.Trim().ToString();
|
||||
|
||||
@@ -28,14 +28,17 @@ namespace CliFx.Models
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
if (!Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(Name);
|
||||
buffer.Append(' ');
|
||||
}
|
||||
|
||||
foreach (var option in Options)
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append('[');
|
||||
buffer.Append(option);
|
||||
buffer.Append(']');
|
||||
buffer.Append(' ');
|
||||
}
|
||||
|
||||
return buffer.Trim().ToString();
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
using System.Drawing;
|
||||
|
||||
namespace CliFx.Models
|
||||
{
|
||||
public class TextSpan
|
||||
{
|
||||
public string Text { get; }
|
||||
|
||||
public Color Color { get; }
|
||||
|
||||
public TextSpan(string text, Color color)
|
||||
{
|
||||
Text = text;
|
||||
Color = color;
|
||||
}
|
||||
|
||||
public TextSpan(string text)
|
||||
: this(text, Color.Gray)
|
||||
{
|
||||
}
|
||||
|
||||
public override string ToString() => Text;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class CommandFactory : ICommandFactory
|
||||
{
|
||||
public ICommand CreateCommand(CommandSchema schema) => (ICommand) Activator.CreateInstance(schema.Type);
|
||||
public ICommand CreateCommand(Type commandType) => (ICommand) Activator.CreateInstance(commandType);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
// TODO: add color
|
||||
public class CommandHelpTextBuilder : ICommandHelpTextBuilder
|
||||
{
|
||||
private IReadOnlyList<string> GetOptionAliasesWithPrefixes(CommandOptionSchema optionSchema)
|
||||
{
|
||||
var result = new List<string>();
|
||||
|
||||
if (!optionSchema.Name.IsNullOrWhiteSpace())
|
||||
result.Add("--" + optionSchema.Name);
|
||||
|
||||
if (optionSchema.ShortName != null)
|
||||
result.Add("-" + optionSchema.ShortName.Value);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private IReadOnlyList<CommandSchema> GetChildCommandSchemas(IReadOnlyList<CommandSchema> availableCommandSchemas,
|
||||
CommandSchema parentCommandSchema)
|
||||
{
|
||||
// TODO: this doesn't really work properly, it shows all descendants instead of direct children
|
||||
var prefix = !parentCommandSchema.Name.IsNullOrWhiteSpace() ? parentCommandSchema.Name + " " : "";
|
||||
|
||||
return availableCommandSchemas
|
||||
.Where(c => !c.Name.IsNullOrWhiteSpace() && c.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToArray();
|
||||
}
|
||||
|
||||
private void AddDescription(StringBuilder buffer, CommandSchema commands)
|
||||
{
|
||||
if (commands.Description.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Description:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.AppendLine(commands.Description);
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
private void AddUsage(StringBuilder buffer, ApplicationMetadata applicationMetadata, CommandSchema command,
|
||||
IReadOnlyList<CommandSchema> subCommands)
|
||||
{
|
||||
buffer.AppendLine("Usage:");
|
||||
|
||||
buffer.Append(" ");
|
||||
buffer.Append(applicationMetadata.ExecutableName);
|
||||
|
||||
if (!command.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append(command.Name);
|
||||
}
|
||||
|
||||
if (subCommands.Any())
|
||||
{
|
||||
buffer.Append(' ');
|
||||
buffer.Append("[command]");
|
||||
}
|
||||
|
||||
buffer.Append(' ');
|
||||
buffer.Append("[options]");
|
||||
|
||||
buffer.AppendLine().AppendLine();
|
||||
}
|
||||
|
||||
private void AddOptions(StringBuilder buffer, CommandSchema command)
|
||||
{
|
||||
buffer.AppendLine("Options:");
|
||||
|
||||
foreach (var option in command.Options)
|
||||
{
|
||||
buffer.Append(option.IsRequired ? "* " : " ");
|
||||
|
||||
buffer.Append(GetOptionAliasesWithPrefixes(option).JoinToString("|"));
|
||||
|
||||
if (!option.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append(option.Description);
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
// Help option
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append("--help|-h");
|
||||
buffer.Append(" ");
|
||||
buffer.Append("Shows helps text.");
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
// Version option
|
||||
if (command.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append("--version");
|
||||
buffer.Append(" ");
|
||||
buffer.Append("Shows application version.");
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
private void AddSubCommands(StringBuilder buffer, IReadOnlyList<CommandSchema> subCommands)
|
||||
{
|
||||
if (!subCommands.Any())
|
||||
return;
|
||||
|
||||
buffer.AppendLine("Commands:");
|
||||
|
||||
foreach (var command in subCommands)
|
||||
{
|
||||
buffer.Append(" ");
|
||||
|
||||
buffer.Append(command.Name);
|
||||
|
||||
if (!command.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(" ");
|
||||
buffer.Append(command.Description);
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
public string Build(ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas,
|
||||
CommandSchema matchingCommandSchema)
|
||||
{
|
||||
var childCommandSchemas = GetChildCommandSchemas(availableCommandSchemas, matchingCommandSchema);
|
||||
|
||||
var buffer = new StringBuilder();
|
||||
|
||||
if (matchingCommandSchema.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
buffer.Append(applicationMetadata.Title);
|
||||
buffer.Append(" v");
|
||||
buffer.Append(applicationMetadata.VersionText);
|
||||
buffer.AppendLine().AppendLine();
|
||||
}
|
||||
|
||||
AddDescription(buffer, matchingCommandSchema);
|
||||
AddUsage(buffer, applicationMetadata, matchingCommandSchema, childCommandSchemas);
|
||||
AddOptions(buffer, matchingCommandSchema);
|
||||
AddSubCommands(buffer, childCommandSchemas);
|
||||
|
||||
if (matchingCommandSchema.Name.IsNullOrWhiteSpace() && childCommandSchemas.Any())
|
||||
{
|
||||
buffer.Append("You can run ");
|
||||
buffer.Append('`').Append(applicationMetadata.ExecutableName).Append(" [command] --help").Append('`');
|
||||
buffer.Append(" to show help on a specific command.");
|
||||
buffer.AppendLine();
|
||||
}
|
||||
|
||||
return buffer.ToString().Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
210
CliFx/Services/CommandHelpTextRenderer.cs
Normal file
210
CliFx/Services/CommandHelpTextRenderer.cs
Normal file
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using CliFx.Internal;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public partial class CommandHelpTextRenderer : ICommandHelpTextRenderer
|
||||
{
|
||||
private readonly IConsole _console;
|
||||
|
||||
public CommandHelpTextRenderer(IConsole console)
|
||||
{
|
||||
_console = console;
|
||||
}
|
||||
|
||||
public CommandHelpTextRenderer()
|
||||
: this(new SystemConsole())
|
||||
{
|
||||
}
|
||||
|
||||
public void RenderHelpText(ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema = null) =>
|
||||
new RenderHelpTextImpl(_console, applicationMetadata, availableCommandSchemas, matchingCommandSchema).Render();
|
||||
}
|
||||
|
||||
public partial class CommandHelpTextRenderer
|
||||
{
|
||||
private class RenderHelpTextImpl
|
||||
{
|
||||
private readonly IConsole _console;
|
||||
private readonly ApplicationMetadata _applicationMetadata;
|
||||
private readonly IReadOnlyList<CommandSchema> _availableCommandSchemas;
|
||||
private readonly CommandSchema _matchingCommandSchema;
|
||||
|
||||
private readonly IReadOnlyList<CommandSchema> _childCommandSchemas;
|
||||
|
||||
public RenderHelpTextImpl(IConsole console, ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema)
|
||||
{
|
||||
_console = console;
|
||||
_applicationMetadata = applicationMetadata;
|
||||
_availableCommandSchemas = availableCommandSchemas;
|
||||
_matchingCommandSchema = matchingCommandSchema;
|
||||
|
||||
_childCommandSchemas = GetChildCommandSchemas();
|
||||
}
|
||||
|
||||
private IReadOnlyList<CommandSchema> GetChildCommandSchemas()
|
||||
{
|
||||
// TODO:
|
||||
var prefix = _matchingCommandSchema == null || _matchingCommandSchema.Name.IsNullOrWhiteSpace()
|
||||
? ""
|
||||
: _matchingCommandSchema.Name + " ";
|
||||
|
||||
return new CommandSchema[0];
|
||||
}
|
||||
|
||||
private void RenderAppInfo()
|
||||
{
|
||||
if (_matchingCommandSchema != null && !_matchingCommandSchema.Name.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
_console.Output.Write(_applicationMetadata.Title);
|
||||
_console.Output.Write(" v");
|
||||
_console.Output.Write(_applicationMetadata.VersionText);
|
||||
_console.Output.WriteLine();
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
private void RenderDescription()
|
||||
{
|
||||
if (_matchingCommandSchema == null || _matchingCommandSchema.Description.IsNullOrWhiteSpace())
|
||||
return;
|
||||
|
||||
_console.WithColor(ConsoleColor.Black, ConsoleColor.DarkCyan, c =>
|
||||
{
|
||||
c.Output.WriteLine("Description");
|
||||
});
|
||||
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.Write(_matchingCommandSchema.Description);
|
||||
_console.Output.WriteLine();
|
||||
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
private void RenderUsage()
|
||||
{
|
||||
var hasChildCommands = _childCommandSchemas.Any();
|
||||
|
||||
_console.WithColor(ConsoleColor.Black, ConsoleColor.DarkCyan, c =>
|
||||
{
|
||||
c.Output.WriteLine("Usage");
|
||||
});
|
||||
|
||||
_console.Output.Write(" ");
|
||||
|
||||
_console.Output.Write(_applicationMetadata.ExecutableName);
|
||||
_console.Output.Write(' ');
|
||||
|
||||
if (_matchingCommandSchema != null && !_matchingCommandSchema.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
_console.Output.Write(_matchingCommandSchema.Name);
|
||||
_console.Output.Write(' ');
|
||||
}
|
||||
|
||||
if (hasChildCommands)
|
||||
{
|
||||
_console.Output.Write("[command]");
|
||||
_console.Output.Write(' ');
|
||||
}
|
||||
|
||||
_console.Output.Write("[options]");
|
||||
_console.Output.WriteLine();
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
private void RenderOptions()
|
||||
{
|
||||
var options = new List<CommandOptionSchema>();
|
||||
options.AddRange(_matchingCommandSchema?.Options ?? Enumerable.Empty<CommandOptionSchema>());
|
||||
options.Add(new CommandOptionSchema(null, "help", 'h', null, false, "Shows help text."));
|
||||
|
||||
if (_matchingCommandSchema == null || _matchingCommandSchema.Name.IsNullOrWhiteSpace())
|
||||
options.Add(new CommandOptionSchema(null, "version", 'v', null, false, "Shows application version."));
|
||||
|
||||
_console.WithColor(ConsoleColor.Black, ConsoleColor.DarkCyan, c =>
|
||||
{
|
||||
c.Output.WriteLine("Options");
|
||||
});
|
||||
|
||||
foreach (var option in options)
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
|
||||
if (!option.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
_console.WithColor(option.IsRequired ? ConsoleColor.Yellow : ConsoleColor.White, c =>
|
||||
{
|
||||
c.Output.Write("--");
|
||||
c.Output.Write(option.Name);
|
||||
});
|
||||
}
|
||||
|
||||
if (!option.Name.IsNullOrWhiteSpace() && option.ShortName != null)
|
||||
{
|
||||
_console.Output.Write('|');
|
||||
}
|
||||
|
||||
if (option.ShortName != null)
|
||||
{
|
||||
_console.WithColor(option.IsRequired ? ConsoleColor.Yellow : ConsoleColor.White, c =>
|
||||
{
|
||||
c.Output.Write('-');
|
||||
c.Output.Write(option.ShortName);
|
||||
});
|
||||
}
|
||||
|
||||
if (!option.Description.IsNullOrWhiteSpace())
|
||||
{
|
||||
_console.Output.Write(" ");
|
||||
_console.Output.Write(option.Description);
|
||||
}
|
||||
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
private void RenderChildCommands()
|
||||
{
|
||||
// TODO
|
||||
}
|
||||
|
||||
private void RenderSubCommandHelpTip()
|
||||
{
|
||||
if (!_childCommandSchemas.Any())
|
||||
return;
|
||||
|
||||
_console.Output.Write("You can run `");
|
||||
|
||||
_console.Output.Write(_applicationMetadata.ExecutableName);
|
||||
_console.Output.Write(' ');
|
||||
|
||||
if (_matchingCommandSchema != null && !_matchingCommandSchema.Name.IsNullOrWhiteSpace())
|
||||
{
|
||||
_console.Output.Write(_matchingCommandSchema.Name);
|
||||
_console.Output.Write(' ');
|
||||
}
|
||||
|
||||
_console.Output.Write("[command] --help` to show help on a specific command.");
|
||||
|
||||
_console.Output.WriteLine();
|
||||
}
|
||||
|
||||
public void Render()
|
||||
{
|
||||
RenderAppInfo();
|
||||
RenderDescription();
|
||||
RenderUsage();
|
||||
RenderOptions();
|
||||
RenderChildCommands();
|
||||
RenderSubCommandHelpTip();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,8 @@ namespace CliFx.Services
|
||||
attribute.Name,
|
||||
attribute.ShortName,
|
||||
attribute.GroupName,
|
||||
attribute.IsRequired, attribute.Description);
|
||||
attribute.IsRequired,
|
||||
attribute.Description);
|
||||
}
|
||||
|
||||
// TODO: validate stuff like duplicate names, multiple default commands, etc
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public partial class ConsoleWriter : IConsoleWriter, IDisposable
|
||||
{
|
||||
private readonly TextWriter _textWriter;
|
||||
private readonly bool _isRedirected;
|
||||
|
||||
public ConsoleWriter(TextWriter textWriter, bool isRedirected)
|
||||
{
|
||||
_textWriter = textWriter;
|
||||
_isRedirected = isRedirected;
|
||||
}
|
||||
|
||||
// TODO: handle colors
|
||||
public void Write(TextSpan text) => _textWriter.Write(text.Text);
|
||||
|
||||
public void WriteLine(TextSpan text) => _textWriter.WriteLine(text.Text);
|
||||
|
||||
public void Dispose() => _textWriter.Dispose();
|
||||
}
|
||||
|
||||
public partial class ConsoleWriter
|
||||
{
|
||||
public static ConsoleWriter GetStandardOutput() => new ConsoleWriter(Console.Out, Console.IsOutputRedirected);
|
||||
|
||||
public static ConsoleWriter GetStandardError() => new ConsoleWriter(Console.Error, Console.IsErrorRedirected);
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,28 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public static class Extensions
|
||||
{
|
||||
public static void Write(this IConsoleWriter consoleWriter, string text) =>
|
||||
consoleWriter.Write(new TextSpan(text));
|
||||
|
||||
public static void Write(this IConsoleWriter consoleWriter, IFormattable formattable) =>
|
||||
consoleWriter.Write(formattable.ToString(null, CultureInfo.InvariantCulture));
|
||||
|
||||
public static void Write(this IConsoleWriter consoleWriter, object obj)
|
||||
public static void WithColor(this IConsole console, ConsoleColor foregroundColor, Action<IConsole> action)
|
||||
{
|
||||
if (obj is IFormattable formattable)
|
||||
consoleWriter.Write(formattable);
|
||||
else
|
||||
consoleWriter.Write(obj.ToString());
|
||||
var lastForegroundColor = console.ForegroundColor;
|
||||
console.ForegroundColor = foregroundColor;
|
||||
|
||||
action(console);
|
||||
|
||||
console.ForegroundColor = lastForegroundColor;
|
||||
}
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, string text) =>
|
||||
consoleWriter.WriteLine(new TextSpan(text));
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, IFormattable formattable) =>
|
||||
consoleWriter.WriteLine(formattable.ToString(null, CultureInfo.InvariantCulture));
|
||||
|
||||
public static void WriteLine(this IConsoleWriter consoleWriter, object obj)
|
||||
public static void WithColor(this IConsole console, ConsoleColor foregroundColor, ConsoleColor backgroundColor,
|
||||
Action<IConsole> action)
|
||||
{
|
||||
if (obj is IFormattable formattable)
|
||||
consoleWriter.WriteLine(formattable);
|
||||
else
|
||||
consoleWriter.WriteLine(obj.ToString());
|
||||
var lastBackgroundColor = console.BackgroundColor;
|
||||
console.BackgroundColor = backgroundColor;
|
||||
|
||||
console.WithColor(foregroundColor, action);
|
||||
|
||||
console.BackgroundColor = lastBackgroundColor;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
using System;
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandFactory
|
||||
{
|
||||
ICommand CreateCommand(CommandSchema schema);
|
||||
ICommand CreateCommand(Type commandType);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,9 @@ using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface ICommandHelpTextBuilder
|
||||
public interface ICommandHelpTextRenderer
|
||||
{
|
||||
string Build(ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas,
|
||||
CommandSchema matchingCommandSchema);
|
||||
void RenderHelpText(ApplicationMetadata applicationMetadata,
|
||||
IReadOnlyList<CommandSchema> availableCommandSchemas, CommandSchema matchingCommandSchema = null);
|
||||
}
|
||||
}
|
||||
26
CliFx/Services/IConsole.cs
Normal file
26
CliFx/Services/IConsole.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface IConsole
|
||||
{
|
||||
TextReader Input { get; }
|
||||
|
||||
bool IsInputRedirected { get; }
|
||||
|
||||
TextWriter Output { get; }
|
||||
|
||||
bool IsOutputRedirected { get; }
|
||||
|
||||
TextWriter Error { get; }
|
||||
|
||||
bool IsErrorRedirected { get; }
|
||||
|
||||
ConsoleColor ForegroundColor { get; set; }
|
||||
|
||||
ConsoleColor BackgroundColor { get; set; }
|
||||
|
||||
void ResetColor();
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
using CliFx.Models;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public interface IConsoleWriter
|
||||
{
|
||||
void Write(TextSpan text);
|
||||
|
||||
void WriteLine(TextSpan text);
|
||||
}
|
||||
}
|
||||
34
CliFx/Services/SystemConsole.cs
Normal file
34
CliFx/Services/SystemConsole.cs
Normal file
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class SystemConsole : IConsole
|
||||
{
|
||||
public TextReader Input => Console.In;
|
||||
|
||||
public bool IsInputRedirected => Console.IsInputRedirected;
|
||||
|
||||
public TextWriter Output => Console.Out;
|
||||
|
||||
public bool IsOutputRedirected => Console.IsOutputRedirected;
|
||||
|
||||
public TextWriter Error => Console.Error;
|
||||
|
||||
public bool IsErrorRedirected => Console.IsErrorRedirected;
|
||||
|
||||
public ConsoleColor ForegroundColor
|
||||
{
|
||||
get => Console.ForegroundColor;
|
||||
set => Console.ForegroundColor = value;
|
||||
}
|
||||
|
||||
public ConsoleColor BackgroundColor
|
||||
{
|
||||
get => Console.BackgroundColor;
|
||||
set => Console.BackgroundColor = value;
|
||||
}
|
||||
|
||||
public void ResetColor() => Console.ResetColor();
|
||||
}
|
||||
}
|
||||
47
CliFx/Services/TestConsole.cs
Normal file
47
CliFx/Services/TestConsole.cs
Normal file
@@ -0,0 +1,47 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
|
||||
namespace CliFx.Services
|
||||
{
|
||||
public class TestConsole : IConsole
|
||||
{
|
||||
public TextReader Input { get; }
|
||||
|
||||
public bool IsInputRedirected => true;
|
||||
|
||||
public TextWriter Output { get; }
|
||||
|
||||
public bool IsOutputRedirected => true;
|
||||
|
||||
public TextWriter Error { get; }
|
||||
|
||||
public bool IsErrorRedirected => true;
|
||||
|
||||
public ConsoleColor ForegroundColor { get; set; } = ConsoleColor.Gray;
|
||||
|
||||
public ConsoleColor BackgroundColor { get; set; } = ConsoleColor.Black;
|
||||
|
||||
public TestConsole(TextReader input, TextWriter output, TextWriter error)
|
||||
{
|
||||
Input = input;
|
||||
Output = output;
|
||||
Error = error;
|
||||
}
|
||||
|
||||
public TestConsole(TextWriter output, TextWriter error)
|
||||
: this(TextReader.Null, output, error)
|
||||
{
|
||||
}
|
||||
|
||||
public TestConsole(TextWriter output)
|
||||
: this(output, TextWriter.Null)
|
||||
{
|
||||
}
|
||||
|
||||
public void ResetColor()
|
||||
{
|
||||
ForegroundColor = ConsoleColor.Gray;
|
||||
BackgroundColor = ConsoleColor.Black;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user