From 041a995c62b68ffcc025b7feee0b3df79445dffe Mon Sep 17 00:00:00 2001 From: Alexey Golub Date: Tue, 30 Jul 2019 17:35:06 +0300 Subject: [PATCH] Add console abstraction, remove CommandContext --- CliFx.Benchmarks/Commands/CliFxCommand.cs | 4 +- CliFx.Tests.Dummy/Commands/GreeterCommand.cs | 5 +- CliFx.Tests.Dummy/Commands/LogCommand.cs | 5 +- CliFx.Tests.Dummy/Commands/SumCommand.cs | 5 +- CliFx.Tests/CliApplicationTests.cs | 8 +- CliFx.Tests/CommandFactoryTests.cs | 6 +- CliFx.Tests/CommandInitializerTests.cs | 5 +- CliFx.Tests/CommandSchemaResolverTests.cs | 2 +- CliFx/CliApplication.cs | 91 +++----- CliFx/ICommand.cs | 4 +- CliFx/Models/CommandContext.cs | 32 --- CliFx/Models/CommandInput.cs | 6 +- CliFx/Models/CommandSchema.cs | 5 +- CliFx/Models/TextSpan.cs | 24 -- CliFx/Services/CommandFactory.cs | 3 +- CliFx/Services/CommandHelpTextBuilder.cs | 173 --------------- CliFx/Services/CommandHelpTextRenderer.cs | 210 ++++++++++++++++++ CliFx/Services/CommandSchemaResolver.cs | 3 +- CliFx/Services/ConsoleWriter.cs | 32 --- CliFx/Services/Extensions.cs | 39 ++-- CliFx/Services/ICommandFactory.cs | 3 +- ...Builder.cs => ICommandHelpTextRenderer.cs} | 7 +- CliFx/Services/IConsole.cs | 26 +++ CliFx/Services/IConsoleWriter.cs | 11 - CliFx/Services/SystemConsole.cs | 34 +++ CliFx/Services/TestConsole.cs | 47 ++++ 26 files changed, 399 insertions(+), 391 deletions(-) delete mode 100644 CliFx/Models/CommandContext.cs delete mode 100644 CliFx/Models/TextSpan.cs delete mode 100644 CliFx/Services/CommandHelpTextBuilder.cs create mode 100644 CliFx/Services/CommandHelpTextRenderer.cs delete mode 100644 CliFx/Services/ConsoleWriter.cs rename CliFx/Services/{ICommandHelpTextBuilder.cs => ICommandHelpTextRenderer.cs} (50%) create mode 100644 CliFx/Services/IConsole.cs delete mode 100644 CliFx/Services/IConsoleWriter.cs create mode 100644 CliFx/Services/SystemConsole.cs create mode 100644 CliFx/Services/TestConsole.cs diff --git a/CliFx.Benchmarks/Commands/CliFxCommand.cs b/CliFx.Benchmarks/Commands/CliFxCommand.cs index 53ac534..fe7ab86 100644 --- a/CliFx.Benchmarks/Commands/CliFxCommand.cs +++ b/CliFx.Benchmarks/Commands/CliFxCommand.cs @@ -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; } } diff --git a/CliFx.Tests.Dummy/Commands/GreeterCommand.cs b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs index c1cf18b..f4c4558 100644 --- a/CliFx.Tests.Dummy/Commands/GreeterCommand.cs +++ b/CliFx.Tests.Dummy/Commands/GreeterCommand.cs @@ -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; } diff --git a/CliFx.Tests.Dummy/Commands/LogCommand.cs b/CliFx.Tests.Dummy/Commands/LogCommand.cs index 8e3a7ef..7786388 100644 --- a/CliFx.Tests.Dummy/Commands/LogCommand.cs +++ b/CliFx.Tests.Dummy/Commands/LogCommand.cs @@ -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; } diff --git a/CliFx.Tests.Dummy/Commands/SumCommand.cs b/CliFx.Tests.Dummy/Commands/SumCommand.cs index dea684b..ec106f0 100644 --- a/CliFx.Tests.Dummy/Commands/SumCommand.cs +++ b/CliFx.Tests.Dummy/Commands/SumCommand.cs @@ -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 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; } diff --git a/CliFx.Tests/CliApplicationTests.cs b/CliFx.Tests/CliApplicationTests.cs index 78ca246..4fa3ed1 100644 --- a/CliFx.Tests/CliApplicationTests.cs +++ b/CliFx.Tests/CliApplicationTests.cs @@ -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)); } } diff --git a/CliFx.Tests/CommandFactoryTests.cs b/CliFx.Tests/CommandFactoryTests.cs index 5ac3a2a..14f1498 100644 --- a/CliFx.Tests/CommandFactoryTests.cs +++ b/CliFx.Tests/CommandFactoryTests.cs @@ -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); diff --git a/CliFx.Tests/CommandInitializerTests.cs b/CliFx.Tests/CommandInitializerTests.cs index e1e3cb0..caa34d4 100644 --- a/CliFx.Tests/CommandInitializerTests.cs +++ b/CliFx.Tests/CommandInitializerTests.cs @@ -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; } } diff --git a/CliFx.Tests/CommandSchemaResolverTests.cs b/CliFx.Tests/CommandSchemaResolverTests.cs index 6c24386..65e3348 100644 --- a/CliFx.Tests/CommandSchemaResolverTests.cs +++ b/CliFx.Tests/CommandSchemaResolverTests.cs @@ -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; } } diff --git a/CliFx/CliApplication.cs b/CliFx/CliApplication.cs index 2deb4ad..049631e 100644 --- a/CliFx/CliApplication.cs +++ b/CliFx/CliApplication.cs @@ -15,29 +15,33 @@ namespace CliFx { private readonly ApplicationMetadata _applicationMetadata; private readonly IReadOnlyList _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 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 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 RunAsync(IReadOnlyList 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; - } - } } } \ No newline at end of file diff --git a/CliFx/ICommand.cs b/CliFx/ICommand.cs index 226826e..6137bd0 100644 --- a/CliFx/ICommand.cs +++ b/CliFx/ICommand.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx/Models/CommandContext.cs b/CliFx/Models/CommandContext.cs deleted file mode 100644 index 39ff6c4..0000000 --- a/CliFx/Models/CommandContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Collections.Generic; -using CliFx.Services; - -namespace CliFx.Models -{ - public class CommandContext - { - public ApplicationMetadata ApplicationMetadata { get; } - - public IReadOnlyList AvailableCommandSchemas { get; } - - public CommandSchema MatchingCommandSchema { get; } - - public CommandInput CommandInput { get; } - - public IConsoleWriter Output { get; } - - public IConsoleWriter Error { get; } - - public CommandContext(ApplicationMetadata applicationMetadata, - IReadOnlyList availableCommandSchemas, CommandSchema matchingCommandSchema, - CommandInput commandInput, IConsoleWriter output, IConsoleWriter error) - { - ApplicationMetadata = applicationMetadata; - AvailableCommandSchemas = availableCommandSchemas; - MatchingCommandSchema = matchingCommandSchema; - CommandInput = commandInput; - Output = output; - Error = error; - } - } -} \ No newline at end of file diff --git a/CliFx/Models/CommandInput.cs b/CliFx/Models/CommandInput.cs index 60a3038..4729df8 100644 --- a/CliFx/Models/CommandInput.cs +++ b/CliFx/Models/CommandInput.cs @@ -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(); diff --git a/CliFx/Models/CommandSchema.cs b/CliFx/Models/CommandSchema.cs index bf4aef0..01b6ed8 100644 --- a/CliFx/Models/CommandSchema.cs +++ b/CliFx/Models/CommandSchema.cs @@ -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(); diff --git a/CliFx/Models/TextSpan.cs b/CliFx/Models/TextSpan.cs deleted file mode 100644 index 7c349f4..0000000 --- a/CliFx/Models/TextSpan.cs +++ /dev/null @@ -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; - } -} \ No newline at end of file diff --git a/CliFx/Services/CommandFactory.cs b/CliFx/Services/CommandFactory.cs index ff8db23..9092299 100644 --- a/CliFx/Services/CommandFactory.cs +++ b/CliFx/Services/CommandFactory.cs @@ -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); } } \ No newline at end of file diff --git a/CliFx/Services/CommandHelpTextBuilder.cs b/CliFx/Services/CommandHelpTextBuilder.cs deleted file mode 100644 index 665e36b..0000000 --- a/CliFx/Services/CommandHelpTextBuilder.cs +++ /dev/null @@ -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 GetOptionAliasesWithPrefixes(CommandOptionSchema optionSchema) - { - var result = new List(); - - if (!optionSchema.Name.IsNullOrWhiteSpace()) - result.Add("--" + optionSchema.Name); - - if (optionSchema.ShortName != null) - result.Add("-" + optionSchema.ShortName.Value); - - return result; - } - - private IReadOnlyList GetChildCommandSchemas(IReadOnlyList 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 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 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 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(); - } - } -} \ No newline at end of file diff --git a/CliFx/Services/CommandHelpTextRenderer.cs b/CliFx/Services/CommandHelpTextRenderer.cs new file mode 100644 index 0000000..a87ba31 --- /dev/null +++ b/CliFx/Services/CommandHelpTextRenderer.cs @@ -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 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 _availableCommandSchemas; + private readonly CommandSchema _matchingCommandSchema; + + private readonly IReadOnlyList _childCommandSchemas; + + public RenderHelpTextImpl(IConsole console, ApplicationMetadata applicationMetadata, + IReadOnlyList availableCommandSchemas, CommandSchema matchingCommandSchema) + { + _console = console; + _applicationMetadata = applicationMetadata; + _availableCommandSchemas = availableCommandSchemas; + _matchingCommandSchema = matchingCommandSchema; + + _childCommandSchemas = GetChildCommandSchemas(); + } + + private IReadOnlyList 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(); + options.AddRange(_matchingCommandSchema?.Options ?? Enumerable.Empty()); + 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(); + } + } + } +} \ No newline at end of file diff --git a/CliFx/Services/CommandSchemaResolver.cs b/CliFx/Services/CommandSchemaResolver.cs index d2b4d59..554939c 100644 --- a/CliFx/Services/CommandSchemaResolver.cs +++ b/CliFx/Services/CommandSchemaResolver.cs @@ -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 diff --git a/CliFx/Services/ConsoleWriter.cs b/CliFx/Services/ConsoleWriter.cs deleted file mode 100644 index 3dd9551..0000000 --- a/CliFx/Services/ConsoleWriter.cs +++ /dev/null @@ -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); - } -} \ No newline at end of file diff --git a/CliFx/Services/Extensions.cs b/CliFx/Services/Extensions.cs index e7afc47..621be44 100644 --- a/CliFx/Services/Extensions.cs +++ b/CliFx/Services/Extensions.cs @@ -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 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 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; } } } \ No newline at end of file diff --git a/CliFx/Services/ICommandFactory.cs b/CliFx/Services/ICommandFactory.cs index 313bde9..b952318 100644 --- a/CliFx/Services/ICommandFactory.cs +++ b/CliFx/Services/ICommandFactory.cs @@ -1,10 +1,9 @@ using System; -using CliFx.Models; namespace CliFx.Services { public interface ICommandFactory { - ICommand CreateCommand(CommandSchema schema); + ICommand CreateCommand(Type commandType); } } \ No newline at end of file diff --git a/CliFx/Services/ICommandHelpTextBuilder.cs b/CliFx/Services/ICommandHelpTextRenderer.cs similarity index 50% rename from CliFx/Services/ICommandHelpTextBuilder.cs rename to CliFx/Services/ICommandHelpTextRenderer.cs index 0021199..ef223ab 100644 --- a/CliFx/Services/ICommandHelpTextBuilder.cs +++ b/CliFx/Services/ICommandHelpTextRenderer.cs @@ -3,10 +3,9 @@ using CliFx.Models; namespace CliFx.Services { - public interface ICommandHelpTextBuilder + public interface ICommandHelpTextRenderer { - string Build(ApplicationMetadata applicationMetadata, - IReadOnlyList availableCommandSchemas, - CommandSchema matchingCommandSchema); + void RenderHelpText(ApplicationMetadata applicationMetadata, + IReadOnlyList availableCommandSchemas, CommandSchema matchingCommandSchema = null); } } \ No newline at end of file diff --git a/CliFx/Services/IConsole.cs b/CliFx/Services/IConsole.cs new file mode 100644 index 0000000..8210aa7 --- /dev/null +++ b/CliFx/Services/IConsole.cs @@ -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(); + } +} \ No newline at end of file diff --git a/CliFx/Services/IConsoleWriter.cs b/CliFx/Services/IConsoleWriter.cs deleted file mode 100644 index c361525..0000000 --- a/CliFx/Services/IConsoleWriter.cs +++ /dev/null @@ -1,11 +0,0 @@ -using CliFx.Models; - -namespace CliFx.Services -{ - public interface IConsoleWriter - { - void Write(TextSpan text); - - void WriteLine(TextSpan text); - } -} \ No newline at end of file diff --git a/CliFx/Services/SystemConsole.cs b/CliFx/Services/SystemConsole.cs new file mode 100644 index 0000000..f723908 --- /dev/null +++ b/CliFx/Services/SystemConsole.cs @@ -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(); + } +} \ No newline at end of file diff --git a/CliFx/Services/TestConsole.cs b/CliFx/Services/TestConsole.cs new file mode 100644 index 0000000..331f0b4 --- /dev/null +++ b/CliFx/Services/TestConsole.cs @@ -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; + } + } +} \ No newline at end of file