From 0ae419326df1c01efc900150f90472b2c9695747 Mon Sep 17 00:00:00 2001 From: Patrik Svensson Date: Wed, 23 Dec 2020 10:41:29 +0100 Subject: [PATCH] Add Spectre.Cli to Spectre.Console * Renames Spectre.Cli to Spectre.Console.Cli. * Now uses Verify with Spectre.Console.Cli tests. * Removes some duplicate definitions. Closes #168 --- docs/input/appendix/index.cshtml | 2 +- docs/input/cli/index.cshtml | 12 + docs/input/cli/introduction.md | 117 +++ dotnet-tools.json | 4 +- examples/Borders/Borders.csproj | 15 - examples/Calendars/Calendars.csproj | 15 - examples/Canvas/Canvas.csproj | 22 - examples/Charts/Charts.csproj | 15 - examples/Cli/Delegates/BarSettings.cs | 16 + examples/Cli/Delegates/Delegates.csproj | 17 + examples/Cli/Delegates/Program.cs | 39 + .../Demo/Commands/Add/AddPackageCommand.cs | 47 ++ .../Demo/Commands/Add/AddReferenceCommand.cs | 30 + examples/Cli/Demo/Commands/Add/AddSettings.cs | 12 + examples/Cli/Demo/Commands/Run/RunCommand.cs | 70 ++ .../Cli/Demo/Commands/Serve/ServeCommand.cs | 41 + examples/Cli/Demo/Demo.csproj | 17 + examples/Cli/Demo/Program.cs | 37 + examples/Cli/Demo/Utilities/SettingsDumper.cs | 29 + examples/Cli/Demo/Verbosity.cs | 54 ++ examples/Cli/Dynamic/Dynamic.csproj | 17 + examples/Cli/Dynamic/MyCommand.cs | 20 + examples/Cli/Dynamic/Program.cs | 24 + .../Cli/Injection/Commands/DefaultCommand.cs | 30 + examples/Cli/Injection/IGreeter.cs | 17 + .../Injection/Infrastructure/TypeRegistrar.cs | 31 + .../Injection/Infrastructure/TypeResolver.cs | 21 + examples/Cli/Injection/Injection.csproj | 21 + examples/Cli/Injection/Program.cs | 23 + examples/Colors/Colors.csproj | 15 - examples/Console/Borders/Borders.csproj | 15 + examples/{ => Console}/Borders/Program.cs | 0 examples/Console/Calendars/Calendars.csproj | 15 + examples/{ => Console}/Calendars/Program.cs | 0 examples/Console/Canvas/Canvas.csproj | 22 + examples/{ => Console}/Canvas/Mandelbrot.cs | 0 examples/{ => Console}/Canvas/Program.cs | 0 examples/{ => Console}/Canvas/cake.png | Bin examples/Console/Charts/Charts.csproj | 15 + examples/{ => Console}/Charts/Program.cs | 0 examples/Console/Colors/Colors.csproj | 15 + examples/{ => Console}/Colors/Program.cs | 0 examples/{ => Console}/Colors/Utilities.cs | 0 examples/{ => Console}/Columns/Columns.csproj | 8 +- examples/{ => Console}/Columns/Program.cs | 0 examples/{ => Console}/Columns/User.cs | 0 examples/Console/Cursor/Cursor.csproj | 15 + examples/{ => Console}/Cursor/Program.cs | 0 examples/Console/Emojis/Emojis.csproj | 15 + examples/{ => Console}/Emojis/Program.cs | 0 examples/Console/Exceptions/Exceptions.csproj | 15 + examples/{ => Console}/Exceptions/Program.cs | 0 examples/Console/Figlet/Figlet.csproj | 15 + examples/{ => Console}/Figlet/Program.cs | 0 examples/Console/Grids/Grids.csproj | 15 + examples/{ => Console}/Grids/Program.cs | 0 examples/Console/Info/Info.csproj | 15 + examples/{ => Console}/Info/Program.cs | 0 examples/Console/Links/Links.csproj | 15 + examples/{ => Console}/Links/Program.cs | 0 examples/Console/Panels/Panels.csproj | 15 + examples/{ => Console}/Panels/Program.cs | 0 .../Progress/DescriptionGenerator.cs | 0 examples/{ => Console}/Progress/Program.cs | 0 .../{ => Console}/Progress/Progress.csproj | 8 +- examples/{ => Console}/Prompt/Program.cs | 0 examples/Console/Prompt/Prompt.csproj | 16 + examples/{ => Console}/Rules/Program.cs | 0 examples/Console/Rules/Rules.csproj | 15 + examples/{ => Console}/Status/Program.cs | 0 examples/{ => Console}/Status/Status.csproj | 8 +- examples/{ => Console}/Tables/Program.cs | 0 examples/Console/Tables/Tables.csproj | 15 + examples/Cursor/Cursor.csproj | 15 - examples/Directory.Build.props | 5 + examples/Emojis/Emojis.csproj | 15 - examples/Exceptions/Exceptions.csproj | 15 - examples/Figlet/Figlet.csproj | 15 - examples/Grids/Grids.csproj | 15 - examples/Info/Info.csproj | 15 - examples/Links/Links.csproj | 15 - examples/Panels/Panels.csproj | 15 - examples/Prompt/Prompt.csproj | 16 - examples/Rules/Rules.csproj | 15 - examples/Tables/Tables.csproj | 15 - src/Directory.Build.props | 7 +- .../Spectre.Console.ImageSharp.csproj | 1 + src/Spectre.Console.Testing/.editorconfig | 14 + .../CommandAppFixture.cs | 92 +++ .../EmbeddedResourceReader.cs | 38 + .../Extensions/CommandContextExtensions.cs | 30 + .../Extensions/ShouldlyExtensions.cs | 49 ++ .../Extensions/StringExtensions.cs | 79 ++ .../Extensions/StyleExtensions.cs | 2 +- .../Extensions/XmlElementExtensions.cs | 69 ++ .../Fakes/FakeAnsiConsole.cs} | 12 +- .../Fakes/FakeAnsiConsoleCursor.cs} | 4 +- .../Fakes/FakeCommandInterceptor.cs | 20 + .../Fakes/FakeConsole.cs} | 12 +- .../Fakes/FakeConsoleInput.cs} | 6 +- .../Fakes/FakeLinkIdentityGenerator.cs | 17 + .../Fakes/FakeTypeRegistrar.cs | 45 + .../Fakes/FakeTypeResolver.cs | 31 + .../Spectre.Console.Testing.csproj | 17 + .../Widgets/DummySpinner1.cs | 12 + .../Widgets/DummySpinner2.cs | 12 + src/Spectre.Console.Tests/Constants.cs | 19 + .../Data/Commands/AnimalCommand.cs | 49 ++ .../Data/Commands/CatCommand.cs | 13 + .../Data/Commands/DogCommand.cs | 36 + .../Data/Commands/EmptyCommand.cs | 12 + .../Data/Commands/GenericCommand.cs | 13 + .../Data/Commands/GiraffeCommand.cs | 14 + .../Data/Commands/HorseCommand.cs | 15 + .../Data/Commands/InterceptingCommand.cs | 22 + .../Data/Commands/InvalidCommand.cs | 12 + .../Data/Commands/LionCommand.cs | 14 + .../Data/Commands/OptionVectorCommand.cs | 12 + .../Data/Commands/ThrowingCommand.cs | 17 + .../Data/Converters/CatAgilityConverter.cs | 18 + .../Converters/StringToIntegerConverter.cs | 18 + .../Data/Settings/AnimalSettings.cs | 18 + .../Data/Settings/ArgumentVectorSettings.cs | 12 + .../Data/Settings/BarCommandSettings.cs | 12 + .../Data/Settings/CatSettings.cs | 15 + .../Data/Settings/DogSettings.cs | 23 + .../Data/Settings/EmptySettings.cs | 8 + .../Data/Settings/FooSettings.cs | 12 + .../Data/Settings/GiraffeSettings.cs | 12 + .../Data/Settings/InvalidSettings.cs | 10 + .../Data/Settings/LionSettings.cs | 16 + .../Data/Settings/MammalSettings.cs | 10 + .../MultipleArgumentVectorSettings.cs | 26 + .../Data/Settings/OptionVectorSettings.cs | 16 + ...ptionalArgumentWithDefaultValueSettings.cs | 27 + .../Data/Settings/StringOptionSettings.cs | 10 + .../EvenNumberValidatorAttribute.cs | 29 + .../PositiveNumberValidatorAttribute.cs | 29 + .../EmbeddedResourceDataAttribute.cs | 46 -- ...d_Examples_Defined_On_Command.verified.txt | 16 + ...ould_Output_Command_Correctly.verified.txt | 14 + ...put_Default_Command_Correctly.verified.txt | 13 + ....Should_Output_Leaf_Correctly.verified.txt | 9 + ....Should_Output_Root_Correctly.verified.txt | 11 + ...dren_If_Root_Have_No_Examples.verified.txt | 14 + ...f_No_Other_Examples_Are_Found.verified.txt | 13 + ...Root_Examples_Defined_On_Root.verified.txt | 14 + ..._Default_Command_Is_Specified.verified.txt | 16 + ...p.Should_Skip_Hidden_Commands.verified.txt | 10 + ..._Correct_Text_For_Long_Option.verified.txt | 4 + ...Correct_Text_For_Short_Option.verified.txt | 4 + ...me.Should_Return_Correct_Text.verified.txt | 4 + ...ol.Should_Return_Correct_Text.verified.txt | 4 + ...ng.Should_Return_Correct_Text.verified.txt | 4 + ...er.Should_Return_Correct_Text.verified.txt | 4 + ...it.Should_Return_Correct_Text.verified.txt | 4 + ...nt.Should_Return_Correct_Text.verified.txt | 4 + ..._Correct_Text_For_Long_Option.verified.txt | 4 + ...Correct_Text_For_Short_Option.verified.txt | 4 + ...on_Value_With_Colon_Separator.verified.txt | 4 + ...Value_With_Equality_Separator.verified.txt | 4 + ...on_Value_With_Colon_Separator.verified.txt | 4 + ...Value_With_Equality_Separator.verified.txt | 4 + ...Correct_Text_For_Short_Option.verified.txt | 4 + ..._Correct_Text_For_Long_Option.verified.txt | 4 + ...Correct_Text_For_Short_Option.verified.txt | 4 + ...rent_Command_Has_No_Arguments.verified.txt | 4 + ..._Text_When_Command_Is_Unknown.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...Unknown_And_Distance_Is_Small.verified.txt | 4 + ...ion_If_Strict_Mode_Is_Enabled.verified.txt | 4 + ...ion_If_Strict_Mode_Is_Enabled.verified.txt | 4 + ...te.Should_Return_Correct_Text.verified.txt | 4 + ...Dump_Correct_Model_For_Case_1.verified.txt | 37 + ...Dump_Correct_Model_For_Case_2.verified.txt | 21 + ...Dump_Correct_Model_For_Case_3.verified.txt | 32 + ...Dump_Correct_Model_For_Case_4.verified.txt | 26 + ...Dump_Correct_Model_For_Case_5.verified.txt | 10 + ...or_Model_With_Default_Command.verified.txt | 37 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + ...od.Should_Return_Correct_Text.verified.txt | 5 + .../Extensions/StringExtensions.cs | 34 - .../Spectre.Console.Tests.csproj | 6 +- .../Tools/DummySpinners.cs | 25 - .../Tools/ResourceReader.cs | 21 - .../Tools/TestLinkIdentityGenerator.cs | 10 - .../Unit/AnsiConsoleTests.Colors.cs | 23 +- .../Unit/AnsiConsoleTests.Markup.cs | 11 +- .../Unit/AnsiConsoleTests.Style.cs | 5 +- .../Unit/AnsiConsoleTests.cs | 13 +- .../Unit/BarChartTests.cs | 3 +- .../Unit/BoxBorderTests.cs | 13 +- .../Unit/CalendarTests.cs | 11 +- ...CommandArgumentAttributeTests.Rendering.cs | 92 +++ .../CommandArgumentAttributeTests.cs | 64 ++ .../CommandOptionAttributeTests.Rendering.cs | 239 ++++++ .../CommandOptionAttributeTests.cs | 197 +++++ .../Unit/Cli/CommandAppTests.FlagValues.cs | 234 ++++++ .../Unit/Cli/CommandAppTests.Help.cs | 231 ++++++ .../Unit/Cli/CommandAppTests.Injection.cs | 67 ++ .../Unit/Cli/CommandAppTests.Pairs.cs | 292 +++++++ .../Unit/Cli/CommandAppTests.Parsing.cs | 615 ++++++++++++++ .../Unit/Cli/CommandAppTests.Sensitivity.cs | 118 +++ .../Unit/Cli/CommandAppTests.Settings.cs | 26 + .../Cli/CommandAppTests.TypeConverters.cs | 40 + .../Unit/Cli/CommandAppTests.Unsafe.cs | 299 +++++++ .../Unit/Cli/CommandAppTests.Validation.cs | 88 ++ .../Unit/Cli/CommandAppTests.Vectors.cs | 111 +++ .../Unit/Cli/CommandAppTests.Version.cs | 37 + .../Unit/Cli/CommandAppTests.Xml.cs | 148 ++++ .../Unit/Cli/CommandAppTests.cs | 769 ++++++++++++++++++ .../Unit/ColumnsTests.cs | 3 +- src/Spectre.Console.Tests/Unit/EmojiTests.cs | 3 +- .../Unit/ExceptionTests.cs | 11 +- src/Spectre.Console.Tests/Unit/FigletTests.cs | 15 +- src/Spectre.Console.Tests/Unit/GridTests.cs | 15 +- src/Spectre.Console.Tests/Unit/MarkupTests.cs | 7 +- src/Spectre.Console.Tests/Unit/PadderTests.cs | 7 +- src/Spectre.Console.Tests/Unit/PanelTests.cs | 35 +- .../Unit/ProgressTests.cs | 9 +- src/Spectre.Console.Tests/Unit/PromptTests.cs | 19 +- .../Unit/RecorderTests.cs | 5 +- .../Unit/RenderHookTests.cs | 3 +- src/Spectre.Console.Tests/Unit/RowsTests.cs | 7 +- src/Spectre.Console.Tests/Unit/RuleTests.cs | 19 +- src/Spectre.Console.Tests/Unit/StatusTests.cs | 5 +- .../Unit/TableBorderTests.cs | 43 +- src/Spectre.Console.Tests/Unit/TableTests.cs | 37 +- src/Spectre.Console.Tests/Unit/TextTests.cs | 11 +- src/Spectre.Console.sln | 120 ++- .../Annotations/CommandArgumentAttribute.cs | 54 ++ .../Cli/Annotations/CommandOptionAttribute.cs | 66 ++ .../Annotations/PairDeconstructorAttribute.cs | 31 + .../ParameterValidationAttribute.cs | 35 + src/Spectre.Console/Cli/AsyncCommand.cs | 35 + src/Spectre.Console/Cli/AsyncCommand`1.cs | 51 ++ src/Spectre.Console/Cli/CaseSensitivity.cs | 33 + src/Spectre.Console/Cli/Command.cs | 35 + src/Spectre.Console/Cli/CommandApp.cs | 155 ++++ .../Cli/CommandAppException.cs | 27 + src/Spectre.Console/Cli/CommandApp`1.cs | 55 ++ .../Cli/CommandConfigurationException.cs | 82 ++ src/Spectre.Console/Cli/CommandContext.cs | 45 + .../Cli/CommandParseException.cs | 128 +++ .../Cli/CommandRuntimeException.cs | 59 ++ src/Spectre.Console/Cli/CommandSettings.cs | 17 + .../Cli/CommandTemplateException.cs | 161 ++++ src/Spectre.Console/Cli/Command`1.cs | 52 ++ .../Cli/ConfiguratorExtensions.cs | 211 +++++ .../Cli/EmptyCommandSettings.cs | 9 + src/Spectre.Console/Cli/FlagValue.cs | 57 ++ src/Spectre.Console/Cli/ICommand.cs | 26 + src/Spectre.Console/Cli/ICommandApp.cs | 32 + .../Cli/ICommandAppSettings.cs | 49 ++ .../Cli/ICommandConfigurator.cs | 44 + .../Cli/ICommandInterceptor.cs | 17 + src/Spectre.Console/Cli/ICommandLimiter`1.cs | 12 + .../Cli/ICommandParameterInfo.cs | 20 + src/Spectre.Console/Cli/ICommand`1.cs | 20 + src/Spectre.Console/Cli/IConfigurator.cs | 49 ++ src/Spectre.Console/Cli/IConfigurator`1.cs | 59 ++ src/Spectre.Console/Cli/IFlagValue.cs | 25 + .../Cli/IRemainingArguments.cs | 21 + src/Spectre.Console/Cli/ITypeRegistrar.cs | 31 + .../Cli/ITypeRegistrarFrontend.cs | 32 + src/Spectre.Console/Cli/ITypeResolver.cs | 17 + .../Binding/CommandConstructorBinder.cs | 55 ++ .../Internal/Binding/CommandPropertyBinder.cs | 36 + .../Internal/Binding/CommandValueBinder.cs | 118 +++ .../Internal/Binding/CommandValueLookup.cs | 63 ++ .../Internal/Binding/CommandValueResolver.cs | 133 +++ .../Cli/Internal/Collections/IMultiMap.cs | 14 + .../Cli/Internal/Collections/MultiMap.cs | 173 ++++ .../Cli/Internal/CommandBinder.cs | 31 + .../Cli/Internal/CommandExecutor.cs | 108 +++ .../Cli/Internal/CommandPart.cs | 8 + .../Cli/Internal/CommandSuggestor.cs | 77 ++ .../Cli/Internal/CommandValidator.cs | 45 + .../Cli/Internal/Commands/VersionCommand.cs | 34 + .../Cli/Internal/Commands/XmlDocCommand.cs | 184 +++++ src/Spectre.Console/Cli/Internal/Composer.cs | 106 +++ .../Cli/Internal/Composition/Activators.cs | 133 +++ .../Composition/ComponentRegistration.cs | 31 + .../Internal/Composition/ComponentRegistry.cs | 60 ++ .../Composition/DefaultTypeRegistrar.cs | 39 + .../Composition/DefaultTypeResolver.cs | 67 ++ .../Configuration/CommandAppSettings.cs | 44 + .../Configuration/CommandConfigurator.cs | 42 + .../Configuration/ConfigurationHelper.cs | 43 + .../Internal/Configuration/Configurator.cs | 95 +++ .../Internal/Configuration/Configurator`1.cs | 94 +++ .../Configuration/ConfiguredCommand.cs | 69 ++ .../Internal/Configuration/IConfiguration.cs | 30 + .../Internal/Configuration/TemplateParser.cs | 149 ++++ .../Internal/Configuration/TemplateToken.cs | 27 + .../Configuration/TemplateTokenizer.cs | 135 +++ src/Spectre.Console/Cli/Internal/Constants.cs | 22 + .../Cli/Internal/DefaultPairDeconstructor.cs | 78 ++ .../Cli/Internal/DelegateCommand.cs | 25 + .../CommandLineParseExceptionFactory.cs | 52 ++ .../CommandLineTemplateExceptionFactory.cs | 57 ++ .../Extensions/AnsiConsoleExtensions.cs | 42 + .../Extensions/CaseSensitivityExtensions.cs | 35 + .../Cli/Internal/Extensions/ListExtensions.cs | 55 ++ .../Internal/Extensions/StringExtensions.cs | 18 + .../Cli/Internal/Extensions/TypeExtensions.cs | 24 + .../Extensions/TypeRegistrarExtensions.cs | 63 ++ .../Extensions/XmlElementExtensions.cs | 47 ++ .../Cli/Internal/HelpWriter.cs | 382 +++++++++ .../Cli/Internal/IPairDeconstructor.cs | 24 + .../Cli/Internal/Modelling/CommandArgument.cs | 24 + .../Modelling/CommandContainerExtensions.cs | 21 + .../Cli/Internal/Modelling/CommandInfo.cs | 55 ++ .../Modelling/CommandInfoExtensions.cs | 79 ++ .../Cli/Internal/Modelling/CommandModel.cs | 34 + .../Internal/Modelling/CommandModelBuilder.cs | 249 ++++++ .../Modelling/CommandModelValidator.cs | 191 +++++ .../Cli/Internal/Modelling/CommandOption.cs | 35 + .../Internal/Modelling/CommandParameter.cs | 151 ++++ .../Modelling/CommandParameterComparer.cs | 32 + .../Internal/Modelling/ICommandContainer.cs | 15 + .../Cli/Internal/Modelling/ParameterKind.cs | 22 + .../Cli/Internal/Parsing/CommandTree.cs | 37 + .../Internal/Parsing/CommandTreeExtensions.cs | 70 ++ .../Cli/Internal/Parsing/CommandTreeParser.cs | 388 +++++++++ .../Parsing/CommandTreeParserContext.cs | 57 ++ .../Parsing/CommandTreeParserResult.cs | 15 + .../Cli/Internal/Parsing/CommandTreeToken.cs | 27 + .../Parsing/CommandTreeTokenStream.cs | 90 ++ .../Internal/Parsing/CommandTreeTokenizer.cs | 289 +++++++ .../Parsing/CommandTreeTokenizerContext.cs | 47 ++ .../Parsing/MappedCommandParameter.cs | 15 + .../Cli/Internal/ParsingMode.cs | 8 + .../Cli/Internal/RemainingArguments.cs | 19 + .../Cli/Internal/StringWriterWithEncoding.cs | 16 + .../Cli/Internal/TextBuffer.cs | 89 ++ .../Cli/Internal/TypeRegistrar.cs | 41 + .../Cli/Internal/TypeResolverAdapter.cs | 47 ++ .../Cli/Internal/VersionHelper.cs | 28 + src/Spectre.Console/Cli/PairDeconstuctor.cs | 31 + .../Cli/Unsafe/IUnsafeBranchConfigurator.cs | 20 + .../Cli/Unsafe/IUnsafeConfigurator.cs | 26 + .../Unsafe/UnsafeConfiguratorExtensions.cs | 83 ++ src/Spectre.Console/Color.cs | 4 +- src/Spectre.Console/CursorDirection.cs | 8 +- src/Spectre.Console/Spectre.Console.csproj | 13 +- 361 files changed, 13934 insertions(+), 604 deletions(-) create mode 100644 docs/input/cli/index.cshtml create mode 100644 docs/input/cli/introduction.md delete mode 100644 examples/Borders/Borders.csproj delete mode 100644 examples/Calendars/Calendars.csproj delete mode 100644 examples/Canvas/Canvas.csproj delete mode 100644 examples/Charts/Charts.csproj create mode 100644 examples/Cli/Delegates/BarSettings.cs create mode 100644 examples/Cli/Delegates/Delegates.csproj create mode 100644 examples/Cli/Delegates/Program.cs create mode 100644 examples/Cli/Demo/Commands/Add/AddPackageCommand.cs create mode 100644 examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs create mode 100644 examples/Cli/Demo/Commands/Add/AddSettings.cs create mode 100644 examples/Cli/Demo/Commands/Run/RunCommand.cs create mode 100644 examples/Cli/Demo/Commands/Serve/ServeCommand.cs create mode 100644 examples/Cli/Demo/Demo.csproj create mode 100644 examples/Cli/Demo/Program.cs create mode 100644 examples/Cli/Demo/Utilities/SettingsDumper.cs create mode 100644 examples/Cli/Demo/Verbosity.cs create mode 100644 examples/Cli/Dynamic/Dynamic.csproj create mode 100644 examples/Cli/Dynamic/MyCommand.cs create mode 100644 examples/Cli/Dynamic/Program.cs create mode 100644 examples/Cli/Injection/Commands/DefaultCommand.cs create mode 100644 examples/Cli/Injection/IGreeter.cs create mode 100644 examples/Cli/Injection/Infrastructure/TypeRegistrar.cs create mode 100644 examples/Cli/Injection/Infrastructure/TypeResolver.cs create mode 100644 examples/Cli/Injection/Injection.csproj create mode 100644 examples/Cli/Injection/Program.cs delete mode 100644 examples/Colors/Colors.csproj create mode 100644 examples/Console/Borders/Borders.csproj rename examples/{ => Console}/Borders/Program.cs (100%) create mode 100644 examples/Console/Calendars/Calendars.csproj rename examples/{ => Console}/Calendars/Program.cs (100%) create mode 100644 examples/Console/Canvas/Canvas.csproj rename examples/{ => Console}/Canvas/Mandelbrot.cs (100%) rename examples/{ => Console}/Canvas/Program.cs (100%) rename examples/{ => Console}/Canvas/cake.png (100%) create mode 100644 examples/Console/Charts/Charts.csproj rename examples/{ => Console}/Charts/Program.cs (100%) create mode 100644 examples/Console/Colors/Colors.csproj rename examples/{ => Console}/Colors/Program.cs (100%) rename examples/{ => Console}/Colors/Utilities.cs (100%) rename examples/{ => Console}/Columns/Columns.csproj (52%) rename examples/{ => Console}/Columns/Program.cs (100%) rename examples/{ => Console}/Columns/User.cs (100%) create mode 100644 examples/Console/Cursor/Cursor.csproj rename examples/{ => Console}/Cursor/Program.cs (100%) create mode 100644 examples/Console/Emojis/Emojis.csproj rename examples/{ => Console}/Emojis/Program.cs (100%) create mode 100644 examples/Console/Exceptions/Exceptions.csproj rename examples/{ => Console}/Exceptions/Program.cs (100%) create mode 100644 examples/Console/Figlet/Figlet.csproj rename examples/{ => Console}/Figlet/Program.cs (100%) create mode 100644 examples/Console/Grids/Grids.csproj rename examples/{ => Console}/Grids/Program.cs (100%) create mode 100644 examples/Console/Info/Info.csproj rename examples/{ => Console}/Info/Program.cs (100%) create mode 100644 examples/Console/Links/Links.csproj rename examples/{ => Console}/Links/Program.cs (100%) create mode 100644 examples/Console/Panels/Panels.csproj rename examples/{ => Console}/Panels/Program.cs (100%) rename examples/{ => Console}/Progress/DescriptionGenerator.cs (100%) rename examples/{ => Console}/Progress/Program.cs (100%) rename examples/{ => Console}/Progress/Progress.csproj (53%) rename examples/{ => Console}/Prompt/Program.cs (100%) create mode 100644 examples/Console/Prompt/Prompt.csproj rename examples/{ => Console}/Rules/Program.cs (100%) create mode 100644 examples/Console/Rules/Rules.csproj rename examples/{ => Console}/Status/Program.cs (100%) rename examples/{ => Console}/Status/Status.csproj (53%) rename examples/{ => Console}/Tables/Program.cs (100%) create mode 100644 examples/Console/Tables/Tables.csproj delete mode 100644 examples/Cursor/Cursor.csproj create mode 100644 examples/Directory.Build.props delete mode 100644 examples/Emojis/Emojis.csproj delete mode 100644 examples/Exceptions/Exceptions.csproj delete mode 100644 examples/Figlet/Figlet.csproj delete mode 100644 examples/Grids/Grids.csproj delete mode 100644 examples/Info/Info.csproj delete mode 100644 examples/Links/Links.csproj delete mode 100644 examples/Panels/Panels.csproj delete mode 100644 examples/Prompt/Prompt.csproj delete mode 100644 examples/Rules/Rules.csproj delete mode 100644 examples/Tables/Tables.csproj create mode 100644 src/Spectre.Console.Testing/.editorconfig create mode 100644 src/Spectre.Console.Testing/CommandAppFixture.cs create mode 100644 src/Spectre.Console.Testing/EmbeddedResourceReader.cs create mode 100644 src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs create mode 100644 src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs create mode 100644 src/Spectre.Console.Testing/Extensions/StringExtensions.cs rename src/{Spectre.Console.Tests => Spectre.Console.Testing}/Extensions/StyleExtensions.cs (87%) create mode 100644 src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs rename src/{Spectre.Console.Tests/Tools/TestableAnsiConsole.cs => Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs} (83%) rename src/{Spectre.Console.Tests/Tools/DummyCursor.cs => Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs} (69%) create mode 100644 src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs rename src/{Spectre.Console.Tests/Tools/PlainConsole.cs => Spectre.Console.Testing/Fakes/FakeConsole.cs} (88%) rename src/{Spectre.Console.Tests/Tools/TestableConsoleInput.cs => Spectre.Console.Testing/Fakes/FakeConsoleInput.cs} (90%) create mode 100644 src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs create mode 100644 src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs create mode 100644 src/Spectre.Console.Testing/Fakes/FakeTypeResolver.cs create mode 100644 src/Spectre.Console.Testing/Spectre.Console.Testing.csproj create mode 100644 src/Spectre.Console.Testing/Widgets/DummySpinner1.cs create mode 100644 src/Spectre.Console.Testing/Widgets/DummySpinner2.cs create mode 100644 src/Spectre.Console.Tests/Constants.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/AnimalCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/CatCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/DogCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/EmptyCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/GenericCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/GiraffeCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/HorseCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/InterceptingCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/InvalidCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/LionCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/OptionVectorCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Commands/ThrowingCommand.cs create mode 100644 src/Spectre.Console.Tests/Data/Converters/CatAgilityConverter.cs create mode 100644 src/Spectre.Console.Tests/Data/Converters/StringToIntegerConverter.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/AnimalSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/ArgumentVectorSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/BarCommandSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/CatSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/DogSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/EmptySettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/FooSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/GiraffeSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/InvalidSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/LionSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/MammalSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/MultipleArgumentVectorSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/OptionVectorSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Settings/StringOptionSettings.cs create mode 100644 src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs create mode 100644 src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs delete mode 100644 src/Spectre.Console.Tests/EmbeddedResourceDataAttribute.cs create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Only_Output_Command_Examples_Defined_On_Command.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Command_Correctly.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Default_Command_Correctly.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Leaf_Correctly.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Correctly.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Root.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_If_Default_Command_Is_Specified.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Skip_Hidden_Commands.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Long_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Short_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.InvalidShortOptionName.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameContainSymbol.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsMissing.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsOneCharacter.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameStartWithDigit.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoMatchingArgument.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Long_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Short_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Short_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Long_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Short_Option.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_When_Command_Is_Unknown.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnterminatedQuote.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_1.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_2.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_3.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_4.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_5.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Model_With_Default_Command.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheArgumentCannotContainOptionsMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheMultipleValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheValuesMustHaveNameMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInOptionNameMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInValueNameMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheLongOptionMustHaveMoreThanOneCharacterMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMissingLongAndShortNameMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMultipleOptionValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionNamesCannotStartWithDigitMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionsMustHaveNameMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheShortOptionMustOnlyBeOneCharacterMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnexpectedCharacterMethod.Should_Return_Correct_Text.verified.txt create mode 100644 src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnterminatedValueNameMethod.Should_Return_Correct_Text.verified.txt delete mode 100644 src/Spectre.Console.Tests/Extensions/StringExtensions.cs delete mode 100644 src/Spectre.Console.Tests/Tools/DummySpinners.cs delete mode 100644 src/Spectre.Console.Tests/Tools/ResourceReader.cs delete mode 100644 src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs create mode 100644 src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs create mode 100644 src/Spectre.Console/Cli/Annotations/CommandArgumentAttribute.cs create mode 100644 src/Spectre.Console/Cli/Annotations/CommandOptionAttribute.cs create mode 100644 src/Spectre.Console/Cli/Annotations/PairDeconstructorAttribute.cs create mode 100644 src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs create mode 100644 src/Spectre.Console/Cli/AsyncCommand.cs create mode 100644 src/Spectre.Console/Cli/AsyncCommand`1.cs create mode 100644 src/Spectre.Console/Cli/CaseSensitivity.cs create mode 100644 src/Spectre.Console/Cli/Command.cs create mode 100644 src/Spectre.Console/Cli/CommandApp.cs create mode 100644 src/Spectre.Console/Cli/CommandAppException.cs create mode 100644 src/Spectre.Console/Cli/CommandApp`1.cs create mode 100644 src/Spectre.Console/Cli/CommandConfigurationException.cs create mode 100644 src/Spectre.Console/Cli/CommandContext.cs create mode 100644 src/Spectre.Console/Cli/CommandParseException.cs create mode 100644 src/Spectre.Console/Cli/CommandRuntimeException.cs create mode 100644 src/Spectre.Console/Cli/CommandSettings.cs create mode 100644 src/Spectre.Console/Cli/CommandTemplateException.cs create mode 100644 src/Spectre.Console/Cli/Command`1.cs create mode 100644 src/Spectre.Console/Cli/ConfiguratorExtensions.cs create mode 100644 src/Spectre.Console/Cli/EmptyCommandSettings.cs create mode 100644 src/Spectre.Console/Cli/FlagValue.cs create mode 100644 src/Spectre.Console/Cli/ICommand.cs create mode 100644 src/Spectre.Console/Cli/ICommandApp.cs create mode 100644 src/Spectre.Console/Cli/ICommandAppSettings.cs create mode 100644 src/Spectre.Console/Cli/ICommandConfigurator.cs create mode 100644 src/Spectre.Console/Cli/ICommandInterceptor.cs create mode 100644 src/Spectre.Console/Cli/ICommandLimiter`1.cs create mode 100644 src/Spectre.Console/Cli/ICommandParameterInfo.cs create mode 100644 src/Spectre.Console/Cli/ICommand`1.cs create mode 100644 src/Spectre.Console/Cli/IConfigurator.cs create mode 100644 src/Spectre.Console/Cli/IConfigurator`1.cs create mode 100644 src/Spectre.Console/Cli/IFlagValue.cs create mode 100644 src/Spectre.Console/Cli/IRemainingArguments.cs create mode 100644 src/Spectre.Console/Cli/ITypeRegistrar.cs create mode 100644 src/Spectre.Console/Cli/ITypeRegistrarFrontend.cs create mode 100644 src/Spectre.Console/Cli/ITypeResolver.cs create mode 100644 src/Spectre.Console/Cli/Internal/Binding/CommandConstructorBinder.cs create mode 100644 src/Spectre.Console/Cli/Internal/Binding/CommandPropertyBinder.cs create mode 100644 src/Spectre.Console/Cli/Internal/Binding/CommandValueBinder.cs create mode 100644 src/Spectre.Console/Cli/Internal/Binding/CommandValueLookup.cs create mode 100644 src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs create mode 100644 src/Spectre.Console/Cli/Internal/Collections/IMultiMap.cs create mode 100644 src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs create mode 100644 src/Spectre.Console/Cli/Internal/CommandBinder.cs create mode 100644 src/Spectre.Console/Cli/Internal/CommandExecutor.cs create mode 100644 src/Spectre.Console/Cli/Internal/CommandPart.cs create mode 100644 src/Spectre.Console/Cli/Internal/CommandSuggestor.cs create mode 100644 src/Spectre.Console/Cli/Internal/CommandValidator.cs create mode 100644 src/Spectre.Console/Cli/Internal/Commands/VersionCommand.cs create mode 100644 src/Spectre.Console/Cli/Internal/Commands/XmlDocCommand.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composer.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composition/Activators.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composition/ComponentRegistration.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composition/ComponentRegistry.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composition/DefaultTypeRegistrar.cs create mode 100644 src/Spectre.Console/Cli/Internal/Composition/DefaultTypeResolver.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/CommandConfigurator.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/ConfigurationHelper.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/Configurator.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/Configurator`1.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/ConfiguredCommand.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/IConfiguration.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/TemplateParser.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/TemplateToken.cs create mode 100644 src/Spectre.Console/Cli/Internal/Configuration/TemplateTokenizer.cs create mode 100644 src/Spectre.Console/Cli/Internal/Constants.cs create mode 100644 src/Spectre.Console/Cli/Internal/DefaultPairDeconstructor.cs create mode 100644 src/Spectre.Console/Cli/Internal/DelegateCommand.cs create mode 100644 src/Spectre.Console/Cli/Internal/Exceptions/CommandLineParseExceptionFactory.cs create mode 100644 src/Spectre.Console/Cli/Internal/Exceptions/CommandLineTemplateExceptionFactory.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/AnsiConsoleExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/CaseSensitivityExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/ListExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/StringExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/TypeExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/TypeRegistrarExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Extensions/XmlElementExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/HelpWriter.cs create mode 100644 src/Spectre.Console/Cli/Internal/IPairDeconstructor.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandContainerExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandInfoExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandModelValidator.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/CommandParameterComparer.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/ICommandContainer.cs create mode 100644 src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTree.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeExtensions.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParser.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserContext.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserResult.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeToken.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenStream.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizer.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizerContext.cs create mode 100644 src/Spectre.Console/Cli/Internal/Parsing/MappedCommandParameter.cs create mode 100644 src/Spectre.Console/Cli/Internal/ParsingMode.cs create mode 100644 src/Spectre.Console/Cli/Internal/RemainingArguments.cs create mode 100644 src/Spectre.Console/Cli/Internal/StringWriterWithEncoding.cs create mode 100644 src/Spectre.Console/Cli/Internal/TextBuffer.cs create mode 100644 src/Spectre.Console/Cli/Internal/TypeRegistrar.cs create mode 100644 src/Spectre.Console/Cli/Internal/TypeResolverAdapter.cs create mode 100644 src/Spectre.Console/Cli/Internal/VersionHelper.cs create mode 100644 src/Spectre.Console/Cli/PairDeconstuctor.cs create mode 100644 src/Spectre.Console/Cli/Unsafe/IUnsafeBranchConfigurator.cs create mode 100644 src/Spectre.Console/Cli/Unsafe/IUnsafeConfigurator.cs create mode 100644 src/Spectre.Console/Cli/Unsafe/UnsafeConfiguratorExtensions.cs diff --git a/docs/input/appendix/index.cshtml b/docs/input/appendix/index.cshtml index 61dcd86..0485f88 100644 --- a/docs/input/appendix/index.cshtml +++ b/docs/input/appendix/index.cshtml @@ -1,5 +1,5 @@ Title: Appendix -Order: 10 +Order: 100 ---

Sections

diff --git a/docs/input/cli/index.cshtml b/docs/input/cli/index.cshtml new file mode 100644 index 0000000..4f61d5f --- /dev/null +++ b/docs/input/cli/index.cshtml @@ -0,0 +1,12 @@ +Title: CLI +Order: 10 +--- + +

Sections

+ + \ No newline at end of file diff --git a/docs/input/cli/introduction.md b/docs/input/cli/introduction.md new file mode 100644 index 0000000..a38f04d --- /dev/null +++ b/docs/input/cli/introduction.md @@ -0,0 +1,117 @@ +Title: Introduction +Order: 1 +--- + +`Spectre.Console.Cli` is a modern library for parsing command line arguments. While it's extremely +opinionated in what it does, it tries to follow established industry conventions, and draws +its inspiration from applications you use everyday. + +# How does it work? + +The underlying philosophy behind `Spectre.Console.Cli` is to rely on the .NET type system to +declare the commands, but tie everything together via composition. + +Imagine the following command structure: + +* dotnet *(executable)* + * add `[PROJECT]` + * package `` --version `` + * reference `` + +For this I would like to implement the commands (the different levels in the tree that +executes something) separately from the settings (the options, flags and arguments), +which I want to be able to inherit from each other. + +## Specify the settings + +We start by creating some settings that represents the options, flags and arguments +that we want to act upon. + +```csharp +public class AddSettings : CommandSettings +{ + [CommandArgument(0, "[PROJECT]")] + public string Project { get; set; } +} + +public class AddPackageSettings : AddSettings +{ + [CommandArgument(0, "")] + public string PackageName { get; set; } + + [CommandOption("-v|--version ")] + public string Version { get; set; } +} + +public class AddReferenceSettings : AddSettings +{ + [CommandArgument(0, "")] + public string ProjectReference { get; set; } +} +``` + +## Specify the commands + +Now it's time to specify the commands that act on the settings we created +in the previous step. + +```csharp +public class AddPackageCommand : Command +{ + public override int Execute(AddPackageSettings settings, ILookup remaining) + { + // Omitted + } +} + +public class AddReferenceCommand : Command +{ + public override int Execute(AddReferenceSettings settings, ILookup remaining) + { + // Omitted + } +} +``` + +## Let's tie it together + +Now when we have our commands and settings implemented, we can compose a command tree +that tells the parser how to interpret user input. + +```csharp +using Spectre.Console.Cli; + +namespace MyApp +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + + app.Configure(config => + { + config.AddBranch("add", add => + { + add.AddCommand("package"); + add.AddCommand("reference"); + }); + }); + + return app.Run(args); + } + } +} +``` + +# So why this way? + +Now you might wonder, why do things like this? Well, for starters the different parts +of the application are separated, while still having the option to share things like options, +flags and arguments between them. + +This make the resulting code very clean and easy to navigate, not to mention to unit test. +And most importantly at all, the type system guides me to do the right thing. I can't configure +commands in non-compatible ways, and if I want to add a new top-level `add-package` command +(or move the command completely), it's just a single line to change. This makes it easy to +experiment and makes the CLI experience a first class citizen of your application. \ No newline at end of file diff --git a/dotnet-tools.json b/dotnet-tools.json index 57b0606..720fc54 100644 --- a/dotnet-tools.json +++ b/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "cake.tool": { - "version": "1.0.0-rc0001", + "version": "1.0.0-rc0002", "commands": [ "dotnet-cake" ] @@ -15,7 +15,7 @@ ] }, "dotnet-example": { - "version": "1.1.0", + "version": "1.2.0", "commands": [ "dotnet-example" ] diff --git a/examples/Borders/Borders.csproj b/examples/Borders/Borders.csproj deleted file mode 100644 index 492bbd5..0000000 --- a/examples/Borders/Borders.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Borders - Demonstrates the different kind of borders. - - - - - - - diff --git a/examples/Calendars/Calendars.csproj b/examples/Calendars/Calendars.csproj deleted file mode 100644 index 116dfa2..0000000 --- a/examples/Calendars/Calendars.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Calendars - Demonstrates how to render calendars. - - - - - - - diff --git a/examples/Canvas/Canvas.csproj b/examples/Canvas/Canvas.csproj deleted file mode 100644 index a98bd15..0000000 --- a/examples/Canvas/Canvas.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - Exe - netcoreapp3.1 - false - Canvas - Demonstrates how to render pixels and images. - - - - - - - - - - PreserveNewest - - - - diff --git a/examples/Charts/Charts.csproj b/examples/Charts/Charts.csproj deleted file mode 100644 index 5b16a50..0000000 --- a/examples/Charts/Charts.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Charts - Demonstrates how to render charts in a console. - - - - - - - diff --git a/examples/Cli/Delegates/BarSettings.cs b/examples/Cli/Delegates/BarSettings.cs new file mode 100644 index 0000000..75fa93c --- /dev/null +++ b/examples/Cli/Delegates/BarSettings.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Delegates +{ + public static partial class Program + { + public sealed class BarSettings : CommandSettings + { + [CommandOption("--count")] + [Description("The number of bars to print")] + [DefaultValue(1)] + public int Count { get; set; } + } + } +} diff --git a/examples/Cli/Delegates/Delegates.csproj b/examples/Cli/Delegates/Delegates.csproj new file mode 100644 index 0000000..4c6756c --- /dev/null +++ b/examples/Cli/Delegates/Delegates.csproj @@ -0,0 +1,17 @@ + + + + Exe + net5.0 + false + Delegates + Demonstrates how to specify commands as delegates. + Cli + false + + + + + + + diff --git a/examples/Cli/Delegates/Program.cs b/examples/Cli/Delegates/Program.cs new file mode 100644 index 0000000..6592372 --- /dev/null +++ b/examples/Cli/Delegates/Program.cs @@ -0,0 +1,39 @@ +using System; +using Spectre.Console.Cli; + +namespace Delegates +{ + public static partial class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + app.Configure(config => + { + config.AddDelegate("foo", Foo) + .WithDescription("Foos the bars"); + + config.AddDelegate("bar", Bar) + .WithDescription("Bars the foos"); ; + }); + + return app.Run(args); + } + + private static int Foo(CommandContext context) + { + Console.WriteLine("Foo"); + return 0; + } + + private static int Bar(CommandContext context, BarSettings settings) + { + for (var index = 0; index < settings.Count; index++) + { + Console.WriteLine("Bar"); + } + + return 0; + } + } +} diff --git a/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs b/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs new file mode 100644 index 0000000..16fec31 --- /dev/null +++ b/examples/Cli/Demo/Commands/Add/AddPackageCommand.cs @@ -0,0 +1,47 @@ +using System.ComponentModel; +using Demo.Utilities; +using Spectre.Console.Cli; + +namespace Demo.Commands +{ + [Description("Add a NuGet package reference to the project.")] + public sealed class AddPackageCommand : Command + { + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public string PackageName { get; set; } + + [CommandOption("-v|--version ")] + [Description("The version of the package to add.")] + public string Version { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--no-restore")] + [Description("Add the reference without performing restore preview and compatibility check.")] + public bool NoRestore { get; set; } + + [CommandOption("--source ")] + [Description("The NuGet package source to use during the restore.")] + public string Source { get; set; } + + [CommandOption("--package-directory ")] + [Description("The directory to restore packages to.")] + public string PackageDirectory { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } + } +} diff --git a/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs b/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs new file mode 100644 index 0000000..6229743 --- /dev/null +++ b/examples/Cli/Demo/Commands/Add/AddReferenceCommand.cs @@ -0,0 +1,30 @@ +using System.ComponentModel; +using Demo.Utilities; +using Spectre.Console.Cli; + +namespace Demo.Commands +{ + public sealed class AddReferenceCommand : Command + { + public sealed class Settings : AddSettings + { + [CommandArgument(0, "")] + [Description("The package reference to add.")] + public string ProjectPath { get; set; } + + [CommandOption("-f|--framework ")] + [Description("Add the reference only when targeting a specific framework.")] + public string Framework { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public bool Interactive { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } + } +} diff --git a/examples/Cli/Demo/Commands/Add/AddSettings.cs b/examples/Cli/Demo/Commands/Add/AddSettings.cs new file mode 100644 index 0000000..95743ac --- /dev/null +++ b/examples/Cli/Demo/Commands/Add/AddSettings.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Demo.Commands +{ + public abstract class AddSettings : CommandSettings + { + [CommandArgument(0, "")] + [Description("The project file to operate on. If a file is not specified, the command will search the current directory for one.")] + public string Project { get; set; } + } +} diff --git a/examples/Cli/Demo/Commands/Run/RunCommand.cs b/examples/Cli/Demo/Commands/Run/RunCommand.cs new file mode 100644 index 0000000..772b572 --- /dev/null +++ b/examples/Cli/Demo/Commands/Run/RunCommand.cs @@ -0,0 +1,70 @@ +using System.ComponentModel; +using Demo.Utilities; +using Spectre.Console.Cli; + +namespace Demo.Commands +{ + [Description("Build and run a .NET project output.")] + public sealed class RunCommand : Command + { + public sealed class Settings : CommandSettings + { + [CommandOption("-c|--configuration ")] + [Description("The configuration to run for. The default for most projects is '[grey]Debug[/]'.")] + [DefaultValue("Debug")] + public string Configuration { get; set; } + + [CommandOption("-f|--framework ")] + [Description("The target framework to run for. The target framework must also be specified in the project file.")] + public string Framework { get; set; } + + [CommandOption("-r|--runtime ")] + [Description("The target runtime to run for.")] + public string RuntimeIdentifier { get; set; } + + [CommandOption("-p|--project ")] + [Description("The path to the project file to run (defaults to the current directory if there is only one project).")] + public string ProjectPath { get; set; } + + [CommandOption("--launch-profile ")] + [Description("The name of the launch profile (if any) to use when launching the application.")] + public string LaunchProfile { get; set; } + + [CommandOption("--no-launch-profile")] + [Description("Do not attempt to use [grey]launchSettings.json[/] to configure the application.")] + public bool NoLaunchProfile { get; set; } + + [CommandOption("--no-build")] + [Description("Do not build the project before running. Implies [grey]--no-restore[/].")] + public bool NoBuild { get; set; } + + [CommandOption("--interactive")] + [Description("Allows the command to stop and wait for user input or action (for example to complete authentication).")] + public string Interactive { get; set; } + + [CommandOption("--no-restore")] + [Description("Do not restore the project before building.")] + public bool NoRestore { get; set; } + + [CommandOption("--verbosity ")] + [Description("Set the MSBuild verbosity level. Allowed values are q[grey]uiet[/], m[grey]inimal[/], n[grey]ormal[/], d[grey]etailed[/], and diag[grey]nostic[/].")] + [TypeConverter(typeof(VerbosityConverter))] + [DefaultValue(Verbosity.Normal)] + public Verbosity Verbosity { get; set; } + + [CommandOption("--no-dependencies")] + [Description("Do not restore project-to-project references and only restore the specified project.")] + public bool NoDependencies { get; set; } + + [CommandOption("--force")] + [Description("Force all dependencies to be resolved even if the last restore was successful. This is equivalent to deleting [grey]project.assets.json[/].")] + public bool Force { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + SettingsDumper.Dump(settings); + return 0; + } + } +} diff --git a/examples/Cli/Demo/Commands/Serve/ServeCommand.cs b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs new file mode 100644 index 0000000..49298c5 --- /dev/null +++ b/examples/Cli/Demo/Commands/Serve/ServeCommand.cs @@ -0,0 +1,41 @@ +using System; +using System.ComponentModel; +using Demo.Utilities; +using Spectre.Console.Cli; + +namespace Demo.Commands +{ + [Description("Launches a web server in the current working directory and serves all files in it.")] + public sealed class ServeCommand : Command + { + public sealed class Settings : CommandSettings + { + [CommandOption("-p|--port ")] + [Description("Port to use. Defaults to [grey]8080[/]. Use [grey]0[/] for a dynamic port.")] + public int Port { get; set; } + + [CommandOption("-o|--open-browser [BROWSER]")] + [Description("Open a web browser when the server starts. You can also specify which browser to use. If none is specified, the default one will be used.")] + public FlagValue OpenBrowser { get; set; } + } + + public override int Execute(CommandContext context, Settings settings) + { + if (settings.OpenBrowser.IsSet) + { + var browser = settings.OpenBrowser.Value; + if (browser != null) + { + Console.WriteLine($"Open in {browser}"); + } + else + { + Console.WriteLine($"Open in default browser."); + } + } + + SettingsDumper.Dump(settings); + return 0; + } + } +} diff --git a/examples/Cli/Demo/Demo.csproj b/examples/Cli/Demo/Demo.csproj new file mode 100644 index 0000000..86b43be --- /dev/null +++ b/examples/Cli/Demo/Demo.csproj @@ -0,0 +1,17 @@ + + + + Exe + net5.0 + false + Demo + Demonstrates the most common use cases of Spectre.Cli. + Cli + false + + + + + + + diff --git a/examples/Cli/Demo/Program.cs b/examples/Cli/Demo/Program.cs new file mode 100644 index 0000000..e63fa04 --- /dev/null +++ b/examples/Cli/Demo/Program.cs @@ -0,0 +1,37 @@ +using Demo.Commands; +using Spectre.Console.Cli; + +namespace Demo +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + app.Configure(config => + { + config.SetApplicationName("fake-dotnet"); + config.ValidateExamples(); + config.AddExample(new[] { "run", "--no-build" }); + + // Run + config.AddCommand("run"); + + // Add + config.AddBranch("add", add => + { + add.SetDescription("Add a package or reference to a .NET project"); + add.AddCommand("package"); + add.AddCommand("reference"); + }); + + // Serve + config.AddCommand("serve") + .WithExample(new[] { "serve", "-o", "firefox" }) + .WithExample(new[] { "serve", "--port", "80", "-o", "firefox" }); + }); + + return app.Run(args); + } + } +} diff --git a/examples/Cli/Demo/Utilities/SettingsDumper.cs b/examples/Cli/Demo/Utilities/SettingsDumper.cs new file mode 100644 index 0000000..f46d5cf --- /dev/null +++ b/examples/Cli/Demo/Utilities/SettingsDumper.cs @@ -0,0 +1,29 @@ +using Spectre.Console; +using Spectre.Console.Cli; + +namespace Demo.Utilities +{ + public static class SettingsDumper + { + public static void Dump(CommandSettings settings) + { + var table = new Table().RoundedBorder(); + table.AddColumn("[grey]Name[/]"); + table.AddColumn("[grey]Value[/]"); + + var properties = settings.GetType().GetProperties(); + foreach (var property in properties) + { + var value = property.GetValue(settings) + ?.ToString() + ?.Replace("[", "[["); + + table.AddRow( + property.Name, + value ?? "[grey]null[/]"); + } + + AnsiConsole.Render(table); + } + } +} diff --git a/examples/Cli/Demo/Verbosity.cs b/examples/Cli/Demo/Verbosity.cs new file mode 100644 index 0000000..498a8c8 --- /dev/null +++ b/examples/Cli/Demo/Verbosity.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; + +namespace Demo +{ + public enum Verbosity + { + Quiet, + Minimal, + Normal, + Detailed, + Diagnostic + } + + public sealed class VerbosityConverter : TypeConverter + { + private readonly Dictionary _lookup; + + public VerbosityConverter() + { + _lookup = new Dictionary(StringComparer.OrdinalIgnoreCase) + { + { "q", Verbosity.Quiet }, + { "quiet", Verbosity.Quiet }, + { "m", Verbosity.Minimal }, + { "minimal", Verbosity.Minimal }, + { "n", Verbosity.Normal }, + { "normal", Verbosity.Normal }, + { "d", Verbosity.Detailed }, + { "detailed", Verbosity.Detailed }, + { "diag", Verbosity.Diagnostic }, + { "diagnostic", Verbosity.Diagnostic } + }; + } + + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string stringValue) + { + var result = _lookup.TryGetValue(stringValue, out var verbosity); + if (!result) + { + const string format = "The value '{0}' is not a valid verbosity."; + var message = string.Format(CultureInfo.InvariantCulture, format, value); + throw new InvalidOperationException(message); + } + return verbosity; + } + throw new NotSupportedException("Can't convert value to verbosity."); + } + } +} diff --git a/examples/Cli/Dynamic/Dynamic.csproj b/examples/Cli/Dynamic/Dynamic.csproj new file mode 100644 index 0000000..2066c58 --- /dev/null +++ b/examples/Cli/Dynamic/Dynamic.csproj @@ -0,0 +1,17 @@ + + + + Exe + net5.0 + false + Dynamic + Demonstrates how to define dynamic commands. + Cli + false + + + + + + + diff --git a/examples/Cli/Dynamic/MyCommand.cs b/examples/Cli/Dynamic/MyCommand.cs new file mode 100644 index 0000000..b16caa8 --- /dev/null +++ b/examples/Cli/Dynamic/MyCommand.cs @@ -0,0 +1,20 @@ +using System; +using Spectre.Console.Cli; + +namespace Dynamic +{ + public sealed class MyCommand : Command + { + public override int Execute(CommandContext context) + { + if (!(context.Data is int data)) + { + throw new InvalidOperationException("Command has no associated data."); + + } + + Console.WriteLine("Value = {0}", data); + return 0; + } + } +} diff --git a/examples/Cli/Dynamic/Program.cs b/examples/Cli/Dynamic/Program.cs new file mode 100644 index 0000000..519fab7 --- /dev/null +++ b/examples/Cli/Dynamic/Program.cs @@ -0,0 +1,24 @@ +using System.Linq; +using Spectre.Console.Cli; + +namespace Dynamic +{ + public static class Program + { + public static int Main(string[] args) + { + var app = new CommandApp(); + app.Configure(config => + { + foreach(var index in Enumerable.Range(1, 10)) + { + config.AddCommand($"c{index}") + .WithDescription($"Prints the number {index}") + .WithData(index); + } + }); + + return app.Run(args); + } + } +} diff --git a/examples/Cli/Injection/Commands/DefaultCommand.cs b/examples/Cli/Injection/Commands/DefaultCommand.cs new file mode 100644 index 0000000..1ae059f --- /dev/null +++ b/examples/Cli/Injection/Commands/DefaultCommand.cs @@ -0,0 +1,30 @@ +using System; +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Injection.Commands +{ + public sealed class DefaultCommand : Command + { + private readonly IGreeter _greeter; + + public sealed class Settings : CommandSettings + { + [CommandOption("-n|--name ")] + [Description("The person or thing to greet.")] + [DefaultValue("World")] + public string Name { get; set; } + } + + public DefaultCommand(IGreeter greeter) + { + _greeter = greeter ?? throw new ArgumentNullException(nameof(greeter)); + } + + public override int Execute(CommandContext context, Settings settings) + { + _greeter.Greet(settings.Name); + return 0; + } + } +} diff --git a/examples/Cli/Injection/IGreeter.cs b/examples/Cli/Injection/IGreeter.cs new file mode 100644 index 0000000..a2c285b --- /dev/null +++ b/examples/Cli/Injection/IGreeter.cs @@ -0,0 +1,17 @@ +using System; + +namespace Injection +{ + public interface IGreeter + { + void Greet(string name); + } + + public sealed class HelloWorldGreeter : IGreeter + { + public void Greet(string name) + { + Console.WriteLine($"Hello {name}!"); + } + } +} diff --git a/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs b/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs new file mode 100644 index 0000000..0632649 --- /dev/null +++ b/examples/Cli/Injection/Infrastructure/TypeRegistrar.cs @@ -0,0 +1,31 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; + +namespace Injection +{ + public sealed class TypeRegistrar : ITypeRegistrar + { + private readonly IServiceCollection _builder; + + public TypeRegistrar(IServiceCollection builder) + { + _builder = builder; + } + + public ITypeResolver Build() + { + return new TypeResolver(_builder.BuildServiceProvider()); + } + + public void Register(Type service, Type implementation) + { + _builder.AddSingleton(service, implementation); + } + + public void RegisterInstance(Type service, object implementation) + { + _builder.AddSingleton(service, implementation); + } + } +} diff --git a/examples/Cli/Injection/Infrastructure/TypeResolver.cs b/examples/Cli/Injection/Infrastructure/TypeResolver.cs new file mode 100644 index 0000000..50320e2 --- /dev/null +++ b/examples/Cli/Injection/Infrastructure/TypeResolver.cs @@ -0,0 +1,21 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; + +namespace Injection +{ + public sealed class TypeResolver : ITypeResolver + { + private readonly IServiceProvider _provider; + + public TypeResolver(IServiceProvider provider) + { + _provider = provider ?? throw new ArgumentNullException(nameof(provider)); + } + + public object Resolve(Type type) + { + return _provider.GetRequiredService(type); + } + } +} diff --git a/examples/Cli/Injection/Injection.csproj b/examples/Cli/Injection/Injection.csproj new file mode 100644 index 0000000..4c14b62 --- /dev/null +++ b/examples/Cli/Injection/Injection.csproj @@ -0,0 +1,21 @@ + + + + Exe + net5.0 + false + Injection + Demonstrates how to use dependency injection with Spectre.Cli. + Cli + false + + + + + + + + + + + diff --git a/examples/Cli/Injection/Program.cs b/examples/Cli/Injection/Program.cs new file mode 100644 index 0000000..1206df9 --- /dev/null +++ b/examples/Cli/Injection/Program.cs @@ -0,0 +1,23 @@ +using Injection.Commands; +using Microsoft.Extensions.DependencyInjection; +using Spectre.Console.Cli; + +namespace Injection +{ + public class Program + { + public static int Main(string[] args) + { + // Create a type registrar and register any dependencies. + // A type registrar is an adapter for a DI framework. + var registrations = new ServiceCollection(); + registrations.AddSingleton(); + var registrar = new TypeRegistrar(registrations); + + // Create a new command app with the registrar + // and run it with the provided arguments. + var app = new CommandApp(registrar); + return app.Run(args); + } + } +} diff --git a/examples/Colors/Colors.csproj b/examples/Colors/Colors.csproj deleted file mode 100644 index 55ab209..0000000 --- a/examples/Colors/Colors.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Colors - Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console. - - - - - - - diff --git a/examples/Console/Borders/Borders.csproj b/examples/Console/Borders/Borders.csproj new file mode 100644 index 0000000..f51cc16 --- /dev/null +++ b/examples/Console/Borders/Borders.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Borders + Demonstrates the different kind of borders. + Widgets + + + + + + + diff --git a/examples/Borders/Program.cs b/examples/Console/Borders/Program.cs similarity index 100% rename from examples/Borders/Program.cs rename to examples/Console/Borders/Program.cs diff --git a/examples/Console/Calendars/Calendars.csproj b/examples/Console/Calendars/Calendars.csproj new file mode 100644 index 0000000..119a479 --- /dev/null +++ b/examples/Console/Calendars/Calendars.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Calendars + Demonstrates how to render calendars. + Widgets + + + + + + + diff --git a/examples/Calendars/Program.cs b/examples/Console/Calendars/Program.cs similarity index 100% rename from examples/Calendars/Program.cs rename to examples/Console/Calendars/Program.cs diff --git a/examples/Console/Canvas/Canvas.csproj b/examples/Console/Canvas/Canvas.csproj new file mode 100644 index 0000000..1a07982 --- /dev/null +++ b/examples/Console/Canvas/Canvas.csproj @@ -0,0 +1,22 @@ + + + + Exe + netcoreapp3.1 + Canvas + Demonstrates how to render pixels and images. + Widgets + + + + + + + + + + PreserveNewest + + + + diff --git a/examples/Canvas/Mandelbrot.cs b/examples/Console/Canvas/Mandelbrot.cs similarity index 100% rename from examples/Canvas/Mandelbrot.cs rename to examples/Console/Canvas/Mandelbrot.cs diff --git a/examples/Canvas/Program.cs b/examples/Console/Canvas/Program.cs similarity index 100% rename from examples/Canvas/Program.cs rename to examples/Console/Canvas/Program.cs diff --git a/examples/Canvas/cake.png b/examples/Console/Canvas/cake.png similarity index 100% rename from examples/Canvas/cake.png rename to examples/Console/Canvas/cake.png diff --git a/examples/Console/Charts/Charts.csproj b/examples/Console/Charts/Charts.csproj new file mode 100644 index 0000000..3763e82 --- /dev/null +++ b/examples/Console/Charts/Charts.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Charts + Demonstrates how to render charts in a console. + Widgets + + + + + + + diff --git a/examples/Charts/Program.cs b/examples/Console/Charts/Program.cs similarity index 100% rename from examples/Charts/Program.cs rename to examples/Console/Charts/Program.cs diff --git a/examples/Console/Colors/Colors.csproj b/examples/Console/Colors/Colors.csproj new file mode 100644 index 0000000..32185ad --- /dev/null +++ b/examples/Console/Colors/Colors.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Colors + Demonstrates how to use [yellow]c[/][red]o[/][green]l[/][blue]o[/][aqua]r[/][lime]s[/] in the console. + Misc + + + + + + + diff --git a/examples/Colors/Program.cs b/examples/Console/Colors/Program.cs similarity index 100% rename from examples/Colors/Program.cs rename to examples/Console/Colors/Program.cs diff --git a/examples/Colors/Utilities.cs b/examples/Console/Colors/Utilities.cs similarity index 100% rename from examples/Colors/Utilities.cs rename to examples/Console/Colors/Utilities.cs diff --git a/examples/Columns/Columns.csproj b/examples/Console/Columns/Columns.csproj similarity index 52% rename from examples/Columns/Columns.csproj rename to examples/Console/Columns/Columns.csproj index efbe910..474b855 100644 --- a/examples/Columns/Columns.csproj +++ b/examples/Console/Columns/Columns.csproj @@ -3,9 +3,9 @@ Exe net5.0 - false - Columns - Demonstrates how to render data into columns. + Columns + Demonstrates how to render data into columns. + Widgets @@ -13,7 +13,7 @@ - + diff --git a/examples/Columns/Program.cs b/examples/Console/Columns/Program.cs similarity index 100% rename from examples/Columns/Program.cs rename to examples/Console/Columns/Program.cs diff --git a/examples/Columns/User.cs b/examples/Console/Columns/User.cs similarity index 100% rename from examples/Columns/User.cs rename to examples/Console/Columns/User.cs diff --git a/examples/Console/Cursor/Cursor.csproj b/examples/Console/Cursor/Cursor.csproj new file mode 100644 index 0000000..9daddac --- /dev/null +++ b/examples/Console/Cursor/Cursor.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Cursor + Demonstrates how to move the cursor. + Misc + + + + + + + diff --git a/examples/Cursor/Program.cs b/examples/Console/Cursor/Program.cs similarity index 100% rename from examples/Cursor/Program.cs rename to examples/Console/Cursor/Program.cs diff --git a/examples/Console/Emojis/Emojis.csproj b/examples/Console/Emojis/Emojis.csproj new file mode 100644 index 0000000..eceb0bd --- /dev/null +++ b/examples/Console/Emojis/Emojis.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Emojis + Demonstrates how to render emojis. + Misc + + + + + + + diff --git a/examples/Emojis/Program.cs b/examples/Console/Emojis/Program.cs similarity index 100% rename from examples/Emojis/Program.cs rename to examples/Console/Emojis/Program.cs diff --git a/examples/Console/Exceptions/Exceptions.csproj b/examples/Console/Exceptions/Exceptions.csproj new file mode 100644 index 0000000..128d8c0 --- /dev/null +++ b/examples/Console/Exceptions/Exceptions.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Exceptions + Demonstrates how to render formatted exceptions. + Misc + + + + + + + diff --git a/examples/Exceptions/Program.cs b/examples/Console/Exceptions/Program.cs similarity index 100% rename from examples/Exceptions/Program.cs rename to examples/Console/Exceptions/Program.cs diff --git a/examples/Console/Figlet/Figlet.csproj b/examples/Console/Figlet/Figlet.csproj new file mode 100644 index 0000000..fbac2d6 --- /dev/null +++ b/examples/Console/Figlet/Figlet.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Figlet + Demonstrates how to render FIGlet text. + Widgets + + + + + + + diff --git a/examples/Figlet/Program.cs b/examples/Console/Figlet/Program.cs similarity index 100% rename from examples/Figlet/Program.cs rename to examples/Console/Figlet/Program.cs diff --git a/examples/Console/Grids/Grids.csproj b/examples/Console/Grids/Grids.csproj new file mode 100644 index 0000000..95c9004 --- /dev/null +++ b/examples/Console/Grids/Grids.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Grids + Demonstrates how to render grids in a console. + Widgets + + + + + + + diff --git a/examples/Grids/Program.cs b/examples/Console/Grids/Program.cs similarity index 100% rename from examples/Grids/Program.cs rename to examples/Console/Grids/Program.cs diff --git a/examples/Console/Info/Info.csproj b/examples/Console/Info/Info.csproj new file mode 100644 index 0000000..f4ec75f --- /dev/null +++ b/examples/Console/Info/Info.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Info + Displays the capabilities of the current console. + Misc + + + + + + + diff --git a/examples/Info/Program.cs b/examples/Console/Info/Program.cs similarity index 100% rename from examples/Info/Program.cs rename to examples/Console/Info/Program.cs diff --git a/examples/Console/Links/Links.csproj b/examples/Console/Links/Links.csproj new file mode 100644 index 0000000..5054afa --- /dev/null +++ b/examples/Console/Links/Links.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Links + Demonstrates how to render links in a console. + Misc + + + + + + + diff --git a/examples/Links/Program.cs b/examples/Console/Links/Program.cs similarity index 100% rename from examples/Links/Program.cs rename to examples/Console/Links/Program.cs diff --git a/examples/Console/Panels/Panels.csproj b/examples/Console/Panels/Panels.csproj new file mode 100644 index 0000000..03add39 --- /dev/null +++ b/examples/Console/Panels/Panels.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Panels + Demonstrates how to render items in panels. + Widgets + + + + + + + diff --git a/examples/Panels/Program.cs b/examples/Console/Panels/Program.cs similarity index 100% rename from examples/Panels/Program.cs rename to examples/Console/Panels/Program.cs diff --git a/examples/Progress/DescriptionGenerator.cs b/examples/Console/Progress/DescriptionGenerator.cs similarity index 100% rename from examples/Progress/DescriptionGenerator.cs rename to examples/Console/Progress/DescriptionGenerator.cs diff --git a/examples/Progress/Program.cs b/examples/Console/Progress/Program.cs similarity index 100% rename from examples/Progress/Program.cs rename to examples/Console/Progress/Program.cs diff --git a/examples/Progress/Progress.csproj b/examples/Console/Progress/Progress.csproj similarity index 53% rename from examples/Progress/Progress.csproj rename to examples/Console/Progress/Progress.csproj index f84c230..859b3f2 100644 --- a/examples/Progress/Progress.csproj +++ b/examples/Console/Progress/Progress.csproj @@ -3,9 +3,9 @@ Exe net5.0 - false - Progress - Demonstrates how to show progress bars. + Progress + Demonstrates how to show progress bars. + Status @@ -13,7 +13,7 @@ - + diff --git a/examples/Prompt/Program.cs b/examples/Console/Prompt/Program.cs similarity index 100% rename from examples/Prompt/Program.cs rename to examples/Console/Prompt/Program.cs diff --git a/examples/Console/Prompt/Prompt.csproj b/examples/Console/Prompt/Prompt.csproj new file mode 100644 index 0000000..9c861c6 --- /dev/null +++ b/examples/Console/Prompt/Prompt.csproj @@ -0,0 +1,16 @@ + + + + Exe + netcoreapp3.1 + 9 + Prompt + Demonstrates how to get input from a user. + Misc + + + + + + + diff --git a/examples/Rules/Program.cs b/examples/Console/Rules/Program.cs similarity index 100% rename from examples/Rules/Program.cs rename to examples/Console/Rules/Program.cs diff --git a/examples/Console/Rules/Rules.csproj b/examples/Console/Rules/Rules.csproj new file mode 100644 index 0000000..7d590fc --- /dev/null +++ b/examples/Console/Rules/Rules.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Rules + Demonstrates how to render horizontal rules (lines). + Widgets + + + + + + + diff --git a/examples/Status/Program.cs b/examples/Console/Status/Program.cs similarity index 100% rename from examples/Status/Program.cs rename to examples/Console/Status/Program.cs diff --git a/examples/Status/Status.csproj b/examples/Console/Status/Status.csproj similarity index 53% rename from examples/Status/Status.csproj rename to examples/Console/Status/Status.csproj index 16128da..eb20f18 100644 --- a/examples/Status/Status.csproj +++ b/examples/Console/Status/Status.csproj @@ -3,9 +3,9 @@ Exe net5.0 - false - Status - Demonstrates how to show status updates. + Status + Demonstrates how to show status updates. + Status @@ -13,7 +13,7 @@ - + diff --git a/examples/Tables/Program.cs b/examples/Console/Tables/Program.cs similarity index 100% rename from examples/Tables/Program.cs rename to examples/Console/Tables/Program.cs diff --git a/examples/Console/Tables/Tables.csproj b/examples/Console/Tables/Tables.csproj new file mode 100644 index 0000000..451f79a --- /dev/null +++ b/examples/Console/Tables/Tables.csproj @@ -0,0 +1,15 @@ + + + + Exe + net5.0 + Tables + Demonstrates how to render tables in a console. + Widgets + + + + + + + diff --git a/examples/Cursor/Cursor.csproj b/examples/Cursor/Cursor.csproj deleted file mode 100644 index 721c41b..0000000 --- a/examples/Cursor/Cursor.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Cursor - Demonstrates how to move the cursor. - - - - - - - diff --git a/examples/Directory.Build.props b/examples/Directory.Build.props new file mode 100644 index 0000000..3080093 --- /dev/null +++ b/examples/Directory.Build.props @@ -0,0 +1,5 @@ + + + false + + \ No newline at end of file diff --git a/examples/Emojis/Emojis.csproj b/examples/Emojis/Emojis.csproj deleted file mode 100644 index 7fa39a9..0000000 --- a/examples/Emojis/Emojis.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Emojis - Demonstrates how to render emojis. - - - - - - - diff --git a/examples/Exceptions/Exceptions.csproj b/examples/Exceptions/Exceptions.csproj deleted file mode 100644 index 7ff8afa..0000000 --- a/examples/Exceptions/Exceptions.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Exceptions - Demonstrates how to render formatted exceptions. - - - - - - - diff --git a/examples/Figlet/Figlet.csproj b/examples/Figlet/Figlet.csproj deleted file mode 100644 index de0a616..0000000 --- a/examples/Figlet/Figlet.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Figlet - Demonstrates how to render FIGlet text. - - - - - - - diff --git a/examples/Grids/Grids.csproj b/examples/Grids/Grids.csproj deleted file mode 100644 index 914a1de..0000000 --- a/examples/Grids/Grids.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Grids - Demonstrates how to render grids in a console. - - - - - - - diff --git a/examples/Info/Info.csproj b/examples/Info/Info.csproj deleted file mode 100644 index bde96a4..0000000 --- a/examples/Info/Info.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Info - Displays the capabilities of the current console. - - - - - - - diff --git a/examples/Links/Links.csproj b/examples/Links/Links.csproj deleted file mode 100644 index 3b91568..0000000 --- a/examples/Links/Links.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Links - Demonstrates how to render links in a console. - - - - - - - diff --git a/examples/Panels/Panels.csproj b/examples/Panels/Panels.csproj deleted file mode 100644 index 21abf28..0000000 --- a/examples/Panels/Panels.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Panels - Demonstrates how to render items in panels. - - - - - - - diff --git a/examples/Prompt/Prompt.csproj b/examples/Prompt/Prompt.csproj deleted file mode 100644 index 21559d0..0000000 --- a/examples/Prompt/Prompt.csproj +++ /dev/null @@ -1,16 +0,0 @@ - - - - Exe - netcoreapp3.1 - 9 - false - Prompt - Demonstrates how to get input from a user. - - - - - - - diff --git a/examples/Rules/Rules.csproj b/examples/Rules/Rules.csproj deleted file mode 100644 index 9522281..0000000 --- a/examples/Rules/Rules.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Rules - Demonstrates how to render horizontal rules (lines). - - - - - - - diff --git a/examples/Tables/Tables.csproj b/examples/Tables/Tables.csproj deleted file mode 100644 index 287b416..0000000 --- a/examples/Tables/Tables.csproj +++ /dev/null @@ -1,15 +0,0 @@ - - - - Exe - net5.0 - false - Tables - Demonstrates how to render tables in a console. - - - - - - - diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a1e8b22..bdf0b83 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -6,6 +6,7 @@ embedded true true + false @@ -32,13 +33,13 @@ - + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + All diff --git a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj index 7e86188..5e2e05f 100644 --- a/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj +++ b/src/Spectre.Console.ImageSharp/Spectre.Console.ImageSharp.csproj @@ -3,6 +3,7 @@ netstandard2.0 enable + true A library that extends Spectre.Console with ImageSharp superpowers. diff --git a/src/Spectre.Console.Testing/.editorconfig b/src/Spectre.Console.Testing/.editorconfig new file mode 100644 index 0000000..70136ae --- /dev/null +++ b/src/Spectre.Console.Testing/.editorconfig @@ -0,0 +1,14 @@ +root = false + +[*.cs] +# CS1591: Missing XML comment for publicly visible type or member +dotnet_diagnostic.CS1591.severity = none + +# SA1600: Elements should be documented +dotnet_diagnostic.SA1600.severity = none + +# Default severity for analyzer diagnostics with category 'StyleCop.CSharp.OrderingRules' +dotnet_analyzer_diagnostic.category-StyleCop.CSharp.OrderingRules.severity = none + +# CA1819: Properties should not return arrays +dotnet_diagnostic.CA1819.severity = none \ No newline at end of file diff --git a/src/Spectre.Console.Testing/CommandAppFixture.cs b/src/Spectre.Console.Testing/CommandAppFixture.cs new file mode 100644 index 0000000..f26e364 --- /dev/null +++ b/src/Spectre.Console.Testing/CommandAppFixture.cs @@ -0,0 +1,92 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Testing +{ + public sealed class CommandAppFixture + { + private Action _appConfiguration = _ => { }; + private Action _configuration; + + public CommandAppFixture() + { + _configuration = (_) => { }; + } + + public CommandAppFixture WithDefaultCommand() + where T : class, ICommand + { + _appConfiguration = (app) => app.SetDefaultCommand(); + return this; + } + + public void Configure(Action action) + { + _configuration = action; + } + + public (string Message, string Output) RunAndCatch(params string[] args) + where T : Exception + { + CommandContext context = null; + CommandSettings settings = null; + + using var console = new FakeConsole(); + + var app = new CommandApp(); + _appConfiguration?.Invoke(app); + + app.Configure(_configuration); + app.Configure(c => c.ConfigureConsole(console)); + app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) => + { + context = ctx; + settings = s; + }))); + + try + { + app.Run(args); + } + catch (T ex) + { + var output = console.Output + .NormalizeLineEndings() + .TrimLines() + .Trim(); + + return (ex.Message, output); + } + + throw new InvalidOperationException("No exception was thrown"); + } + + public (int ExitCode, string Output, CommandContext Context, CommandSettings Settings) Run(params string[] args) + { + CommandContext context = null; + CommandSettings settings = null; + + using var console = new FakeConsole(width: int.MaxValue); + + var app = new CommandApp(); + _appConfiguration?.Invoke(app); + + app.Configure(_configuration); + app.Configure(c => c.ConfigureConsole(console)); + app.Configure(c => c.SetInterceptor(new FakeCommandInterceptor((ctx, s) => + { + context = ctx; + settings = s; + }))); + + var result = app.Run(args); + + var output = console.Output + .NormalizeLineEndings() + .TrimLines() + .Trim(); + + return (result, output, context, settings); + } + } +} diff --git a/src/Spectre.Console.Testing/EmbeddedResourceReader.cs b/src/Spectre.Console.Testing/EmbeddedResourceReader.cs new file mode 100644 index 0000000..0b3fe21 --- /dev/null +++ b/src/Spectre.Console.Testing/EmbeddedResourceReader.cs @@ -0,0 +1,38 @@ +using System; +using System.IO; +using System.Reflection; + +namespace Spectre.Console.Tests +{ + public static class EmbeddedResourceReader + { + public static Stream LoadResourceStream(string resourceName) + { + if (resourceName is null) + { + throw new ArgumentNullException(nameof(resourceName)); + } + + var assembly = Assembly.GetCallingAssembly(); + resourceName = resourceName.ReplaceExact("/", "."); + + return assembly.GetManifestResourceStream(resourceName); + } + + public static Stream LoadResourceStream(Assembly assembly, string resourceName) + { + if (assembly is null) + { + throw new ArgumentNullException(nameof(assembly)); + } + + if (resourceName is null) + { + throw new ArgumentNullException(nameof(resourceName)); + } + + resourceName = resourceName.ReplaceExact("/", "."); + return assembly.GetManifestResourceStream(resourceName); + } + } +} diff --git a/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs b/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs new file mode 100644 index 0000000..16b41ae --- /dev/null +++ b/src/Spectre.Console.Testing/Extensions/CommandContextExtensions.cs @@ -0,0 +1,30 @@ +using System; +using System.Linq; +using Shouldly; + +namespace Spectre.Console.Cli +{ + public static class CommandContextExtensions + { + public static void ShouldHaveRemainingArgument(this CommandContext context, string name, string[] values) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (values == null) + { + throw new ArgumentNullException(nameof(values)); + } + + context.Remaining.Parsed.Contains(name).ShouldBeTrue(); + context.Remaining.Parsed[name].Count().ShouldBe(values.Length); + + foreach (var value in values) + { + context.Remaining.Parsed[name].ShouldContain(value); + } + } + } +} diff --git a/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs b/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs new file mode 100644 index 0000000..4701acc --- /dev/null +++ b/src/Spectre.Console.Testing/Extensions/ShouldlyExtensions.cs @@ -0,0 +1,49 @@ +using System; +using System.Diagnostics; +using Shouldly; + +namespace Spectre.Console +{ + public static class ShouldlyExtensions + { + [DebuggerStepThrough] + public static T And(this T item, Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + action(item); + return item; + } + + [DebuggerStepThrough] + public static void As(this T item, Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + action(item); + } + + [DebuggerStepThrough] + public static void As(this object item, Action action) + { + if (action == null) + { + throw new ArgumentNullException(nameof(action)); + } + + action((T)item); + } + + [DebuggerStepThrough] + public static void ShouldBe(this Type item) + { + item.ShouldBe(typeof(T)); + } + } +} diff --git a/src/Spectre.Console.Testing/Extensions/StringExtensions.cs b/src/Spectre.Console.Testing/Extensions/StringExtensions.cs new file mode 100644 index 0000000..05ac9e3 --- /dev/null +++ b/src/Spectre.Console.Testing/Extensions/StringExtensions.cs @@ -0,0 +1,79 @@ +using System.Collections.Generic; +using System.Text.RegularExpressions; + +namespace Spectre.Console +{ + public static class StringExtensions + { + private static readonly Regex _lineNumberRegex = new Regex(":\\d+", RegexOptions.Singleline); + private static readonly Regex _filenameRegex = new Regex("\\sin\\s.*cs:nn", RegexOptions.Multiline); + + public static string TrimLines(this string value) + { + if (value is null) + { + return string.Empty; + } + + var result = new List(); + var lines = value.Split(new[] { '\n' }); + + foreach (var line in lines) + { + var current = line.TrimEnd(); + if (string.IsNullOrWhiteSpace(current)) + { + result.Add(string.Empty); + } + else + { + result.Add(current); + } + } + + return string.Join("\n", result); + } + + public static string NormalizeLineEndings(this string value) + { + if (value != null) + { + value = value.Replace("\r\n", "\n"); + return value.Replace("\r", string.Empty); + } + + return string.Empty; + } + + public static string NormalizeStackTrace(this string text) + { + text = _lineNumberRegex.Replace(text, match => + { + return ":nn"; + }); + + return _filenameRegex.Replace(text, match => + { + var value = match.Value; + var index = value.LastIndexOfAny(new[] { '\\', '/' }); + var filename = value.Substring(index + 1, value.Length - index - 1); + + return $" in /xyz/{filename}"; + }); + } + + internal static string ReplaceExact(this string text, string oldValue, string newValue) + { + if (string.IsNullOrWhiteSpace(newValue)) + { + return text; + } + +#if NET5_0 + return text.Replace(oldValue, newValue, StringComparison.Ordinal); +#else + return text.Replace(oldValue, newValue); +#endif + } + } +} diff --git a/src/Spectre.Console.Tests/Extensions/StyleExtensions.cs b/src/Spectre.Console.Testing/Extensions/StyleExtensions.cs similarity index 87% rename from src/Spectre.Console.Tests/Extensions/StyleExtensions.cs rename to src/Spectre.Console.Testing/Extensions/StyleExtensions.cs index d64c9ac..cf8d466 100644 --- a/src/Spectre.Console.Tests/Extensions/StyleExtensions.cs +++ b/src/Spectre.Console.Testing/Extensions/StyleExtensions.cs @@ -1,6 +1,6 @@ namespace Spectre.Console.Tests { - internal static class StyleExtensions + public static class StyleExtensions { public static Style SetColor(this Style style, Color color, bool foreground) { diff --git a/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs b/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs new file mode 100644 index 0000000..8bbee95 --- /dev/null +++ b/src/Spectre.Console.Testing/Extensions/XmlElementExtensions.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Xml; + +namespace Spectre.Console +{ + public static class XmlElementExtensions + { + public static void SetNullableAttribute(this XmlElement element, string name, string value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetAttribute(name, value ?? "NULL"); + } + + public static void SetNullableAttribute(this XmlElement element, string name, IEnumerable values) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + if (values?.Any() != true) + { + element.SetAttribute(name, "NULL"); + } + + element.SetAttribute(name, string.Join(",", values)); + } + + public static void SetBooleanAttribute(this XmlElement element, string name, bool value) + { + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + element.SetAttribute(name, value ? "true" : "false"); + } + + public static void SetEnumAttribute(this XmlElement element, string name, Enum value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (element == null) + { + throw new ArgumentNullException(nameof(element)); + } + + var field = value.GetType().GetField(value.ToString()); + var attribute = field.GetCustomAttribute(false); + if (attribute == null) + { + throw new InvalidOperationException("Enum is missing description."); + } + + element.SetAttribute(name, attribute.Description); + } + } +} diff --git a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs similarity index 83% rename from src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs rename to src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs index 48af9d4..f92ea46 100644 --- a/src/Spectre.Console.Tests/Tools/TestableAnsiConsole.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsole.cs @@ -4,9 +4,9 @@ using System.IO; using System.Text; using Spectre.Console.Rendering; -namespace Spectre.Console.Tests +namespace Spectre.Console.Testing { - public sealed class TestableAnsiConsole : IDisposable, IAnsiConsole + public sealed class FakeAnsiConsole : IDisposable, IAnsiConsole { private readonly StringWriter _writer; private readonly IAnsiConsole _console; @@ -18,12 +18,12 @@ namespace Spectre.Console.Tests public int Width { get; } public int Height => _console.Height; public IAnsiConsoleCursor Cursor => _console.Cursor; - public TestableConsoleInput Input { get; } + public FakeConsoleInput Input { get; } public RenderPipeline Pipeline => _console.Pipeline; IAnsiConsoleInput IAnsiConsole.Input => Input; - public TestableAnsiConsole( + public FakeAnsiConsole( ColorSystem system, AnsiSupport ansi = AnsiSupport.Yes, InteractionSupport interaction = InteractionSupport.Yes, int width = 80) @@ -35,11 +35,11 @@ namespace Spectre.Console.Tests ColorSystem = (ColorSystemSupport)system, Interactive = interaction, Out = _writer, - LinkIdentityGenerator = new TestLinkIdentityGenerator(), + LinkIdentityGenerator = new FakeLinkIdentityGenerator(1024), }); Width = width; - Input = new TestableConsoleInput(); + Input = new FakeConsoleInput(); } public void Dispose() diff --git a/src/Spectre.Console.Tests/Tools/DummyCursor.cs b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs similarity index 69% rename from src/Spectre.Console.Tests/Tools/DummyCursor.cs rename to src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs index fe1f75a..a67f506 100644 --- a/src/Spectre.Console.Tests/Tools/DummyCursor.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeAnsiConsoleCursor.cs @@ -1,6 +1,6 @@ -namespace Spectre.Console.Tests +namespace Spectre.Console.Testing { - public sealed class DummyCursor : IAnsiConsoleCursor + public sealed class FakeAnsiConsoleCursor : IAnsiConsoleCursor { public void Move(CursorDirection direction, int steps) { diff --git a/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs b/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs new file mode 100644 index 0000000..254a8a8 --- /dev/null +++ b/src/Spectre.Console.Testing/Fakes/FakeCommandInterceptor.cs @@ -0,0 +1,20 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Testing +{ + public sealed class FakeCommandInterceptor : ICommandInterceptor + { + private readonly Action _action; + + public FakeCommandInterceptor(Action action) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + } + + public void Intercept(CommandContext context, CommandSettings settings) + { + _action(context, settings); + } + } +} diff --git a/src/Spectre.Console.Tests/Tools/PlainConsole.cs b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs similarity index 88% rename from src/Spectre.Console.Tests/Tools/PlainConsole.cs rename to src/Spectre.Console.Testing/Fakes/FakeConsole.cs index 105d056..048b758 100644 --- a/src/Spectre.Console.Tests/Tools/PlainConsole.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeConsole.cs @@ -5,14 +5,14 @@ using System.Linq; using System.Text; using Spectre.Console.Rendering; -namespace Spectre.Console.Tests +namespace Spectre.Console.Testing { - public sealed class PlainConsole : IAnsiConsole, IDisposable + public sealed class FakeConsole : IAnsiConsole, IDisposable { public Capabilities Capabilities { get; } public Encoding Encoding { get; } - public IAnsiConsoleCursor Cursor => new DummyCursor(); - public TestableConsoleInput Input { get; } + public IAnsiConsoleCursor Cursor => new FakeAnsiConsoleCursor(); + public FakeConsoleInput Input { get; } public int Width { get; } public int Height { get; } @@ -29,7 +29,7 @@ namespace Spectre.Console.Tests public string Output => Writer.ToString(); public IReadOnlyList Lines => Output.TrimEnd('\n').Split(new char[] { '\n' }); - public PlainConsole( + public FakeConsole( int width = 80, int height = 9000, Encoding encoding = null, bool supportsAnsi = true, ColorSystem colorSystem = ColorSystem.Standard, bool legacyConsole = false, bool interactive = true) @@ -39,7 +39,7 @@ namespace Spectre.Console.Tests Width = width; Height = height; Writer = new StringWriter(); - Input = new TestableConsoleInput(); + Input = new FakeConsoleInput(); Pipeline = new RenderPipeline(); } diff --git a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs b/src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs similarity index 90% rename from src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs rename to src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs index 0764be4..a3223fe 100644 --- a/src/Spectre.Console.Tests/Tools/TestableConsoleInput.cs +++ b/src/Spectre.Console.Testing/Fakes/FakeConsoleInput.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; -namespace Spectre.Console.Tests +namespace Spectre.Console.Testing { - public sealed class TestableConsoleInput : IAnsiConsoleInput + public sealed class FakeConsoleInput : IAnsiConsoleInput { private readonly Queue _input; - public TestableConsoleInput() + public FakeConsoleInput() { _input = new Queue(); } diff --git a/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs b/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs new file mode 100644 index 0000000..41f9443 --- /dev/null +++ b/src/Spectre.Console.Testing/Fakes/FakeLinkIdentityGenerator.cs @@ -0,0 +1,17 @@ +namespace Spectre.Console.Testing +{ + public sealed class FakeLinkIdentityGenerator : ILinkIdentityGenerator + { + private readonly int _linkId; + + public FakeLinkIdentityGenerator(int linkId) + { + _linkId = linkId; + } + + public int GenerateId(string link, string text) + { + return _linkId; + } + } +} diff --git a/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs b/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs new file mode 100644 index 0000000..440856b --- /dev/null +++ b/src/Spectre.Console.Testing/Fakes/FakeTypeRegistrar.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Cli; + +namespace Spectre.Console.Testing +{ + public sealed class FakeTypeRegistrar : ITypeRegistrar + { + private readonly ITypeResolver _resolver; + public Dictionary> Registrations { get; } + public Dictionary> Instances { get; } + + public FakeTypeRegistrar(ITypeResolver resolver = null) + { + _resolver = resolver; + Registrations = new Dictionary>(); + Instances = new Dictionary>(); + } + + public void Register(Type service, Type implementation) + { + if (!Registrations.ContainsKey(service)) + { + Registrations.Add(service, new List { implementation }); + } + else + { + Registrations[service].Add(implementation); + } + } + + public void RegisterInstance(Type service, object implementation) + { + if (!Instances.ContainsKey(service)) + { + Instances.Add(service, new List { implementation }); + } + } + + public ITypeResolver Build() + { + return _resolver; + } + } +} diff --git a/src/Spectre.Console.Testing/Fakes/FakeTypeResolver.cs b/src/Spectre.Console.Testing/Fakes/FakeTypeResolver.cs new file mode 100644 index 0000000..a676ed3 --- /dev/null +++ b/src/Spectre.Console.Testing/Fakes/FakeTypeResolver.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Cli; + +namespace Spectre.Console.Testing +{ + public sealed class FakeTypeResolver : ITypeResolver + { + private readonly IDictionary _lookup; + + public FakeTypeResolver() + { + _lookup = new Dictionary(); + } + + public void Register(T instance) + { + _lookup[typeof(T)] = instance; + } + + public object Resolve(Type type) + { + if (_lookup.TryGetValue(type, out var value)) + { + return value; + } + + return Activator.CreateInstance(type); + } + } +} diff --git a/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj new file mode 100644 index 0000000..578662f --- /dev/null +++ b/src/Spectre.Console.Testing/Spectre.Console.Testing.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.0 + + + + + + + + + + + + + diff --git a/src/Spectre.Console.Testing/Widgets/DummySpinner1.cs b/src/Spectre.Console.Testing/Widgets/DummySpinner1.cs new file mode 100644 index 0000000..d73bd42 --- /dev/null +++ b/src/Spectre.Console.Testing/Widgets/DummySpinner1.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Testing +{ + public sealed class DummySpinner1 : Spinner + { + public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); + public override bool IsUnicode => true; + public override IReadOnlyList Frames => new List { "*", }; + } +} diff --git a/src/Spectre.Console.Testing/Widgets/DummySpinner2.cs b/src/Spectre.Console.Testing/Widgets/DummySpinner2.cs new file mode 100644 index 0000000..8c248ae --- /dev/null +++ b/src/Spectre.Console.Testing/Widgets/DummySpinner2.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Testing +{ + public sealed class DummySpinner2 : Spinner + { + public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); + public override bool IsUnicode => true; + public override IReadOnlyList Frames => new List { "-", }; + } +} diff --git a/src/Spectre.Console.Tests/Constants.cs b/src/Spectre.Console.Tests/Constants.cs new file mode 100644 index 0000000..8c287e7 --- /dev/null +++ b/src/Spectre.Console.Tests/Constants.cs @@ -0,0 +1,19 @@ +namespace Spectre.Console.Tests +{ + public static class Constants + { + public static string[] VersionCommand { get; } = + new[] + { + Spectre.Console.Cli.Internal.Constants.Commands.Branch, + Spectre.Console.Cli.Internal.Constants.Commands.Version, + }; + + public static string[] XmlDocCommand { get; } = + new[] + { + Spectre.Console.Cli.Internal.Constants.Commands.Branch, + Spectre.Console.Cli.Internal.Constants.Commands.XmlDoc, + }; + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/AnimalCommand.cs b/src/Spectre.Console.Tests/Data/Commands/AnimalCommand.cs new file mode 100644 index 0000000..48864bc --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/AnimalCommand.cs @@ -0,0 +1,49 @@ +using System; +using System.Linq; +using Spectre.Console.Cli; +using SystemConsole = System.Console; + +namespace Spectre.Console.Tests.Data +{ + public abstract class AnimalCommand : Command + where TSettings : CommandSettings + { + protected void DumpSettings(CommandContext context, TSettings settings) + { + if (context == null) + { + throw new ArgumentNullException(nameof(context)); + } + + if (settings == null) + { + throw new ArgumentNullException(nameof(settings)); + } + + var properties = settings.GetType().GetProperties(); + foreach (var group in properties.GroupBy(x => x.DeclaringType).Reverse()) + { + SystemConsole.WriteLine(); + SystemConsole.ForegroundColor = ConsoleColor.Yellow; + SystemConsole.WriteLine(group.Key.FullName); + SystemConsole.ResetColor(); + + foreach (var property in group) + { + SystemConsole.WriteLine($" {property.Name} = {property.GetValue(settings)}"); + } + } + + if (context.Remaining.Raw.Count > 0) + { + SystemConsole.WriteLine(); + SystemConsole.ForegroundColor = ConsoleColor.Yellow; + SystemConsole.WriteLine("Remaining:"); + SystemConsole.ResetColor(); + SystemConsole.WriteLine(string.Join(", ", context.Remaining)); + } + + SystemConsole.WriteLine(); + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/CatCommand.cs b/src/Spectre.Console.Tests/Data/Commands/CatCommand.cs new file mode 100644 index 0000000..df0af93 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/CatCommand.cs @@ -0,0 +1,13 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class CatCommand : AnimalCommand + { + public override int Execute(CommandContext context, CatSettings settings) + { + DumpSettings(context, settings); + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/DogCommand.cs b/src/Spectre.Console.Tests/Data/Commands/DogCommand.cs new file mode 100644 index 0000000..fb37c1d --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/DogCommand.cs @@ -0,0 +1,36 @@ +using System.ComponentModel; +using System.Linq; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [Description("The dog command.")] + public class DogCommand : AnimalCommand + { + public override ValidationResult Validate(CommandContext context, DogSettings settings) + { + if (context is null) + { + throw new System.ArgumentNullException(nameof(context)); + } + + if (settings is null) + { + throw new System.ArgumentNullException(nameof(settings)); + } + + if (settings.Age > 100 && !context.Remaining.Raw.Contains("zombie")) + { + return ValidationResult.Error("Dog is too old..."); + } + + return base.Validate(context, settings); + } + + public override int Execute(CommandContext context, DogSettings settings) + { + DumpSettings(context, settings); + return 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Commands/EmptyCommand.cs b/src/Spectre.Console.Tests/Data/Commands/EmptyCommand.cs new file mode 100644 index 0000000..745b5b1 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/EmptyCommand.cs @@ -0,0 +1,12 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class EmptyCommand : Command + { + public override int Execute(CommandContext context, EmptyCommandSettings settings) + { + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/GenericCommand.cs b/src/Spectre.Console.Tests/Data/Commands/GenericCommand.cs new file mode 100644 index 0000000..b85c097 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/GenericCommand.cs @@ -0,0 +1,13 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class GenericCommand : Command + where TSettings : CommandSettings + { + public override int Execute(CommandContext context, TSettings settings) + { + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/GiraffeCommand.cs b/src/Spectre.Console.Tests/Data/Commands/GiraffeCommand.cs new file mode 100644 index 0000000..b5e4131 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/GiraffeCommand.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [Description("The giraffe command.")] + public sealed class GiraffeCommand : Command + { + public override int Execute(CommandContext context, GiraffeSettings settings) + { + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/HorseCommand.cs b/src/Spectre.Console.Tests/Data/Commands/HorseCommand.cs new file mode 100644 index 0000000..5400a01 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/HorseCommand.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [Description("The horse command.")] + public class HorseCommand : AnimalCommand + { + public override int Execute(CommandContext context, MammalSettings settings) + { + DumpSettings(context, settings); + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/InterceptingCommand.cs b/src/Spectre.Console.Tests/Data/Commands/InterceptingCommand.cs new file mode 100644 index 0000000..6e71ca9 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/InterceptingCommand.cs @@ -0,0 +1,22 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class InterceptingCommand : Command + where TSettings : CommandSettings + { + private readonly Action _action; + + public InterceptingCommand(Action action) + { + _action = action ?? throw new ArgumentNullException(nameof(action)); + } + + public override int Execute(CommandContext context, TSettings settings) + { + _action(context, settings); + return 0; + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Commands/InvalidCommand.cs b/src/Spectre.Console.Tests/Data/Commands/InvalidCommand.cs new file mode 100644 index 0000000..361172a --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/InvalidCommand.cs @@ -0,0 +1,12 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class InvalidCommand : Command + { + public override int Execute(CommandContext context, InvalidSettings settings) + { + return 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Commands/LionCommand.cs b/src/Spectre.Console.Tests/Data/Commands/LionCommand.cs new file mode 100644 index 0000000..f63bcf0 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/LionCommand.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [Description("The lion command.")] + public class LionCommand : AnimalCommand + { + public override int Execute(CommandContext context, LionSettings settings) + { + return 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Commands/OptionVectorCommand.cs b/src/Spectre.Console.Tests/Data/Commands/OptionVectorCommand.cs new file mode 100644 index 0000000..a10bda2 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/OptionVectorCommand.cs @@ -0,0 +1,12 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class OptionVectorCommand : Command + { + public override int Execute(CommandContext context, OptionVectorSettings settings) + { + return 0; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Commands/ThrowingCommand.cs b/src/Spectre.Console.Tests/Data/Commands/ThrowingCommand.cs new file mode 100644 index 0000000..647592c --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Commands/ThrowingCommand.cs @@ -0,0 +1,17 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class ThrowingCommand : Command + { + public override int Execute(CommandContext context, ThrowingCommandSettings settings) + { + throw new InvalidOperationException("W00t?"); + } + } + + public sealed class ThrowingCommandSettings : CommandSettings + { + } +} diff --git a/src/Spectre.Console.Tests/Data/Converters/CatAgilityConverter.cs b/src/Spectre.Console.Tests/Data/Converters/CatAgilityConverter.cs new file mode 100644 index 0000000..c6535ef --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Converters/CatAgilityConverter.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Spectre.Console.Tests.Data +{ + public sealed class CatAgilityConverter : TypeConverter + { + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string stringValue) + { + return stringValue.Length; + } + + return base.ConvertFrom(context, culture, value); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Converters/StringToIntegerConverter.cs b/src/Spectre.Console.Tests/Data/Converters/StringToIntegerConverter.cs new file mode 100644 index 0000000..831d799 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Converters/StringToIntegerConverter.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using System.Globalization; + +namespace Spectre.Console.Tests.Data +{ + public sealed class StringToIntegerConverter : TypeConverter + { + public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) + { + if (value is string stringValue) + { + return int.Parse(stringValue, CultureInfo.InvariantCulture); + } + + return base.ConvertFrom(context, culture, value); + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/AnimalSettings.cs b/src/Spectre.Console.Tests/Data/Settings/AnimalSettings.cs new file mode 100644 index 0000000..8e81d7e --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/AnimalSettings.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public abstract class AnimalSettings : CommandSettings + { + [CommandOption("-a|--alive|--not-dead")] + [Description("Indicates whether or not the animal is alive.")] + public bool IsAlive { get; set; } + + [CommandArgument(1, "[LEGS]")] + [Description("The number of legs.")] + [EvenNumberValidator("Animals must have an even number of legs.")] + [PositiveNumberValidator("Number of legs must be greater than 0.")] + public int Legs { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/ArgumentVectorSettings.cs b/src/Spectre.Console.Tests/Data/Settings/ArgumentVectorSettings.cs new file mode 100644 index 0000000..eefa000 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/ArgumentVectorSettings.cs @@ -0,0 +1,12 @@ +using System.Diagnostics.CodeAnalysis; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class ArgumentVectorSettings : CommandSettings + { + [CommandArgument(0, "")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Foo { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/BarCommandSettings.cs b/src/Spectre.Console.Tests/Data/Settings/BarCommandSettings.cs new file mode 100644 index 0000000..8c80bd0 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/BarCommandSettings.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class BarCommandSettings : FooCommandSettings + { + [CommandArgument(0, "")] + [Description("The corgi value.")] + public string Corgi { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/CatSettings.cs b/src/Spectre.Console.Tests/Data/Settings/CatSettings.cs new file mode 100644 index 0000000..2677c73 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/CatSettings.cs @@ -0,0 +1,15 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class CatSettings : MammalSettings + { + [CommandOption("--agility ")] + [TypeConverter(typeof(CatAgilityConverter))] + [DefaultValue(10)] + [Description("The agility between 0 and 100.")] + [PositiveNumberValidator("Agility cannot be negative.")] + public int Agility { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/DogSettings.cs b/src/Spectre.Console.Tests/Data/Settings/DogSettings.cs new file mode 100644 index 0000000..aa79c6f --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/DogSettings.cs @@ -0,0 +1,23 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class DogSettings : MammalSettings + { + [CommandArgument(0, "")] + public int Age { get; set; } + + [CommandOption("-g|--good-boy")] + public bool GoodBoy { get; set; } + + public override ValidationResult Validate() + { + if (Name == "Tiger") + { + return ValidationResult.Error("Tiger is not a dog name!"); + } + + return ValidationResult.Success(); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/EmptySettings.cs b/src/Spectre.Console.Tests/Data/Settings/EmptySettings.cs new file mode 100644 index 0000000..200d60f --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/EmptySettings.cs @@ -0,0 +1,8 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class EmptySettings : CommandSettings + { + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/FooSettings.cs b/src/Spectre.Console.Tests/Data/Settings/FooSettings.cs new file mode 100644 index 0000000..3fbb07c --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/FooSettings.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class FooCommandSettings : CommandSettings + { + [CommandArgument(0, "[QUX]")] + [Description("The qux value.")] + public string Qux { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/GiraffeSettings.cs b/src/Spectre.Console.Tests/Data/Settings/GiraffeSettings.cs new file mode 100644 index 0000000..a308b88 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/GiraffeSettings.cs @@ -0,0 +1,12 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class GiraffeSettings : MammalSettings + { + [CommandArgument(0, "")] + [Description("The option description.")] + public int Length { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/InvalidSettings.cs b/src/Spectre.Console.Tests/Data/Settings/InvalidSettings.cs new file mode 100644 index 0000000..3856adc --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/InvalidSettings.cs @@ -0,0 +1,10 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class InvalidSettings : CommandSettings + { + [CommandOption("-f|--foo [BAR]")] + public string Value { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/LionSettings.cs b/src/Spectre.Console.Tests/Data/Settings/LionSettings.cs new file mode 100644 index 0000000..301bd5c --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/LionSettings.cs @@ -0,0 +1,16 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class LionSettings : CatSettings + { + [CommandArgument(0, "")] + [Description("The number of teeth the lion has.")] + public int Teeth { get; set; } + + [CommandOption("-c ")] + [Description("The number of children the lion has.")] + public int Children { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/MammalSettings.cs b/src/Spectre.Console.Tests/Data/Settings/MammalSettings.cs new file mode 100644 index 0000000..79a2018 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/MammalSettings.cs @@ -0,0 +1,10 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class MammalSettings : AnimalSettings + { + [CommandOption("-n|-p|--name|--pet-name ")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Data/Settings/MultipleArgumentVectorSettings.cs b/src/Spectre.Console.Tests/Data/Settings/MultipleArgumentVectorSettings.cs new file mode 100644 index 0000000..992a92e --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/MultipleArgumentVectorSettings.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class MultipleArgumentVectorSettings : CommandSettings + { + [CommandArgument(0, "")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Foo { get; set; } + + [CommandArgument(0, "")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Bar { get; set; } + } + + public class MultipleArgumentVectorSpecifiedFirstSettings : CommandSettings + { + [CommandArgument(0, "")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Foo { get; set; } + + [CommandArgument(1, "")] + public string Bar { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/OptionVectorSettings.cs b/src/Spectre.Console.Tests/Data/Settings/OptionVectorSettings.cs new file mode 100644 index 0000000..e8296e4 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/OptionVectorSettings.cs @@ -0,0 +1,16 @@ +using System.Diagnostics.CodeAnalysis; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public class OptionVectorSettings : CommandSettings + { + [CommandOption("--foo")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public string[] Foo { get; set; } + + [CommandOption("--bar")] + [SuppressMessage("Performance", "CA1819:Properties should not return arrays")] + public int[] Bar { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs b/src/Spectre.Console.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs new file mode 100644 index 0000000..b5b63e2 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/OptionalArgumentWithDefaultValueSettings.cs @@ -0,0 +1,27 @@ +using System.ComponentModel; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class OptionalArgumentWithDefaultValueSettings : CommandSettings + { + [CommandArgument(0, "[GREETING]")] + [DefaultValue("Hello World")] + public string Greeting { get; set; } + } + + public sealed class OptionalArgumentWithDefaultValueAndTypeConverterSettings : CommandSettings + { + [CommandArgument(0, "[GREETING]")] + [DefaultValue("5")] + [TypeConverter(typeof(StringToIntegerConverter))] + public int Greeting { get; set; } + } + + public sealed class RequiredArgumentWithDefaultValueSettings : CommandSettings + { + [CommandArgument(0, "")] + [DefaultValue("Hello World")] + public string Greeting { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Settings/StringOptionSettings.cs b/src/Spectre.Console.Tests/Data/Settings/StringOptionSettings.cs new file mode 100644 index 0000000..467cb53 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Settings/StringOptionSettings.cs @@ -0,0 +1,10 @@ +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + public sealed class StringOptionSettings : CommandSettings + { + [CommandOption("-f|--foo")] + public string Foo { get; set; } + } +} diff --git a/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs b/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs new file mode 100644 index 0000000..c04aaad --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Validators/EvenNumberValidatorAttribute.cs @@ -0,0 +1,29 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public sealed class EvenNumberValidatorAttribute : ParameterValidationAttribute + { + public EvenNumberValidatorAttribute(string errorMessage) + : base(errorMessage) + { + } + + public override ValidationResult Validate(ICommandParameterInfo info, object value) + { + if (value is int integer) + { + if (integer % 2 == 0) + { + return ValidationResult.Success(); + } + + return ValidationResult.Error($"Number is not even ({info?.PropertyName})."); + } + + throw new InvalidOperationException($"Parameter is not a number ({info?.PropertyName})."); + } + } +} diff --git a/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs b/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs new file mode 100644 index 0000000..19d5c07 --- /dev/null +++ b/src/Spectre.Console.Tests/Data/Validators/PositiveNumberValidatorAttribute.cs @@ -0,0 +1,29 @@ +using System; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Data +{ + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public sealed class PositiveNumberValidatorAttribute : ParameterValidationAttribute + { + public PositiveNumberValidatorAttribute(string errorMessage) + : base(errorMessage) + { + } + + public override ValidationResult Validate(ICommandParameterInfo info, object value) + { + if (value is int integer) + { + if (integer > 0) + { + return ValidationResult.Success(); + } + + return ValidationResult.Error($"Number is not greater than 0 ({info?.PropertyName})."); + } + + throw new InvalidOperationException($"Parameter is not a number ({info?.PropertyName})."); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console.Tests/EmbeddedResourceDataAttribute.cs b/src/Spectre.Console.Tests/EmbeddedResourceDataAttribute.cs deleted file mode 100644 index 4951b87..0000000 --- a/src/Spectre.Console.Tests/EmbeddedResourceDataAttribute.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Reflection; -using Xunit.Sdk; - -namespace Spectre.Console.Tests -{ - public sealed class EmbeddedResourceDataAttribute : DataAttribute - { - private readonly string _args; - - public EmbeddedResourceDataAttribute(string args) - { - _args = args ?? throw new ArgumentNullException(nameof(args)); - } - - public override IEnumerable GetData(MethodInfo testMethod) - { - var result = new object[1]; - result[0] = ReadManifestData(_args); - return new[] { result }; - } - - public static string ReadManifestData(string resourceName) - { - if (resourceName is null) - { - throw new ArgumentNullException(nameof(resourceName)); - } - - using (var stream = ResourceReader.LoadResourceStream(resourceName)) - { - if (stream == null) - { - throw new InvalidOperationException("Could not load manifest resource stream."); - } - - using (var reader = new StreamReader(stream)) - { - return reader.ReadToEnd().NormalizeLineEndings(); - } - } - } - } -} diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Only_Output_Command_Examples_Defined_On_Command.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Only_Output_Command_Examples_Defined_On_Command.verified.txt new file mode 100644 index 0000000..ccadb2c --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Only_Output_Command_Examples_Defined_On_Command.verified.txt @@ -0,0 +1,16 @@ +USAGE: + myapp animal [LEGS] [OPTIONS] + +EXAMPLES: + myapp animal --help + +ARGUMENTS: + [LEGS] The number of legs + +OPTIONS: + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Command_Correctly.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Command_Correctly.verified.txt new file mode 100644 index 0000000..1408721 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Command_Correctly.verified.txt @@ -0,0 +1,14 @@ +USAGE: + myapp cat [LEGS] [OPTIONS] + +ARGUMENTS: + [LEGS] The number of legs + +OPTIONS: + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + -n, --name + --agility The agility between 0 and 100 + +COMMANDS: + lion The lion command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Default_Command_Correctly.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Default_Command_Correctly.verified.txt new file mode 100644 index 0000000..dae4ce6 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Default_Command_Correctly.verified.txt @@ -0,0 +1,13 @@ +USAGE: + myapp [LEGS] [OPTIONS] + +ARGUMENTS: + [LEGS] The number of legs + The number of teeth the lion has + +OPTIONS: + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + -n, --name + --agility The agility between 0 and 100 + -c The number of children the lion has \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Leaf_Correctly.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Leaf_Correctly.verified.txt new file mode 100644 index 0000000..bceaf85 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Leaf_Correctly.verified.txt @@ -0,0 +1,9 @@ +USAGE: + myapp cat [LEGS] lion [OPTIONS] + +ARGUMENTS: + The number of teeth the lion has + +OPTIONS: + -h, --help Prints help information + -c The number of children the lion has \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Correctly.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Correctly.verified.txt new file mode 100644 index 0000000..366b6b3 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Correctly.verified.txt @@ -0,0 +1,11 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command + giraffe The giraffe command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples.verified.txt new file mode 100644 index 0000000..6f5066a --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples.verified.txt @@ -0,0 +1,14 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp dog --name Rufus --age 12 --good-boy + myapp horse --name Brutus + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found.verified.txt new file mode 100644 index 0000000..7d3b86e --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found.verified.txt @@ -0,0 +1,13 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp animal dog --name Rufus --age 12 --good-boy + myapp animal horse --name Brutus + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + animal The animal command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Root.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Root.verified.txt new file mode 100644 index 0000000..6f5066a --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_Defined_On_Root.verified.txt @@ -0,0 +1,14 @@ +USAGE: + myapp [OPTIONS] + +EXAMPLES: + myapp dog --name Rufus --age 12 --good-boy + myapp horse --name Brutus + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_If_Default_Command_Is_Specified.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_If_Default_Command_Is_Specified.verified.txt new file mode 100644 index 0000000..6725d51 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Output_Root_Examples_If_Default_Command_Is_Specified.verified.txt @@ -0,0 +1,16 @@ +USAGE: + myapp [LEGS] [OPTIONS] + +EXAMPLES: + myapp 12 -c 3 + +ARGUMENTS: + [LEGS] The number of legs + The number of teeth the lion has + +OPTIONS: + -h, --help Prints help information + -a, --alive Indicates whether or not the animal is alive + -n, --name + --agility The agility between 0 and 100 + -c The number of children the lion has \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Skip_Hidden_Commands.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Skip_Hidden_Commands.verified.txt new file mode 100644 index 0000000..6a792da --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Help.Help.Should_Skip_Hidden_Commands.verified.txt @@ -0,0 +1,10 @@ +USAGE: + myapp [OPTIONS] + +OPTIONS: + -h, --help Prints help information + -v, --version Prints version information + +COMMANDS: + dog The dog command + horse The horse command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Long_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Long_Option.verified.txt new file mode 100644 index 0000000..7def92f --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Long_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Flags cannot be assigned a value. + + dog --alive foo + ^^^^^^^ Can't assign value \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Short_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Short_Option.verified.txt new file mode 100644 index 0000000..b3381d2 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.CannotAssignValueToFlag.Should_Return_Correct_Text_For_Short_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Flags cannot be assigned a value. + + dog -a foo + ^^ Can't assign value \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.InvalidShortOptionName.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.InvalidShortOptionName.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..b6efb4d --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.InvalidShortOptionName.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Short option does not have a valid name. + + dog -f0o + ^ Not a valid name for a short option \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameContainSymbol.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameContainSymbol.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..aced667 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameContainSymbol.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Invalid long option name. + + dog --f€oo + ^ Invalid character \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsMissing.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsMissing.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..62f3c00 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsMissing.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Invalid long option name. + + dog -- + ^^ Did you forget the option name? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsOneCharacter.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsOneCharacter.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..22a5672 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameIsOneCharacter.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Invalid long option name. + + dog --f + ^^^ Did you mean -f? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameStartWithDigit.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameStartWithDigit.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..295fe90 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.LongOptionNameStartWithDigit.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Invalid long option name. + + dog --1foo + ^^^^^^ Option names cannot start with a digit \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoMatchingArgument.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoMatchingArgument.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..1873f49 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoMatchingArgument.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Could not match 'baz' with an argument. + + giraffe foo bar baz + ^^^ Could not match to argument \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Long_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Long_Option.verified.txt new file mode 100644 index 0000000..346f4d4 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Long_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Option 'name' is defined but no value has been provided. + + dog --name + ^^^^^^ No value provided \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Short_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Short_Option.verified.txt new file mode 100644 index 0000000..97e0cbd --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.NoValueForOption.Should_Return_Correct_Text_For_Short_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Option 'name' is defined but no value has been provided. + + dog -n + ^^ No value provided \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator.verified.txt new file mode 100644 index 0000000..5ae49a6 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator.verified.txt @@ -0,0 +1,4 @@ +Error: Expected an option value. + + dog --foo: + ^ Did you forget the option value? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator.verified.txt new file mode 100644 index 0000000..52d5bcb --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator.verified.txt @@ -0,0 +1,4 @@ +Error: Expected an option value. + + dog --foo= + ^ Did you forget the option value? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator.verified.txt new file mode 100644 index 0000000..2dafc04 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator.verified.txt @@ -0,0 +1,4 @@ +Error: Expected an option value. + + dog -f: + ^ Did you forget the option value? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator.verified.txt new file mode 100644 index 0000000..bd61abc --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator.verified.txt @@ -0,0 +1,4 @@ +Error: Expected an option value. + + dog -f= + ^ Did you forget the option value? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Short_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Short_Option.verified.txt new file mode 100644 index 0000000..8733f9d --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.OptionWithoutName.Should_Return_Correct_Text_For_Short_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Option does not have a name. + + dog - + ^ Did you forget the option name? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Long_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Long_Option.verified.txt new file mode 100644 index 0000000..67b7cff --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Long_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Unexpected option 'foo'. + + --foo + ^^^^^ Did you forget the command? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Short_Option.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Short_Option.verified.txt new file mode 100644 index 0000000..4371967 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnexpectedOption.Should_Return_Correct_Text_For_Short_Option.verified.txt @@ -0,0 +1,4 @@ +Error: Unexpected option 'f'. + + -f + ^^ Did you forget the command? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments.verified.txt new file mode 100644 index 0000000..ac7a69d --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'other'. + + empty other + ^^^^^ No such command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_When_Command_Is_Unknown.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_When_Command_Is_Unknown.verified.txt new file mode 100644 index 0000000..8a61b33 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_When_Command_Is_Unknown.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'cat'. + + cat 14 + ^^^ No such command \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..cdabe28 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + dog bat + ^^^ Did you mean 'cat'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..225f86e --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + bat + ^^^ Did you mean 'cat'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..94f805b --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + foo qux bat + ^^^ Did you mean 'bar'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..cbea5ea --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + dog bat 14 + ^^^ Did you mean 'cat'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..d9eacbb --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + qux bat + ^^^ Did you mean 'bar'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt new file mode 100644 index 0000000..bc8a096 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownCommand.Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown command 'bat'. + + bat 14 + ^^^ Did you mean 'cat'? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled.verified.txt new file mode 100644 index 0000000..d23f5a6 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown option 'unknown'. + + dog --unknown + ^^^^^^^^^ Unknown option \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled.verified.txt new file mode 100644 index 0000000..d86e8ec --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnknownOption.Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled.verified.txt @@ -0,0 +1,4 @@ +Error: Unknown option 'u'. + + dog -u + ^^ Unknown option \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnterminatedQuote.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnterminatedQuote.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..a6d7032 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Parsing.UnterminatedQuote.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,4 @@ +Error: Encountered unterminated quoted string 'Rufus'. + + --name "Rufus + ^^^^^^ Did you forget the closing quotation mark? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_1.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_1.verified.txt new file mode 100644 index 0000000..13ed69d --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_1.verified.txt @@ -0,0 +1,37 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_2.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_2.verified.txt new file mode 100644 index 0000000..9f34630 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_2.verified.txt @@ -0,0 +1,21 @@ + + + + + + + The number of legs. + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_3.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_3.verified.txt new file mode 100644 index 0000000..f2a9c3f --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_3.verified.txt @@ -0,0 +1,32 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_4.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_4.verified.txt new file mode 100644 index 0000000..5949189 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_4.verified.txt @@ -0,0 +1,26 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_5.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_5.verified.txt new file mode 100644 index 0000000..2b44695 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Case_5.verified.txt @@ -0,0 +1,10 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Model_With_Default_Command.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Model_With_Default_Command.verified.txt new file mode 100644 index 0000000..8ece874 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandAppTests.Xml.Xml.Should_Dump_Correct_Model_For_Model_With_Default_Command.verified.txt @@ -0,0 +1,37 @@ + + + + + + + The number of legs. + + + + + + + + + + + + + + The number of legs. + + + + + + + + + \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheArgumentCannotContainOptionsMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheArgumentCannotContainOptionsMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..fbfd4d7 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheArgumentCannotContainOptionsMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Arguments can not contain options. + + --foo + ^^^^^ Not permitted \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheMultipleValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheMultipleValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..413d129 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheMultipleValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Multiple values are not supported. + + + ^^^^^ Too many values \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheValuesMustHaveNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheValuesMustHaveNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..8b2500e --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandArgumentAttributeTests.Rendering.TheValuesMustHaveNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Values without name are not allowed. + + <> + ^^ Missing value name \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInOptionNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInOptionNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..87989bd --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInOptionNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Encountered invalid character '$' in option name. + + --f$oo + ^ Invalid character \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInValueNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInValueNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..6d40834 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheInvalidCharacterInValueNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Encountered invalid character '$' in value name. + + -f|--foo + ^ Invalid character \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheLongOptionMustHaveMoreThanOneCharacterMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheLongOptionMustHaveMoreThanOneCharacterMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..1f72ae6 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheLongOptionMustHaveMoreThanOneCharacterMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Long option names must consist of more than one character. + + --f + ^ Invalid option name \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMissingLongAndShortNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMissingLongAndShortNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..0c41190 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMissingLongAndShortNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + No long or short name for option has been specified. + + + ^^^^^ Missing option. Was this meant to be an argument? \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMultipleOptionValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMultipleOptionValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..e27e657 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheMultipleOptionValuesAreNotSupportedMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Multiple option values are not supported. + + -f|--foo + ^^^^^ Too many option values \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionNamesCannotStartWithDigitMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionNamesCannotStartWithDigitMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..ff5ff30 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionNamesCannotStartWithDigitMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Option names cannot start with a digit. + + --1foo + ^^^^ Invalid option name \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionsMustHaveNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionsMustHaveNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..3e277d9 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheOptionsMustHaveNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Options without name are not allowed. + + --foo|- + ^ Missing option name \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheShortOptionMustOnlyBeOneCharacterMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheShortOptionMustOnlyBeOneCharacterMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..d7986d9 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheShortOptionMustOnlyBeOneCharacterMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Short option names can not be longer than one character. + + --foo|-bar + ^^^ Invalid option name \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnexpectedCharacterMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnexpectedCharacterMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..ed7e614 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnexpectedCharacterMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Encountered unexpected character '$'. + + $ + ^ Unexpected character \ No newline at end of file diff --git a/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnterminatedValueNameMethod.Should_Return_Correct_Text.verified.txt b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnterminatedValueNameMethod.Should_Return_Correct_Text.verified.txt new file mode 100644 index 0000000..b245f77 --- /dev/null +++ b/src/Spectre.Console.Tests/Expectations/CommandOptionAttributeTests.Rendering.TheUnterminatedValueNameMethod.Should_Return_Correct_Text.verified.txt @@ -0,0 +1,5 @@ +Error: An error occured when parsing template. + Encountered unterminated value name 'BAR'. + + --foo|-f - { - return ":nn"; - }); - - return _filenameRegex.Replace(text, match => - { - var value = match.Value; - var index = value.LastIndexOfAny(new[] { '\\', '/' }); - var filename = value.Substring(index + 1, value.Length - index - 1); - - return $" in /xyz/{filename}"; - }); - } - } -} diff --git a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj index d5afbb9..f0477fa 100644 --- a/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj +++ b/src/Spectre.Console.Tests/Spectre.Console.Tests.csproj @@ -2,9 +2,12 @@ net5.0 - false + + + + @@ -25,6 +28,7 @@ + diff --git a/src/Spectre.Console.Tests/Tools/DummySpinners.cs b/src/Spectre.Console.Tests/Tools/DummySpinners.cs deleted file mode 100644 index 63444c9..0000000 --- a/src/Spectre.Console.Tests/Tools/DummySpinners.cs +++ /dev/null @@ -1,25 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Spectre.Console.Tests -{ - public sealed class DummySpinner1 : Spinner - { - public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); - public override bool IsUnicode => true; - public override IReadOnlyList Frames => new List - { - "*", - }; - } - - public sealed class DummySpinner2 : Spinner - { - public override TimeSpan Interval => TimeSpan.FromMilliseconds(100); - public override bool IsUnicode => true; - public override IReadOnlyList Frames => new List - { - "-", - }; - } -} diff --git a/src/Spectre.Console.Tests/Tools/ResourceReader.cs b/src/Spectre.Console.Tests/Tools/ResourceReader.cs deleted file mode 100644 index 9a09c72..0000000 --- a/src/Spectre.Console.Tests/Tools/ResourceReader.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.IO; - -namespace Spectre.Console.Tests -{ - public static class ResourceReader - { - public static Stream LoadResourceStream(string resourceName) - { - if (resourceName is null) - { - throw new ArgumentNullException(nameof(resourceName)); - } - - var assembly = typeof(EmbeddedResourceDataAttribute).Assembly; - resourceName = resourceName.Replace("/", ".", StringComparison.Ordinal); - - return assembly.GetManifestResourceStream(resourceName); - } - } -} diff --git a/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs b/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs deleted file mode 100644 index 0a34220..0000000 --- a/src/Spectre.Console.Tests/Tools/TestLinkIdentityGenerator.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace Spectre.Console.Tests -{ - public sealed class TestLinkIdentityGenerator : ILinkIdentityGenerator - { - public int GenerateId(string link, string text) - { - return 1024; - } - } -} diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs index d7c32bf..8fc9970 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Colors.cs @@ -1,4 +1,5 @@ using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -13,7 +14,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Return_Correct_Code(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor); + var console = new FakeAnsiConsole(ColorSystem.TrueColor); // When console.Write("Hello", new Style().SetColor(new Color(128, 0, 128), foreground)); @@ -28,7 +29,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Return_Eight_Bit_Ansi_Code_For_Known_Colors(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor); + var console = new FakeAnsiConsole(ColorSystem.TrueColor); // When console.Write("Hello", new Style().SetColor(Color.Purple, foreground)); @@ -46,7 +47,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.EightBit); + var console = new FakeAnsiConsole(ColorSystem.EightBit); // When console.Write("Hello", new Style().SetColor(Color.Olive, foreground)); @@ -61,7 +62,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Map_TrueColor_To_Nearest_Eight_Bit_Color_If_Possible(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.EightBit); + var console = new FakeAnsiConsole(ColorSystem.EightBit); // When console.Write("Hello", new Style().SetColor(new Color(128, 128, 0), foreground)); @@ -76,7 +77,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Estimate_TrueColor_To_Nearest_Eight_Bit_Color(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.EightBit); + var console = new FakeAnsiConsole(ColorSystem.EightBit); // When console.Write("Hello", new Style().SetColor(new Color(126, 127, 0), foreground)); @@ -94,7 +95,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write("Hello", new Style().SetColor(Color.Olive, foreground)); @@ -114,7 +115,7 @@ namespace Spectre.Console.Tests.Unit string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground)); @@ -134,7 +135,7 @@ namespace Spectre.Console.Tests.Unit string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground)); @@ -152,7 +153,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Return_Correct_Code_For_Known_Color(bool foreground, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Legacy); + var console = new FakeAnsiConsole(ColorSystem.Legacy); // When console.Write("Hello", new Style().SetColor(Color.Olive, foreground)); @@ -172,7 +173,7 @@ namespace Spectre.Console.Tests.Unit string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Legacy); + var console = new FakeAnsiConsole(ColorSystem.Legacy); // When console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground)); @@ -192,7 +193,7 @@ namespace Spectre.Console.Tests.Unit string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Legacy); + var console = new FakeAnsiConsole(ColorSystem.Legacy); // When console.Write("Hello", new Style().SetColor(new Color(r, g, b), foreground)); diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs index 1ed652b..f8d3006 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Markup.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics.CodeAnalysis; using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -18,7 +19,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Output_Expected_Ansi_For_Markup(string markup, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When console.Markup(markup); @@ -32,7 +33,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Be_Able_To_Escape_Tags(string markup, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When console.Markup(markup); @@ -49,7 +50,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Throw_If_Encounters_Malformed_Tag(string markup, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When var result = Record.Exception(() => console.Markup(markup)); @@ -63,7 +64,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Throw_If_Tags_Are_Unbalanced() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When var result = Record.Exception(() => console.Markup("[yellow][blue]Hello[/]")); @@ -77,7 +78,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Throw_If_Encounters_Closing_Tag() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When var result = Record.Exception(() => console.Markup("Hello[/]World")); diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs index 3b41a40..8424d03 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.Style.cs @@ -1,4 +1,5 @@ using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -18,7 +19,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Write_Decorated_Text_Correctly(Decoration decoration, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor); + var console = new FakeAnsiConsole(ColorSystem.TrueColor); // When console.Write("Hello World", new Style().Decoration(decoration)); @@ -33,7 +34,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Write_Text_With_Multiple_Decorations_Correctly(Decoration decoration, string expected) { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor); + var console = new FakeAnsiConsole(ColorSystem.TrueColor); // When console.Write("Hello World", new Style().Decoration(decoration)); diff --git a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs index b63597f..9d2ab16 100644 --- a/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs +++ b/src/Spectre.Console.Tests/Unit/AnsiConsoleTests.cs @@ -1,5 +1,6 @@ using System; using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -10,7 +11,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Combine_Decoration_And_Colors() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write( @@ -28,7 +29,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Not_Include_Foreground_If_Set_To_Default_Color() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write( @@ -46,7 +47,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Not_Include_Background_If_Set_To_Default_Color() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write( @@ -64,7 +65,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Not_Include_Decoration_If_Set_To_None() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard); + var console = new FakeAnsiConsole(ColorSystem.Standard); // When console.Write( @@ -84,7 +85,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Reset_Colors_Correctly_After_Line_Break() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When console.WriteLine("Hello", new Style().Background(ConsoleColor.Red)); @@ -99,7 +100,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Reset_Colors_Correctly_After_Line_Break_In_Text() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When console.WriteLine("Hello\nWorld", new Style().Background(ConsoleColor.Red)); diff --git a/src/Spectre.Console.Tests/Unit/BarChartTests.cs b/src/Spectre.Console.Tests/Unit/BarChartTests.cs index 558cf52..50b46c9 100644 --- a/src/Spectre.Console.Tests/Unit/BarChartTests.cs +++ b/src/Spectre.Console.Tests/Unit/BarChartTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -11,7 +12,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new BarChart() diff --git a/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs b/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs index ed48994..ebd86a6 100644 --- a/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs +++ b/src/Spectre.Console.Tests/Unit/BoxBorderTests.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Shouldly; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -29,7 +30,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().NoBorder(); // When @@ -60,7 +61,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().AsciiBorder(); // When @@ -91,7 +92,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().DoubleBorder(); // When @@ -122,7 +123,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().HeavyBorder(); // When @@ -150,7 +151,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().RoundedBorder(); // When @@ -178,7 +179,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var panel = Fixture.GetPanel().SquareBorder(); // When diff --git a/src/Spectre.Console.Tests/Unit/CalendarTests.cs b/src/Spectre.Console.Tests/Unit/CalendarTests.cs index 0c8d6a6..9dd7dac 100644 --- a/src/Spectre.Console.Tests/Unit/CalendarTests.cs +++ b/src/Spectre.Console.Tests/Unit/CalendarTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -12,7 +13,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Calendar_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var calendar = new Calendar(2020, 10) .AddCalendarEvent(new DateTime(2020, 9, 1)) .AddCalendarEvent(new DateTime(2020, 10, 3)) @@ -29,7 +30,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Center_Calendar_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var calendar = new Calendar(2020, 10) .Centered() .AddCalendarEvent(new DateTime(2020, 9, 1)) @@ -47,7 +48,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Left_Align_Calendar_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var calendar = new Calendar(2020, 10) .LeftAligned() .AddCalendarEvent(new DateTime(2020, 9, 1)) @@ -65,7 +66,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Right_Align_Calendar_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var calendar = new Calendar(2020, 10) .RightAligned() .AddCalendarEvent(new DateTime(2020, 9, 1)) @@ -83,7 +84,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Calendar_Correctly_For_Specific_Culture() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var calendar = new Calendar(2020, 10, 15) .Culture("de-DE") .AddCalendarEvent(new DateTime(2020, 9, 1)) diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs new file mode 100644 index 0000000..73b4232 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.Rendering.cs @@ -0,0 +1,92 @@ +using System.Threading.Tasks; +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli.Annotations +{ + public sealed partial class CommandArgumentAttributeTests + { + [UsesVerify] + public sealed class TheArgumentCannotContainOptionsMethod + { + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "--foo ")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var result = Fixture.Run(); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheMultipleValuesAreNotSupportedMethod + { + public sealed class Settings : CommandSettings + { + [CommandArgument(0, " ")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var result = Fixture.Run(); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheValuesMustHaveNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandArgument(0, "<>")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var result = Fixture.Run(); + + // Then + return Verifier.Verify(result); + } + } + + private static class Fixture + { + public static string Run(params string[] args) + where TSettings : CommandSettings + { + using (var writer = new FakeConsole()) + { + var app = new CommandApp(); + app.Configure(c => c.ConfigureConsole(writer)); + app.Configure(c => c.AddCommand>("foo")); + app.Run(args); + + return writer.Output + .NormalizeLineEndings() + .TrimLines() + .Trim(); + } + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs new file mode 100644 index 0000000..b4689fd --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandArgumentAttributeTests.cs @@ -0,0 +1,64 @@ +using Shouldly; +using Spectre.Console.Cli; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli.Annotations +{ + public sealed partial class CommandArgumentAttributeTests + { + [Fact] + public void Should_Not_Contain_Options() + { + // Given, When + var result = Record.Exception(() => new CommandArgumentAttribute(0, "--foo ")); + + // Then + result.ShouldNotBe(null); + result.ShouldBeOfType().And(exception => + exception.Message.ShouldBe("Arguments can not contain options.")); + } + + [Theory] + [InlineData(" ")] + [InlineData("[FOO] [BAR]")] + [InlineData("[FOO] ")] + [InlineData(" [BAR]")] + public void Should_Not_Contain_Multiple_Value_Names(string template) + { + // Given, When + var result = Record.Exception(() => new CommandArgumentAttribute(0, template)); + + // Then + result.ShouldNotBe(null); + result.ShouldBeOfType().And(exception => + exception.Message.ShouldBe("Multiple values are not supported.")); + } + + [Theory] + [InlineData("<>")] + [InlineData("[]")] + public void Should_Not_Contain_Empty_Value_Name(string template) + { + // Given, When + var result = Record.Exception(() => new CommandArgumentAttribute(0, template)); + + // Then + result.ShouldNotBe(null); + result.ShouldBeOfType().And(exception => + exception.Message.ShouldBe("Values without name are not allowed.")); + } + + [Theory] + [InlineData("", true)] + [InlineData("[FOO]", false)] + public void Should_Parse_Valid_Options(string template, bool required) + { + // Given, When + var result = new CommandArgumentAttribute(0, template); + + // Then + result.ValueName.ShouldBe("FOO"); + result.IsRequired.ShouldBe(required); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs new file mode 100644 index 0000000..4185369 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.Rendering.cs @@ -0,0 +1,239 @@ +using System.Threading.Tasks; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli.Annotations +{ + public sealed partial class CommandOptionAttributeTests + { + [UsesVerify] + public sealed class TheUnexpectedCharacterMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption(" $ ")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Encountered unexpected character '$'."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheUnterminatedValueNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--foo|-f (); + + // Then + message.ShouldBe("Encountered unterminated value name 'BAR'."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheOptionsMustHaveNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--foo|-")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Options without name are not allowed."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheOptionNamesCannotStartWithDigitMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--1foo")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Option names cannot start with a digit."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheInvalidCharacterInOptionNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--f$oo")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Encountered invalid character '$' in option name."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheLongOptionMustHaveMoreThanOneCharacterMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--f")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Long option names must consist of more than one character."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheShortOptionMustOnlyBeOneCharacterMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("--foo|-bar")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Short option names can not be longer than one character."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheMultipleOptionValuesAreNotSupportedMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("-f|--foo ")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Multiple option values are not supported."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheInvalidCharacterInValueNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("-f|--foo ")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("Encountered invalid character '$' in value name."); + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class TheMissingLongAndShortNameMethod + { + public sealed class Settings : CommandSettings + { + [CommandOption("")] + public string Foo { get; set; } + } + + [Fact] + public Task Should_Return_Correct_Text() + { + // Given, When + var (message, result) = Fixture.Run(); + + // Then + message.ShouldBe("No long or short name for option has been specified."); + return Verifier.Verify(result); + } + } + + private static class Fixture + { + public static (string Message, string Output) Run(params string[] args) + where TSettings : CommandSettings + { + var app = new CommandAppFixture(); + app.Configure(c => + { + c.PropagateExceptions(); + c.AddCommand>("foo"); + }); + + return app.RunAndCatch(args); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs new file mode 100644 index 0000000..d9efb57 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/Annotations/CommandOptionAttributeTests.cs @@ -0,0 +1,197 @@ +using Shouldly; +using Spectre.Console.Cli; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli.Annotations +{ + public sealed partial class CommandOptionAttributeTests + { + [Fact] + public void Should_Parse_Short_Name_Correctly() + { + // Given, When + var option = new CommandOptionAttribute("-o|--option "); + + // Then + option.ShortNames.ShouldContain("o"); + } + + [Fact] + public void Should_Parse_Long_Name_Correctly() + { + // Given, When + var option = new CommandOptionAttribute("-o|--option "); + + // Then + option.LongNames.ShouldContain("option"); + } + + [Theory] + [InlineData("")] + public void Should_Parse_Value_Correctly(string value) + { + // Given, When + var option = new CommandOptionAttribute($"-o|--option {value}"); + + // Then + option.ValueName.ShouldBe("VALUE"); + } + + [Fact] + public void Should_Parse_Only_Short_Name() + { + // Given, When + var option = new CommandOptionAttribute("-o"); + + // Then + option.ShortNames.ShouldContain("o"); + } + + [Fact] + public void Should_Parse_Only_Long_Name() + { + // Given, When + var option = new CommandOptionAttribute("--option"); + + // Then + option.LongNames.ShouldContain("option"); + } + + [Theory] + [InlineData("")] + [InlineData("")] + public void Should_Throw_If_Template_Is_Empty(string value) + { + // Given, When + var option = Record.Exception(() => new CommandOptionAttribute(value)); + + // Then + option.ShouldBeOfType().And(e => + e.Message.ShouldBe("No long or short name for option has been specified.")); + } + + [Theory] + [InlineData("--bar|-foo")] + [InlineData("--bar|-f-b")] + public void Should_Throw_If_Short_Name_Is_Invalid(string value) + { + // Given, When + var option = Record.Exception(() => new CommandOptionAttribute(value)); + + // Then + option.ShouldBeOfType().And(e => + e.Message.ShouldBe("Short option names can not be longer than one character.")); + } + + [Theory] + [InlineData("--o")] + public void Should_Throw_If_Long_Name_Is_Invalid(string value) + { + // Given, When + var option = Record.Exception(() => new CommandOptionAttribute(value)); + + // Then + option.ShouldBeOfType().And(e => + e.Message.ShouldBe("Long option names must consist of more than one character.")); + } + + [Theory] + [InlineData("-")] + [InlineData("--")] + public void Should_Throw_If_Option_Have_No_Name(string template) + { + // Given, When + var option = Record.Exception(() => new CommandOptionAttribute(template)); + + // Then + option.ShouldBeOfType().And(e => + e.Message.ShouldBe("Options without name are not allowed.")); + } + + [Theory] + [InlineData("--foo|-foo[b", '[')] + [InlineData("--foo|-f€b", '€')] + [InlineData("--foo|-foo@b", '@')] + public void Should_Throw_If_Option_Contains_Invalid_Name(string template, char invalid) + { + // Given, When + var result = Record.Exception(() => new CommandOptionAttribute(template)); + + // Then + result.ShouldBeOfType().And(e => + { + e.Message.ShouldBe($"Encountered invalid character '{invalid}' in option name."); + e.Template.ShouldBe(template); + }); + } + + [Theory] + [InlineData("--foo ", "HELLO-WORLD")] + [InlineData("--foo ", "HELLO_WORLD")] + public void Should_Accept_Dash_And_Underscore_In_Value_Name(string template, string name) + { + // Given, When + var result = new CommandOptionAttribute(template); + + // Then + result.ValueName.ShouldBe(name); + } + + [Theory] + [InlineData("--foo|-1")] + public void Should_Throw_If_First_Letter_Of_An_Option_Name_Is_A_Digit(string template) + { + // Given, When + var result = Record.Exception(() => new CommandOptionAttribute(template)); + + // Then + result.ShouldBeOfType().And(e => + { + e.Message.ShouldBe("Option names cannot start with a digit."); + e.Template.ShouldBe(template); + }); + } + + [Fact] + public void Multiple_Short_Options_Are_Supported() + { + // Given, When + var result = new CommandOptionAttribute("-f|-b"); + + // Then + result.ShortNames.Count.ShouldBe(2); + result.ShortNames.ShouldContain("f"); + result.ShortNames.ShouldContain("b"); + } + + [Fact] + public void Multiple_Long_Options_Are_Supported() + { + // Given, When + var result = new CommandOptionAttribute("--foo|--bar"); + + // Then + result.LongNames.Count.ShouldBe(2); + result.LongNames.ShouldContain("foo"); + result.LongNames.ShouldContain("bar"); + } + + [Theory] + [InlineData("-f|--foo ")] + [InlineData("--foo|-f ")] + [InlineData(" --foo|-f")] + [InlineData(" -f|--foo")] + [InlineData("-f --foo")] + [InlineData("--foo -f")] + public void Template_Parts_Can_Appear_In_Any_Order(string template) + { + // Given, When + var result = new CommandOptionAttribute(template); + + // Then + result.LongNames.ShouldContain("foo"); + result.ShortNames.ShouldContain("f"); + result.ValueName.ShouldBe("BAR"); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs new file mode 100644 index 0000000..9e7483d --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.FlagValues.cs @@ -0,0 +1,234 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Tests.Data; +using Spectre.Console.Testing; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class FlagValues + { + [SuppressMessage("Performance", "CA1812", Justification = "It's OK")] + private sealed class FlagSettings : CommandSettings + { + [CommandOption("--serve [PORT]")] + public FlagValue Serve { get; set; } + } + + [SuppressMessage("Performance", "CA1812", Justification = "It's OK")] + private sealed class FlagSettingsWithNullableValueType : CommandSettings + { + [CommandOption("--serve [PORT]")] + public FlagValue Serve { get; set; } + } + + [SuppressMessage("Performance", "CA1812", Justification = "It's OK")] + private sealed class FlagSettingsWithOptionalOptionButNoFlagValue : CommandSettings + { + [CommandOption("--serve [PORT]")] + public int Serve { get; set; } + } + + [SuppressMessage("Performance", "CA1812", Justification = "It's OK")] + private sealed class FlagSettingsWithDefaultValue : CommandSettings + { + [CommandOption("--serve [PORT]")] + [DefaultValue(987)] + public FlagValue Serve { get; set; } + } + + [Fact] + public void Should_Throw_If_Command_Option_Value_Is_Optional_But_Type_Is_Not_A_Flag_Value() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "foo", "--serve", "123" })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The option 'serve' has an optional value but does not implement IFlagValue."); + }); + } + + [Fact] + public void Should_Set_Flag_And_Value_If_Both_Were_Provided() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "foo", "--serve", "123", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(flag => + { + flag.Serve.IsSet.ShouldBeTrue(); + flag.Serve.Value.ShouldBe(123); + }); + } + + [Fact] + public void Should_Only_Set_Flag_If_No_Value_Was_Provided() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "foo", "--serve", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(flag => + { + flag.Serve.IsSet.ShouldBeTrue(); + flag.Serve.Value.ShouldBe(0); + }); + } + + [Fact] + public void Should_Set_Value_To_Default_Value_If_None_Was_Explicitly_Set() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "foo", "--serve", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(flag => + { + flag.Serve.IsSet.ShouldBeTrue(); + flag.Serve.Value.ShouldBe(987); + }); + } + + [Fact] + public void Should_Create_Unset_Instance_If_Flag_Was_Not_Set() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "foo", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(flag => + { + flag.Serve.IsSet.ShouldBeFalse(); + flag.Serve.Value.ShouldBe(0); + }); + } + + [Fact] + public void Should_Create_Unset_Instance_With_Null_For_Nullable_Value_Type_If_Flag_Was_Not_Set() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "foo", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(flag => + { + flag.Serve.IsSet.ShouldBeFalse(); + flag.Serve.Value.ShouldBeNull(); + }); + } + + [Theory] + [InlineData("Foo", true, "Set=True, Value=Foo")] + [InlineData("Bar", false, "Set=False, Value=Bar")] + public void Should_Return_Correct_String_Representation_From_ToString( + string value, + bool isSet, + string expected) + { + // Given + var flag = new FlagValue(); + flag.Value = value; + flag.IsSet = isSet; + + // When + var result = flag.ToString(); + + // Then + result.ShouldBe(expected); + } + + [Theory] + [InlineData(true, "Set=True")] + [InlineData(false, "Set=False")] + public void Should_Return_Correct_String_Representation_From_ToString_If_Value_Is_Not_Set( + bool isSet, + string expected) + { + // Given + var flag = new FlagValue(); + flag.IsSet = isSet; + + // When + var result = flag.ToString(); + + // Then + result.ShouldBe(expected); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs new file mode 100644 index 0000000..2d7c763 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Help.cs @@ -0,0 +1,231 @@ +using System.Threading.Tasks; +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + [UsesVerify] + public class Help + { + [Fact] + public Task Should_Output_Root_Correctly() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + configurator.AddCommand("horse"); + configurator.AddCommand("giraffe"); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Skip_Hidden_Commands() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog"); + configurator.AddCommand("horse"); + configurator.AddCommand("giraffe").IsHidden(); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Command_Correctly() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("cat", animal => + { + animal.SetDescription("Contains settings for a cat."); + animal.AddCommand("lion"); + }); + }); + + // When + var (_, output, _, _) = fixture.Run("cat", "--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Leaf_Correctly() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("cat", animal => + { + animal.SetDescription("Contains settings for a cat."); + animal.AddCommand("lion"); + }); + }); + + // When + var (_, output, _, _) = fixture.Run("cat", "lion", "--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Default_Command_Correctly() + { + // Given + var fixture = new CommandAppFixture(); + fixture.WithDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Root_Examples_Defined_On_Root() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); + configurator.AddExample(new[] { "horse", "--name", "Brutus" }); + configurator.AddCommand("dog"); + configurator.AddCommand("horse"); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Root_Examples_Defined_On_Direct_Children_If_Root_Have_No_Examples() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddCommand("dog") + .WithExample(new[] { "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); + configurator.AddCommand("horse") + .WithExample(new[] { "horse", "--name", "Brutus" }); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Root_Examples_Defined_On_Leaves_If_No_Other_Examples_Are_Found() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + animal.SetDescription("The animal command."); + animal.AddCommand("dog") + .WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); + animal.AddCommand("horse") + .WithExample(new[] { "animal", "horse", "--name", "Brutus" }); + }); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Only_Output_Command_Examples_Defined_On_Command() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddBranch("animal", animal => + { + animal.SetDescription("The animal command."); + animal.AddExample(new[] { "animal", "--help" }); + + animal.AddCommand("dog") + .WithExample(new[] { "animal", "dog", "--name", "Rufus", "--age", "12", "--good-boy" }); + animal.AddCommand("horse") + .WithExample(new[] { "animal", "horse", "--name", "Brutus" }); + }); + }); + + // When + var (_, output, _, _) = fixture.Run("animal", "--help"); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Output_Root_Examples_If_Default_Command_Is_Specified() + { + // Given + var fixture = new CommandAppFixture(); + fixture.WithDefaultCommand(); + fixture.Configure(configurator => + { + configurator.SetApplicationName("myapp"); + configurator.AddExample(new[] { "12", "-c", "3" }); + }); + + // When + var (_, output, _, _) = fixture.Run("--help"); + + // Then + return Verifier.Verify(output); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs new file mode 100644 index 0000000..77a56c2 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Injection.cs @@ -0,0 +1,67 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Injection + { + public sealed class FakeDependency + { + } + + public sealed class InjectSettings : CommandSettings + { + public FakeDependency Fake { get; set; } + + [CommandOption("--name ")] + public string Name { get; } + + [CommandOption("--age ")] + public int Age { get; set; } + + public InjectSettings(FakeDependency fake, string name) + { + Fake = fake; + Name = "Hello " + name; + } + } + + [Fact] + public void Should_Inject_Parameters() + { + // Given + var app = new CommandAppFixture(); + var dependency = new FakeDependency(); + + app.WithDefaultCommand>(); + app.Configure(config => + { + config.Settings.Registrar.RegisterInstance(dependency); + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--name", "foo", + "--age", "35", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(injected => + { + injected.ShouldNotBeNull(); + injected.Fake.ShouldBeSameAs(dependency); + injected.Name.ShouldBe("Hello foo"); + injected.Age.ShouldBe(35); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs new file mode 100644 index 0000000..3e8cb64 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Pairs.cs @@ -0,0 +1,292 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Pairs + { + public sealed class AmbiguousSettings : CommandSettings + { + [CommandOption("--var ")] + [PairDeconstructor(typeof(StringIntDeconstructor))] + [TypeConverter(typeof(CatAgilityConverter))] + public ILookup Values { get; set; } + } + + public sealed class NotDeconstructableSettings : CommandSettings + { + [CommandOption("--var ")] + [PairDeconstructor(typeof(StringIntDeconstructor))] + public string Values { get; set; } + } + + public sealed class DefaultPairDeconstructorSettings : CommandSettings + { + [CommandOption("--var ")] + public IDictionary Values { get; set; } + } + + public sealed class LookupSettings : CommandSettings + { + [CommandOption("--var ")] + [PairDeconstructor(typeof(StringIntDeconstructor))] + public ILookup Values { get; set; } + } + + public sealed class DictionarySettings : CommandSettings + { + [CommandOption("--var ")] + [PairDeconstructor(typeof(StringIntDeconstructor))] + public IDictionary Values { get; set; } + } + + public sealed class ReadOnlyDictionarySettings : CommandSettings + { + [CommandOption("--var ")] + [PairDeconstructor(typeof(StringIntDeconstructor))] + public IReadOnlyDictionary Values { get; set; } + } + + public sealed class StringIntDeconstructor : PairDeconstuctor + { + protected override (string Key, string Value) Deconstruct(string value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var parts = value.Split(new[] { '=' }); + if (parts.Length != 2) + { + throw new InvalidOperationException("Could not parse pair"); + } + + return (parts[0], parts[1]); + } + } + + [Fact] + public void Should_Throw_If_Option_Has_Pair_Deconstructor_And_Type_Converter() + { + // Given + var app = new CommandApp>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "--var", "foo=bar", + "--var", "foo=qux", + })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The option 'var' is both marked as pair deconstructable and convertable."); + }); + } + + [Fact] + public void Should_Throw_If_Option_Has_Pair_Deconstructor_But_Type_Is_Not_Deconstructable() + { + // Given + var app = new CommandApp>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "--var", "foo=bar", + "--var", "foo=qux", + })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The option 'var' is marked as pair deconstructable, but the underlying type does not support that."); + }); + } + + [Fact] + public void Should_Map_Pairs_To_Pair_Deconstructable_Collection_Using_Default_Deconstructort() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--var", "foo=1", + "--var", "foo=3", + "--var", "bar=4", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(pair => + { + pair.Values.ShouldNotBeNull(); + pair.Values.Count.ShouldBe(2); + pair.Values["foo"].ShouldBe(3); + pair.Values["bar"].ShouldBe(4); + }); + } + + [Theory] + [InlineData("foo=1=2", "Error: The value 'foo=1=2' is not in a correct format")] + [InlineData("foo=1=2=3", "Error: The value 'foo=1=2=3' is not in a correct format")] + public void Should_Throw_If_Value_Is_Not_In_A_Valid_Format_Using_Default_Deconstructor( + string input, string expected) + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + + // When + var (result, output, _, settings) = app.Run(new[] + { + "--var", input, + }); + + // Then + result.ShouldBe(-1); + output.ShouldBe(expected); + } + + [Fact] + public void Should_Map_Lookup_Values() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--var", "foo=bar", + "--var", "foo=qux", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(pair => + { + pair.Values.ShouldNotBeNull(); + pair.Values.Count.ShouldBe(1); + pair.Values["foo"].ToList().Count.ShouldBe(2); + }); + } + + [Fact] + public void Should_Map_Dictionary_Values() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--var", "foo=bar", + "--var", "baz=qux", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(pair => + { + pair.Values.ShouldNotBeNull(); + pair.Values.Count.ShouldBe(2); + pair.Values["foo"].ShouldBe("bar"); + pair.Values["baz"].ShouldBe("qux"); + }); + } + + [Fact] + public void Should_Map_Latest_Value_Of_Same_Key_When_Mapping_To_Dictionary() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--var", "foo=bar", + "--var", "foo=qux", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(pair => + { + pair.Values.ShouldNotBeNull(); + pair.Values.Count.ShouldBe(1); + pair.Values["foo"].ShouldBe("qux"); + }); + } + + [Fact] + public void Should_Map_ReadOnly_Dictionary_Values() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "--var", "foo=bar", + "--var", "baz=qux", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(pair => + { + pair.Values.ShouldNotBeNull(); + pair.Values.Count.ShouldBe(2); + pair.Values["foo"].ShouldBe("bar"); + pair.Values["baz"].ShouldBe("qux"); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs new file mode 100644 index 0000000..8820ff5 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Parsing.cs @@ -0,0 +1,615 @@ +using System; +using System.Threading.Tasks; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using VerifyXunit; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public static class Parsing + { + [UsesVerify] + public sealed class UnknownCommand + { + [Fact] + public Task Should_Return_Correct_Text_When_Command_Is_Unknown() + { + // Given + var fixture = new Fixture(); + fixture.Configure(config => + { + config.AddCommand("dog"); + }); + + // When + var result = fixture.Run("cat", "14"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.Configure(config => + { + config.AddCommand("cat"); + }); + + // When + var result = fixture.Run("bat", "14"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_When_Command_Followed_By_Argument_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.Configure(config => + { + config.AddBranch("dog", a => + { + a.AddCommand("cat"); + }); + }); + + // When + var result = fixture.Run("dog", "bat", "14"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Root_Command_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.WithDefaultCommand>(); + fixture.Configure(config => + { + config.AddCommand>("cat"); + }); + + // When + var result = fixture.Run("bat"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_And_No_Arguments_When_Command_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.WithDefaultCommand>(); + fixture.Configure(configurator => + { + configurator.AddBranch("dog", a => + { + a.AddCommand("cat"); + }); + }); + + // When + var result = fixture.Run("dog", "bat"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_When_Root_Command_After_Argument_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.WithDefaultCommand>(); + fixture.Configure(configurator => + { + configurator.AddCommand>("bar"); + }); + + // When + var result = fixture.Run("qux", "bat"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_With_Suggestion_When_Command_After_Argument_Is_Unknown_And_Distance_Is_Small() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddBranch("foo", a => + { + a.AddCommand>("bar"); + }); + }); + + // When + var result = fixture.Run("foo", "qux", "bat"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Unknown_Command_When_Current_Command_Has_No_Arguments() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("empty"); + }); + + // When + var result = fixture.Run("empty", "other"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class CannotAssignValueToFlag + { + [Fact] + public Task Should_Return_Correct_Text_For_Long_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "--alive", "foo"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Short_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "-a", "foo"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class NoValueForOption + { + [Fact] + public Task Should_Return_Correct_Text_For_Long_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "--name"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Short_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "-n"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class NoMatchingArgument + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("giraffe"); + }); + + // When + var result = fixture.Run("giraffe", "foo", "bar", "baz"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class UnexpectedOption + { + [Fact] + public Task Should_Return_Correct_Text_For_Long_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("--foo"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Short_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("-f"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class UnknownOption + { + [Fact] + public Task Should_Return_Correct_Text_For_Long_Option_If_Strict_Mode_Is_Enabled() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.UseStrictParsing(); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "--unknown"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Short_Option_If_Strict_Mode_Is_Enabled() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.UseStrictParsing(); + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "-u"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class UnterminatedQuote + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("--name", "\"Rufus"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class OptionWithoutName + { + [Fact] + public Task Should_Return_Correct_Text_For_Short_Option() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", "-", " "); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Equality_Separator() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"--foo="); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Missing_Long_Option_Value_With_Colon_Separator() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"--foo:"); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Equality_Separator() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"-f="); + + // Then + return Verifier.Verify(result); + } + + [Fact] + public Task Should_Return_Correct_Text_For_Missing_Short_Option_Value_With_Colon_Separator() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"-f:"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class InvalidShortOptionName + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"-f0o"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class LongOptionNameIsOneCharacter + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"--f"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class LongOptionNameIsMissing + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"-- "); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class LongOptionNameStartWithDigit + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"--1foo"); + + // Then + return Verifier.Verify(result); + } + } + + [UsesVerify] + public sealed class LongOptionNameContainSymbol + { + [Fact] + public Task Should_Return_Correct_Text() + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", $"--f€oo"); + + // Then + return Verifier.Verify(result); + } + + [Theory] + [InlineData("--f-oo")] + [InlineData("--f-o-o")] + [InlineData("--f_oo")] + [InlineData("--f_o_o")] + public void Should_Allow_Special_Symbols_In_Name(string option) + { + // Given + var fixture = new Fixture(); + fixture.Configure(configurator => + { + configurator.AddCommand("dog"); + }); + + // When + var result = fixture.Run("dog", option); + + // Then + result.ShouldBe("Error: Command 'dog' is missing required argument 'AGE'."); + } + } + } + + internal sealed class Fixture + { + private Action _appConfiguration = _ => { }; + private Action _configuration; + + public void WithDefaultCommand() + where T : class, ICommand + { + _appConfiguration = (app) => app.SetDefaultCommand(); + } + + public void Configure(Action action) + { + _configuration = action; + } + + public string Run(params string[] args) + { + using (var console = new FakeConsole()) + { + var app = new CommandApp(); + _appConfiguration?.Invoke(app); + + app.Configure(_configuration); + app.Configure(c => c.ConfigureConsole(console)); + app.Run(args); + + return console.Output + .NormalizeLineEndings() + .TrimLines() + .Trim(); + } + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs new file mode 100644 index 0000000..d82150e --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Sensitivity.cs @@ -0,0 +1,118 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandApptests + { + [Fact] + public void Should_Treat_Commands_As_Case_Sensitive_If_Specified() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.CaseSensitivity(CaseSensitivity.Commands); + config.AddCommand>("command"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "Command", "--foo", "bar", + })); + + // Then + result.ShouldNotBeNull(); + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Unknown command 'Command'."); + }); + } + + [Fact] + public void Should_Treat_Long_Options_As_Case_Sensitive_If_Specified() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.CaseSensitivity(CaseSensitivity.LongOptions); + config.AddCommand>("command"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "command", "--Foo", "bar", + })); + + // Then + result.ShouldNotBeNull(); + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Unknown option 'Foo'."); + }); + } + + [Fact] + public void Should_Treat_Short_Options_As_Case_Sensitive() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.AddCommand>("command"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] + { + "command", "-F", "bar", + })); + + // Then + result.ShouldNotBeNull(); + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Unknown option 'F'."); + }); + } + + [Fact] + public void Should_Suppress_Case_Sensitivity_If_Specified() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.UseStrictParsing(); + config.PropagateExceptions(); + config.CaseSensitivity(CaseSensitivity.None); + config.AddCommand>("command"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "Command", "--Foo", "bar", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.ShouldBe("bar"); + }); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs new file mode 100644 index 0000000..a794807 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Settings.cs @@ -0,0 +1,26 @@ +using Shouldly; +using Spectre.Console.Cli; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + [Fact] + public void Should_Apply_Case_Sensitivity_For_Everything_By_Default() + { + // Given + var app = new CommandApp(); + + // When + var defaultSensitivity = CaseSensitivity.None; + app.Configure(config => + { + defaultSensitivity = config.Settings.CaseSensitivity; + }); + + // Then + defaultSensitivity.ShouldBe(CaseSensitivity.All); + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs new file mode 100644 index 0000000..820f84c --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.TypeConverters.cs @@ -0,0 +1,40 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class TypeConverters + { + [Fact] + public void Should_Bind_Using_Custom_Type_Converter_If_Specified() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("cat"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "cat", "--name", "Tiger", + "--agility", "FOOBAR", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(cat => + { + cat.Agility.ShouldBe(6); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs new file mode 100644 index 0000000..75460a8 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Unsafe.cs @@ -0,0 +1,299 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Spectre.Console.Cli.Unsafe; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class SafetyOff + { + [Fact] + public void Can_Mix_Safe_And_Unsafe_Configurators() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + + config.AddBranch("animal", animal => + { + animal.SafetyOff().AddBranch("mammal", typeof(MammalSettings), mammal => + { + mammal.AddCommand("dog", typeof(DogCommand)); + mammal.AddCommand("horse", typeof(HorseCommand)); + }); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "--alive", "mammal", "--name", + "Rufus", "dog", "12", "--good-boy", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Can_Turn_Safety_On_After_Turning_It_Off_For_Branch() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + + config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal => + { + animal.SafetyOn() + .AddBranch("mammal", mammal => + { + mammal.SafetyOff().AddCommand("dog", typeof(DogCommand)); + mammal.AddCommand("horse"); + }); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "--alive", "mammal", "--name", + "Rufus", "dog", "12", "--good-boy", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Throw_If_Trying_To_Convert_Unsafe_Branch_Configurator_To_Safe_Version_With_Wrong_Type() + { + // Given + var app = new CommandApp(); + + // When + var result = Record.Exception(() => app.Configure(config => + { + config.PropagateExceptions(); + + config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal => + { + animal.SafetyOn().AddCommand("dog"); + }); + })); + + // Then + result.ShouldBeOfType(); + result.Message.ShouldBe("Configurator cannot be converted to a safe configurator of type 'MammalSettings'."); + } + + [Fact] + public void Should_Pass_Case_1() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + + config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal => + { + animal.AddBranch("mammal", typeof(MammalSettings), mammal => + { + mammal.AddCommand("dog", typeof(DogCommand)); + mammal.AddCommand("horse", typeof(HorseCommand)); + }); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "--alive", "mammal", "--name", + "Rufus", "dog", "12", "--good-boy", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Pass_Case_2() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.SafetyOff().AddCommand("dog", typeof(DogCommand)); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "dog", "12", "4", "--good-boy", + "--name", "Rufus", "--alive", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(12); + dog.Age.ShouldBe(4); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Pass_Case_3() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal => + { + animal.AddCommand("dog", typeof(DogCommand)); + animal.AddCommand("horse", typeof(HorseCommand)); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "dog", "12", "--good-boy", + "--name", "Rufus", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(false); + }); + } + + [Fact] + public void Should_Pass_Case_4() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.SafetyOff().AddBranch("animal", typeof(AnimalSettings), animal => + { + animal.AddCommand("dog", typeof(DogCommand)); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "4", "dog", "12", + "--good-boy", "--name", "Rufus", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.IsAlive.ShouldBe(false); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Pass_Case_5() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.SafetyOff().AddCommand("multi", typeof(OptionVectorCommand)); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multi", "--foo", "a", "--foo", "b", "--bar", "1", "--foo", "c", "--bar", "2", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(3); + vec.Foo.ShouldBe(new[] { "a", "b", "c" }); + vec.Bar.Length.ShouldBe(2); + vec.Bar.ShouldBe(new[] { 1, 2 }); + }); + } + + [Fact] + public void Should_Pass_Case_6() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("multi"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multi", "a", "b", "c", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(3); + vec.Foo.ShouldBe(new[] { "a", "b", "c" }); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs new file mode 100644 index 0000000..97c7607 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Validation.cs @@ -0,0 +1,88 @@ +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Tests.Data; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Validation + { + [Fact] + public void Should_Throw_If_Attribute_Validation_Fails() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "animal", "3", "dog", "7", "--name", "Rufus" })); + + // Then + result.ShouldBeOfType().And(e => + { + e.Message.ShouldBe("Animals must have an even number of legs."); + }); + } + + [Fact] + public void Should_Throw_If_Settings_Validation_Fails() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "animal", "4", "dog", "7", "--name", "Tiger" })); + + // Then + result.ShouldBeOfType().And(e => + { + e.Message.ShouldBe("Tiger is not a dog name!"); + }); + } + + [Fact] + public void Should_Throw_If_Command_Validation_Fails() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "animal", "4", "dog", "101", "--name", "Rufus" })); + + // Then + result.ShouldBeOfType().And(e => + { + e.Message.ShouldBe("Dog is too old..."); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs new file mode 100644 index 0000000..fe60da5 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Vectors.cs @@ -0,0 +1,111 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Vectors + { + [Fact] + public void Should_Throw_If_A_Single_Command_Has_Multiple_Argument_Vectors() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("multi"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "multi", "a", "b", "c" })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The command 'multi' specifies more than one vector argument."); + }); + } + + [Fact] + public void Should_Throw_If_An_Argument_Vector_Is_Not_Specified_Last() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("multi"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "multi", "a", "b", "c" })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The command 'multi' specifies an argument vector that is not the last argument."); + }); + } + + [Fact] + public void Should_Assign_Values_To_Argument_Vector() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("multi"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multi", "a", "b", "c", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(3); + vec.Foo[0].ShouldBe("a"); + vec.Foo[1].ShouldBe("b"); + vec.Foo[2].ShouldBe("c"); + }); + } + + [Fact] + public void Should_Assign_Values_To_Option_Vector() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("cmd"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "cmd", "--foo", "red", + "--bar", "4", "--foo", "blue", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.ShouldBe(new string[] { "red", "blue" }); + vec.Bar.ShouldBe(new int[] { 4 }); + }); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs new file mode 100644 index 0000000..324d9e2 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Version.cs @@ -0,0 +1,37 @@ +using Shouldly; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + public sealed class Version + { + [Fact] + public void Should_Output_The_Version_To_The_Console() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.AddCommand("dog"); + mammal.AddCommand("horse"); + }); + }); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.VersionCommand); + + // Then + output.ShouldStartWith("Spectre.Cli version "); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs new file mode 100644 index 0000000..321f172 --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.Xml.cs @@ -0,0 +1,148 @@ +using System.Threading.Tasks; +using Spectre.Console.Testing; +using Spectre.Console.Tests.Data; +using VerifyXunit; +using Xunit; +using Spectre.Console.Cli; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + [UsesVerify] + public sealed class Xml + { + /// + /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-1 + /// + [Fact] + public Task Should_Dump_Correct_Model_For_Case_1() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.AddCommand("dog"); + mammal.AddCommand("horse"); + }); + }); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + + /// + /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-2 + /// + [Fact] + public Task Should_Dump_Correct_Model_For_Case_2() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.AddCommand("dog"); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + + /// + /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-3 + /// + [Fact] + public Task Should_Dump_Correct_Model_For_Case_3() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + + /// + /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-4 + /// + [Fact] + public Task Should_Dump_Correct_Model_For_Case_4() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + + /// + /// https://github.com/spectresystems/spectre.cli/wiki/Test-cases#test-case-5 + /// + [Fact] + public Task Should_Dump_Correct_Model_For_Case_5() + { + // Given + var fixture = new CommandAppFixture(); + fixture.Configure(config => + { + config.AddCommand("cmd"); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + + [Fact] + public Task Should_Dump_Correct_Model_For_Model_With_Default_Command() + { + // Given + var fixture = new CommandAppFixture().WithDefaultCommand(); + fixture.Configure(config => + { + config.AddCommand("horse"); + }); + + // When + var (_, output, _, _) = fixture.Run(Constants.XmlDocCommand); + + // Then + return Verifier.Verify(output); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs new file mode 100644 index 0000000..d366b0f --- /dev/null +++ b/src/Spectre.Console.Tests/Unit/Cli/CommandAppTests.cs @@ -0,0 +1,769 @@ +using System; +using Shouldly; +using Spectre.Console.Cli; +using Spectre.Console.Tests.Data; +using Spectre.Console.Testing; +using Xunit; + +namespace Spectre.Console.Tests.Unit.Cli +{ + public sealed partial class CommandAppTests + { + [Fact] + public void Should_Pass_Case_1() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddBranch("mammal", mammal => + { + mammal.AddCommand("dog"); + mammal.AddCommand("horse"); + }); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "--alive", "mammal", "--name", + "Rufus", "dog", "12", "--good-boy", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Pass_Case_2() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "dog", "12", "4", "--good-boy", + "--name", "Rufus", "--alive", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(12); + dog.Age.ShouldBe(4); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(true); + }); + } + + [Fact] + public void Should_Pass_Case_3() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "dog", "12", "--good-boy", + "--name", "Rufus", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + dog.IsAlive.ShouldBe(false); + }); + } + + [Fact] + public void Should_Pass_Case_4() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "animal", "4", "dog", "12", "--good-boy", + "--name", "Rufus", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.IsAlive.ShouldBe(false); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Pass_Case_5() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("multi"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multi", "--foo", "a", "--foo", "b", + "--bar", "1", "--foo", "c", "--bar", "2", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(3); + vec.Foo.ShouldBe(new[] { "a", "b", "c" }); + vec.Bar.Length.ShouldBe(2); + vec.Bar.ShouldBe(new[] { 1, 2 }); + }); + } + + [Fact] + public void Should_Pass_Case_6() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("multi"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multi", "a", "b", "c", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(3); + vec.Foo.ShouldBe(new[] { "a", "b", "c" }); + }); + } + + [Fact] + public void Should_Be_Able_To_Use_Command_Alias() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("multi").WithAlias("multiple"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "multiple", "--foo", "a", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(vec => + { + vec.Foo.Length.ShouldBe(1); + vec.Foo.ShouldBe(new[] { "a" }); + }); + } + + [Fact] + public void Should_Assign_Default_Value_To_Optional_Argument() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(Array.Empty()); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(settings => + { + settings.Greeting.ShouldBe("Hello World"); + }); + } + + [Fact] + public void Should_Assign_Default_Value_To_Optional_Argument_Using_Converter_If_Necessary() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var (result, _, _, settings) = app.Run(Array.Empty()); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(settings => + { + settings.Greeting.ShouldBe(5); + }); + } + + [Fact] + public void Should_Throw_If_Required_Argument_Have_Default_Value() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand>(); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + var result = Record.Exception(() => app.Run(Array.Empty())); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The required argument 'GREETING' cannot have a default value."); + }); + } + + [Fact] + public void Should_Throw_If_Alias_Conflicts_With_Another_Command() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog").WithAlias("cat"); + config.AddCommand("cat"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "dog", "4", "12" })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("The alias 'cat' for 'dog' conflicts with another command."); + }); + } + + [Fact] + public void Should_Register_Commands_When_Configuring_Application() + { + // Given + var registrar = new FakeTypeRegistrar(); + var app = new CommandApp(registrar); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand>("foo"); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + app.Run(new[] + { + "animal", "4", "dog", "12", + }); + + // Then + registrar.Registrations.ContainsKey(typeof(ICommand)).ShouldBeTrue(); + registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(GenericCommand)); + registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(DogCommand)); + registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(HorseCommand)); + } + + [Fact] + public void Should_Register_Default_Command_When_Configuring_Application() + { + // Given + var registrar = new FakeTypeRegistrar(); + var app = new CommandApp(registrar); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + app.Run(new[] + { + "12", "4", + }); + + // Then + registrar.Registrations.ContainsKey(typeof(ICommand)).ShouldBeTrue(); + registrar.Registrations.ContainsKey(typeof(DogSettings)); + registrar.Registrations[typeof(ICommand)].ShouldContain(typeof(DogCommand)); + } + + [Fact] + public void Should_Register_Default_Command_Settings_When_Configuring_Application() + { + // Given + var registrar = new FakeTypeRegistrar(); + var app = new CommandApp(registrar); + app.Configure(config => + { + config.PropagateExceptions(); + }); + + // When + app.Run(new[] + { + "12", "4", + }); + + // Then + registrar.Registrations.ContainsKey(typeof(DogSettings)); + registrar.Registrations[typeof(DogSettings)].Count.ShouldBe(1); + registrar.Registrations[typeof(DogSettings)].ShouldContain(typeof(DogSettings)); + } + + [Theory] + [InlineData("true", true)] + [InlineData("True", true)] + [InlineData("false", false)] + [InlineData("False", false)] + public void Should_Accept_Explicit_Boolan_Flag(string value, bool expected) + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddCommand("dog"); + }); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "dog", "12", "4", "--alive", value, + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.IsAlive.ShouldBe(expected); + }); + } + + [Fact] + public void Should_Register_Command_Settings_When_Configuring_Application() + { + // Given + var registrar = new FakeTypeRegistrar(); + var app = new CommandApp(registrar); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + app.Run(new[] + { + "animal", "4", "dog", "12", + }); + + // Then + registrar.Registrations.ContainsKey(typeof(DogSettings)).ShouldBeTrue(); + registrar.Registrations[typeof(DogSettings)].Count.ShouldBe(1); + registrar.Registrations[typeof(DogSettings)].ShouldContain(typeof(DogSettings)); + registrar.Registrations.ContainsKey(typeof(MammalSettings)).ShouldBeTrue(); + registrar.Registrations[typeof(MammalSettings)].Count.ShouldBe(1); + registrar.Registrations[typeof(MammalSettings)].ShouldContain(typeof(MammalSettings)); + } + + [Fact] + public void Should_Throw_When_Encountering_Unknown_Option_In_Strict_Mode() + { + // Given + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.UseStrictParsing(); + config.AddCommand("dog"); + }); + + // When + var result = Record.Exception(() => app.Run(new[] { "dog", "--foo" })); + + // Then + result.ShouldBeOfType().And(ex => + { + ex.Message.ShouldBe("Unknown option 'foo'."); + }); + } + + [Fact] + public void Should_Add_Unknown_Option_To_Remaining_Arguments_In_Relaxed_Mode() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", + "--foo", "bar", + }); + + // Then + ctx.ShouldNotBeNull(); + ctx.Remaining.Parsed.Count.ShouldBe(1); + ctx.ShouldHaveRemainingArgument("foo", values: new[] { "bar" }); + } + + [Fact] + public void Should_Add_Unknown_Boolean_Option_To_Remaining_Arguments_In_Relaxed_Mode() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", "--foo", + }); + + // Then + ctx.ShouldNotBeNull(); + ctx.Remaining.Parsed.Count.ShouldBe(1); + ctx.ShouldHaveRemainingArgument("foo", values: new[] { (string)null }); + } + + [Fact] + public void Should_Be_Able_To_Set_The_Default_Command() + { + // Given + var app = new CommandAppFixture(); + app.WithDefaultCommand(); + + // When + var (result, _, _, settings) = app.Run(new[] + { + "4", "12", "--good-boy", "--name", "Rufus", + }); + + // Then + result.ShouldBe(0); + settings.ShouldBeOfType().And(dog => + { + dog.Legs.ShouldBe(4); + dog.Age.ShouldBe(12); + dog.GoodBoy.ShouldBe(true); + dog.Name.ShouldBe("Rufus"); + }); + } + + [Fact] + public void Should_Set_Command_Name_In_Context() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", + }); + + // Then + ctx.ShouldNotBeNull(); + ctx.Name.ShouldBe("dog"); + } + + [Fact] + public void Should_Pass_Command_Data_In_Context() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog").WithData(123); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", + }); + + // Then + ctx.ShouldNotBeNull(); + ctx.Data.ShouldBe(123); + } + + public sealed class Delegate_Commands + { + [Fact] + public void Should_Execute_Delegate_Command_At_Root_Level() + { + // Given + var dog = default(DogSettings); + var data = 0; + + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddDelegate( + "foo", (context, settings) => + { + dog = settings; + data = (int)context.Data; + return 1; + }).WithData(2); + }); + + // When + var result = app.Run(new[] { "foo", "4", "12" }); + + // Then + result.ShouldBe(1); + dog.ShouldNotBeNull(); + dog.Age.ShouldBe(12); + dog.Legs.ShouldBe(4); + data.ShouldBe(2); + } + + [Fact] + public void Should_Execute_Nested_Delegate_Command() + { + // Given + var dog = default(DogSettings); + var data = 0; + + var app = new CommandApp(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("foo", foo => + { + foo.AddDelegate( + "bar", (context, settings) => + { + dog = settings; + data = (int)context.Data; + return 1; + }).WithData(2); + }); + }); + + // When + var result = app.Run(new[] { "foo", "4", "bar", "12" }); + + // Then + result.ShouldBe(1); + dog.ShouldNotBeNull(); + dog.Age.ShouldBe(12); + dog.Legs.ShouldBe(4); + data.ShouldBe(2); + } + } + + public sealed class Remaining_Arguments + { + [Fact] + public void Should_Register_Remaining_Parsed_Arguments_With_Context() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", "--", + "--foo", "bar", "--foo", "baz", + "-bar", "\"baz\"", "qux", + }); + + // Then + ctx.Remaining.Parsed.Count.ShouldBe(4); + ctx.ShouldHaveRemainingArgument("foo", values: new[] { "bar", "baz" }); + ctx.ShouldHaveRemainingArgument("b", values: new[] { (string)null }); + ctx.ShouldHaveRemainingArgument("a", values: new[] { (string)null }); + ctx.ShouldHaveRemainingArgument("r", values: new[] { (string)null }); + } + + [Fact] + public void Should_Register_Remaining_Raw_Arguments_With_Context() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.PropagateExceptions(); + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + }); + }); + + // When + var (result, _, ctx, _) = app.Run(new[] + { + "animal", "4", "dog", "12", "--", + "--foo", "bar", "-bar", "\"baz\"", "qux", + }); + + // Then + ctx.Remaining.Raw.Count.ShouldBe(5); + ctx.Remaining.Raw[0].ShouldBe("--foo"); + ctx.Remaining.Raw[1].ShouldBe("bar"); + ctx.Remaining.Raw[2].ShouldBe("-bar"); + ctx.Remaining.Raw[3].ShouldBe("\"baz\""); + ctx.Remaining.Raw[4].ShouldBe("qux"); + } + } + + public sealed class Exception_Handling + { + [Fact] + public void Should_Not_Propagate_Runtime_Exceptions_If_Not_Explicitly_Told_To_Do_So() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.AddBranch("animal", animal => + { + animal.AddCommand("dog"); + animal.AddCommand("horse"); + }); + }); + + // When + var (result, _, _, _) = app.Run(new[] { "animal", "4", "dog", "101", "--name", "Rufus" }); + + // Then + result.ShouldBe(-1); + } + + [Fact] + public void Should_Not_Propagate_Exceptions_If_Not_Explicitly_Told_To_Do_So() + { + // Given + var app = new CommandAppFixture(); + app.Configure(config => + { + config.AddCommand("throw"); + }); + + // When + var (result, _, _, _) = app.Run(new[] { "throw" }); + + // Then + result.ShouldBe(-1); + } + } + } +} diff --git a/src/Spectre.Console.Tests/Unit/ColumnsTests.cs b/src/Spectre.Console.Tests/Unit/ColumnsTests.cs index 119cfa8..e61c3c9 100644 --- a/src/Spectre.Console.Tests/Unit/ColumnsTests.cs +++ b/src/Spectre.Console.Tests/Unit/ColumnsTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -18,7 +19,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Columns_Correctly() { // Given - var console = new PlainConsole(width: 61); + var console = new FakeConsole(width: 61); var users = new[] { new User { Name = "Savannah Thompson", Country = "Australia" }, diff --git a/src/Spectre.Console.Tests/Unit/EmojiTests.cs b/src/Spectre.Console.Tests/Unit/EmojiTests.cs index da52b86..1e01694 100644 --- a/src/Spectre.Console.Tests/Unit/EmojiTests.cs +++ b/src/Spectre.Console.Tests/Unit/EmojiTests.cs @@ -1,4 +1,5 @@ using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -9,7 +10,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Substitute_Emoji_Shortcodes_In_Markdown() { // Given - var console = new TestableAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); + var console = new FakeAnsiConsole(ColorSystem.Standard, AnsiSupport.Yes); // When console.Markup("Hello :globe_showing_europe_africa:!"); diff --git a/src/Spectre.Console.Tests/Unit/ExceptionTests.cs b/src/Spectre.Console.Tests/Unit/ExceptionTests.cs index b7a1748..8ddd225 100644 --- a/src/Spectre.Console.Tests/Unit/ExceptionTests.cs +++ b/src/Spectre.Console.Tests/Unit/ExceptionTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Spectre.Console.Testing; using Spectre.Console.Tests.Data; using VerifyXunit; using Xunit; @@ -13,7 +14,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Write_Exception() { // Given - var console = new PlainConsole(width: 1024); + var console = new FakeConsole(width: 1024); var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When @@ -27,7 +28,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Write_Exception_With_Shortened_Types() { // Given - var console = new PlainConsole(width: 1024); + var console = new FakeConsole(width: 1024); var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When @@ -41,7 +42,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Write_Exception_With_Shortened_Methods() { // Given - var console = new PlainConsole(width: 1024); + var console = new FakeConsole(width: 1024); var dex = GetException(() => TestExceptions.MethodThatThrows(null)); // When @@ -55,7 +56,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Write_Exception_With_Inner_Exception() { // Given - var console = new PlainConsole(width: 1024); + var console = new FakeConsole(width: 1024); var dex = GetException(() => TestExceptions.ThrowWithInnerException()); // When @@ -69,7 +70,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Write_Exceptions_With_Generic_Type_Parameters_In_Callsite_As_Expected() { // Given - var console = new PlainConsole(width: 1024); + var console = new FakeConsole(width: 1024); var dex = GetException(() => TestExceptions.ThrowWithGenericInnerException()); // When diff --git a/src/Spectre.Console.Tests/Unit/FigletTests.cs b/src/Spectre.Console.Tests/Unit/FigletTests.cs index d756b78..b1b873d 100644 --- a/src/Spectre.Console.Tests/Unit/FigletTests.cs +++ b/src/Spectre.Console.Tests/Unit/FigletTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -11,8 +12,8 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Load_Font_From_Stream() { // Given - var console = new PlainConsole(width: 180); - var font = FigletFont.Load(ResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/starwars.flf")); + var console = new FakeConsole(width: 180); + var font = FigletFont.Load(EmbeddedResourceReader.LoadResourceStream("Spectre.Console.Tests/Data/starwars.flf")); var text = new FigletText(font, "Patrik was here"); // When @@ -26,7 +27,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Text_Correctly() { // Given - var console = new PlainConsole(width: 70); + var console = new FakeConsole(width: 70); var text = new FigletText(FigletFont.Default, "Patrik was here"); // When @@ -40,7 +41,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Wrapped_Text_Correctly() { // Given - var console = new PlainConsole(width: 70); + var console = new FakeConsole(width: 70); var text = new FigletText(FigletFont.Default, "Spectre.Console"); // When @@ -54,7 +55,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Left_Aligned_Text_Correctly() { // Given - var console = new PlainConsole(width: 120); + var console = new FakeConsole(width: 120); var text = new FigletText(FigletFont.Default, "Spectre.Console") .Alignment(Justify.Left); @@ -69,7 +70,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Centered_Text_Correctly() { // Given - var console = new PlainConsole(width: 120); + var console = new FakeConsole(width: 120); var text = new FigletText(FigletFont.Default, "Spectre.Console") .Alignment(Justify.Center); @@ -84,7 +85,7 @@ namespace Spectre.Console.Tests.Unit public async Task Should_Render_Right_Aligned_Text_Correctly() { // Given - var console = new PlainConsole(width: 120); + var console = new FakeConsole(width: 120); var text = new FigletText(FigletFont.Default, "Spectre.Console") .Alignment(Justify.Right); diff --git a/src/Spectre.Console.Tests/Unit/GridTests.cs b/src/Spectre.Console.Tests/Unit/GridTests.cs index afd43a6..ea3417b 100644 --- a/src/Spectre.Console.Tests/Unit/GridTests.cs +++ b/src/Spectre.Console.Tests/Unit/GridTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Shouldly; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -82,7 +83,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Add_Empty_Row() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumns(2); grid.AddRow("Foo", "Bar"); @@ -100,7 +101,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Add_Empty_Row_At_The_End() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumns(2); grid.AddRow("Foo", "Bar"); @@ -120,7 +121,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Grid_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumn(); grid.AddColumn(); @@ -139,7 +140,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Grid_Column_Alignment_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumn(new GridColumn { Alignment = Justify.Right }); grid.AddColumn(new GridColumn { Alignment = Justify.Center }); @@ -159,7 +160,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Use_Default_Padding() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumns(3); grid.AddRow("Foo", "Bar", "Baz"); @@ -177,7 +178,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Explicit_Grid_Column_Padding_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumn(new GridColumn { Padding = new Padding(3, 0, 0, 0) }); grid.AddColumn(new GridColumn { Padding = new Padding(0, 0, 0, 0) }); @@ -196,7 +197,7 @@ namespace Spectre.Console.Tests.Unit [Fact] public Task Should_Render_Grid() { - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var grid = new Grid(); grid.AddColumn(new GridColumn { NoWrap = true }); grid.AddColumn(new GridColumn { Padding = new Padding(2, 0, 0, 0) }); diff --git a/src/Spectre.Console.Tests/Unit/MarkupTests.cs b/src/Spectre.Console.Tests/Unit/MarkupTests.cs index 72f6c51..9b1016d 100644 --- a/src/Spectre.Console.Tests/Unit/MarkupTests.cs +++ b/src/Spectre.Console.Tests/Unit/MarkupTests.cs @@ -1,5 +1,6 @@ using System; using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -30,7 +31,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Throw_If_Closing_Tag_Is_Not_Properly_Escaped(string input) { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); // When var result = Record.Exception(() => new Markup(input)); @@ -45,7 +46,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Escape_Markup_Blocks_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var markup = new Markup("Hello [[ World ]] !"); // When @@ -61,7 +62,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Render_Links_As_Expected(string input, string output) { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var markup = new Markup(input); // When diff --git a/src/Spectre.Console.Tests/Unit/PadderTests.cs b/src/Spectre.Console.Tests/Unit/PadderTests.cs index a732f9c..3cbcb8f 100644 --- a/src/Spectre.Console.Tests/Unit/PadderTests.cs +++ b/src/Spectre.Console.Tests/Unit/PadderTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -11,7 +12,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Padded_Object_Correctly() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var table = new Table(); table.AddColumn("Foo"); table.AddColumn("Bar"); @@ -29,7 +30,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Expanded_Padded_Object_Correctly() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var table = new Table(); table.AddColumn("Foo"); table.AddColumn("Bar"); @@ -49,7 +50,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Padded_Object_Correctly_When_Nested_Within_Other_Object() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var table = new Table(); table.AddColumn("Foo"); table.AddColumn("Bar", c => c.PadLeft(0).PadRight(0)); diff --git a/src/Spectre.Console.Tests/Unit/PanelTests.cs b/src/Spectre.Console.Tests/Unit/PanelTests.cs index 361f9a9..b69a81b 100644 --- a/src/Spectre.Console.Tests/Unit/PanelTests.cs +++ b/src/Spectre.Console.Tests/Unit/PanelTests.cs @@ -1,6 +1,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -13,7 +14,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text("Hello World"))); @@ -26,7 +27,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Padding_Set_To_Zero() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text("Hello World")) @@ -42,7 +43,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Padding() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text("Hello World")) @@ -58,7 +59,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Header() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel("Hello World") @@ -76,7 +77,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Left_Aligned_Header() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel("Hello World") @@ -93,7 +94,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Centered_Header() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel("Hello World") @@ -110,7 +111,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Right_Aligned_Header() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel("Hello World") @@ -127,7 +128,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Collapse_Header_If_It_Will_Not_Fit() { // Given - var console = new PlainConsole(width: 10); + var console = new FakeConsole(width: 10); // When console.Render(new Panel("Hello World") @@ -144,7 +145,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Unicode_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text(" \n💩\n "))); @@ -157,7 +158,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_With_Multiple_Lines() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text("Hello World\nFoo Bar"))); @@ -170,7 +171,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Preserve_Explicit_Line_Ending() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var text = new Panel( new Markup("I heard [underline on blue]you[/] like 📦\n\n\n\nSo I put a 📦 in a 📦")); @@ -185,7 +186,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Expand_Panel_If_Enabled() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Text("Hello World")) @@ -201,7 +202,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Justify_Child_To_Right_Correctly() { // Given - var console = new PlainConsole(width: 25); + var console = new FakeConsole(width: 25); // When console.Render( @@ -218,7 +219,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Center_Child_Correctly() { // Given - var console = new PlainConsole(width: 25); + var console = new FakeConsole(width: 25); // When console.Render( @@ -235,7 +236,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Panel_Inside_Panel_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Panel(new Panel(new Text("Hello World")))); @@ -248,7 +249,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Wrap_Content_Correctly() { // Given - var console = new PlainConsole(width: 84); + var console = new FakeConsole(width: 84); var rows = new List(); var grid = new Grid(); grid.AddColumn(new GridColumn().PadLeft(2).PadRight(0)); @@ -272,7 +273,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Wrap_Table_With_CJK_Tables_In_Panel_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumn("测试"); diff --git a/src/Spectre.Console.Tests/Unit/ProgressTests.cs b/src/Spectre.Console.Tests/Unit/ProgressTests.cs index 00dd482..f673960 100644 --- a/src/Spectre.Console.Tests/Unit/ProgressTests.cs +++ b/src/Spectre.Console.Tests/Unit/ProgressTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Shouldly; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -12,7 +13,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Render_Task_Correctly() { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); + var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10); var progress = new Progress(console) .Columns(new[] { new ProgressBarColumn() }) @@ -37,7 +38,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Not_Auto_Clear_If_Specified() { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); + var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10); var progress = new Progress(console) .Columns(new[] { new ProgressBarColumn() }) @@ -62,7 +63,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Reduce_Width_If_Needed() { // Given - var console = new PlainConsole(width: 20); + var console = new FakeConsole(width: 20); var progress = new Progress(console) .Columns(new ProgressColumn[] @@ -93,7 +94,7 @@ namespace Spectre.Console.Tests.Unit { // Given var task = default(ProgressTask); - var console = new PlainConsole(); + var console = new FakeConsole(); var progress = new Progress(console) .Columns(new[] { new ProgressBarColumn() }) .AutoRefresh(false) diff --git a/src/Spectre.Console.Tests/Unit/PromptTests.cs b/src/Spectre.Console.Tests/Unit/PromptTests.cs index 15894a9..7f8496e 100644 --- a/src/Spectre.Console.Tests/Unit/PromptTests.cs +++ b/src/Spectre.Console.Tests/Unit/PromptTests.cs @@ -1,5 +1,6 @@ using System; using System.Threading.Tasks; +using Spectre.Console.Testing; using Shouldly; using VerifyXunit; using Xunit; @@ -13,7 +14,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Return_Validation_Error_If_Value_Cannot_Be_Converted() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushTextWithEnter("ninety-nine"); console.Input.PushTextWithEnter("99"); @@ -28,7 +29,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Chose_Default_Value_If_Nothing_Is_Entered() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushKey(ConsoleKey.Enter); // When @@ -46,7 +47,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Return_Error_If_An_Invalid_Choice_Is_Made() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushTextWithEnter("Apple"); console.Input.PushTextWithEnter("Banana"); @@ -65,7 +66,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Accept_Choice_In_List() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushTextWithEnter("Orange"); // When @@ -83,7 +84,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Auto_Complete_To_First_Choice_If_Pressing_Tab_On_Empty_String() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushKey(ConsoleKey.Tab); console.Input.PushKey(ConsoleKey.Enter); @@ -102,7 +103,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Auto_Complete_To_Best_Match() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushText("Band"); console.Input.PushKey(ConsoleKey.Tab); console.Input.PushKey(ConsoleKey.Enter); @@ -122,7 +123,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Auto_Complete_To_Next_Choice_When_Pressing_Tab_On_A_Match() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushText("Apple"); console.Input.PushKey(ConsoleKey.Tab); console.Input.PushKey(ConsoleKey.Enter); @@ -142,7 +143,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Return_Error_If_Custom_Validation_Fails() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushTextWithEnter("22"); console.Input.PushTextWithEnter("102"); console.Input.PushTextWithEnter("ABC"); @@ -174,7 +175,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Use_Custom_Converter() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Input.PushTextWithEnter("Banana"); // When diff --git a/src/Spectre.Console.Tests/Unit/RecorderTests.cs b/src/Spectre.Console.Tests/Unit/RecorderTests.cs index 4b4715c..622dbdf 100644 --- a/src/Spectre.Console.Tests/Unit/RecorderTests.cs +++ b/src/Spectre.Console.Tests/Unit/RecorderTests.cs @@ -1,4 +1,5 @@ using System.Threading.Tasks; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -11,7 +12,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Export_Text_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var recorder = new Recorder(console); recorder.Render(new Table() @@ -30,7 +31,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Export_Html_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var recorder = new Recorder(console); recorder.Render(new Table() diff --git a/src/Spectre.Console.Tests/Unit/RenderHookTests.cs b/src/Spectre.Console.Tests/Unit/RenderHookTests.cs index eeee8ec..5f6879d 100644 --- a/src/Spectre.Console.Tests/Unit/RenderHookTests.cs +++ b/src/Spectre.Console.Tests/Unit/RenderHookTests.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using Shouldly; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -20,7 +21,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Inject_Renderable_Before_Writing_To_Console() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); console.Pipeline.Attach(new HelloRenderHook()); // When diff --git a/src/Spectre.Console.Tests/Unit/RowsTests.cs b/src/Spectre.Console.Tests/Unit/RowsTests.cs index 61cdf41..ec8ec69 100644 --- a/src/Spectre.Console.Tests/Unit/RowsTests.cs +++ b/src/Spectre.Console.Tests/Unit/RowsTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -12,7 +13,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Rows() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var rows = new Rows( new IRenderable[] { @@ -34,7 +35,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Rows_Correctly_Inside_Other_Widget() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var table = new Table() .AddColumns("Foo", "Bar") .AddRow("HELLO WORLD") @@ -56,7 +57,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Rows_Correctly_Inside_Other_Widget_When_Expanded() { // Given - var console = new PlainConsole(width: 60); + var console = new FakeConsole(width: 60); var table = new Table() .AddColumns("Foo", "Bar") .AddRow("HELLO WORLD") diff --git a/src/Spectre.Console.Tests/Unit/RuleTests.cs b/src/Spectre.Console.Tests/Unit/RuleTests.cs index 1289a7a..1462bf4 100644 --- a/src/Spectre.Console.Tests/Unit/RuleTests.cs +++ b/src/Spectre.Console.Tests/Unit/RuleTests.cs @@ -1,5 +1,6 @@ using System.Threading.Tasks; using Shouldly; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -12,7 +13,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Default_Rule_Without_Title() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule()); @@ -25,7 +26,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Default_Rule_With_Specified_Box() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule().DoubleBorder()); @@ -38,7 +39,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_With_Specified_Box() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule("Hello World").DoubleBorder()); @@ -51,7 +52,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Default_Rule_With_Title_Centered_By_Default() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule("Hello World")); @@ -64,7 +65,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Default_Rule_With_Title_Left_Aligned() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule("Hello World") @@ -80,7 +81,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Default_Rule_With_Title_Right_Aligned() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule("Hello World") @@ -96,7 +97,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Convert_Line_Breaks_In_Title_To_Spaces() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule("Hello\nWorld\r\n!")); @@ -109,7 +110,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Truncate_Title() { // Given - var console = new PlainConsole(width: 40); + var console = new FakeConsole(width: 40); // When console.Render(new Rule(" Hello World ")); @@ -135,7 +136,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Truncate_Too_Long_Title(int width, string input, string expected) { // Given - var console = new PlainConsole(width); + var console = new FakeConsole(width); // When console.Render(new Rule(input)); diff --git a/src/Spectre.Console.Tests/Unit/StatusTests.cs b/src/Spectre.Console.Tests/Unit/StatusTests.cs index ed191ef..6ec3bd1 100644 --- a/src/Spectre.Console.Tests/Unit/StatusTests.cs +++ b/src/Spectre.Console.Tests/Unit/StatusTests.cs @@ -1,15 +1,16 @@ using Shouldly; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit { - public sealed partial class StatusTests + public sealed class StatusTests { [Fact] public void Should_Render_Status_Correctly() { // Given - var console = new TestableAnsiConsole(ColorSystem.TrueColor, width: 10); + var console = new FakeAnsiConsole(ColorSystem.TrueColor, width: 10); var status = new Status(console); status.AutoRefresh = false; diff --git a/src/Spectre.Console.Tests/Unit/TableBorderTests.cs b/src/Spectre.Console.Tests/Unit/TableBorderTests.cs index 9167067..9b9b3c7 100644 --- a/src/Spectre.Console.Tests/Unit/TableBorderTests.cs +++ b/src/Spectre.Console.Tests/Unit/TableBorderTests.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using Shouldly; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -39,7 +40,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().NoBorder(); // When @@ -80,7 +81,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().AsciiBorder(); // When @@ -121,7 +122,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().Ascii2Border(); // When @@ -162,7 +163,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().AsciiDoubleHeadBorder(); // When @@ -203,7 +204,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().SquareBorder(); // When @@ -244,7 +245,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().RoundedBorder(); // When @@ -285,7 +286,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().MinimalBorder(); // When @@ -326,7 +327,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().MinimalHeavyHeadBorder(); // When @@ -367,7 +368,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().MinimalDoubleHeadBorder(); // When @@ -408,7 +409,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().SimpleBorder(); // When @@ -449,7 +450,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().HorizontalBorder(); // When @@ -490,7 +491,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().SimpleHeavyBorder(); // When @@ -531,7 +532,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().HeavyBorder(); // When @@ -572,7 +573,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().HeavyEdgeBorder(); // When @@ -613,7 +614,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().HeavyHeadBorder(); // When @@ -654,7 +655,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().DoubleBorder(); // When @@ -695,7 +696,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().DoubleEdgeBorder(); // When @@ -736,7 +737,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable().MarkdownBorder(); // When @@ -750,7 +751,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Left_Aligned_Table_Columns_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable(header2: Justify.Left).MarkdownBorder(); // When @@ -764,7 +765,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Center_Aligned_Table_Columns_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable(header2: Justify.Center).MarkdownBorder(); // When @@ -778,7 +779,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Right_Aligned_Table_Columns_As_Expected() { // Given - var console = new PlainConsole(); + var console = new FakeConsole(); var table = Fixture.GetTable(header2: Justify.Right).MarkdownBorder(); // When diff --git a/src/Spectre.Console.Tests/Unit/TableTests.cs b/src/Spectre.Console.Tests/Unit/TableTests.cs index 69f372f..ca211dd 100644 --- a/src/Spectre.Console.Tests/Unit/TableTests.cs +++ b/src/Spectre.Console.Tests/Unit/TableTests.cs @@ -1,6 +1,7 @@ using System; using System.Threading.Tasks; using Shouldly; +using Spectre.Console.Testing; using VerifyXunit; using Xunit; @@ -127,7 +128,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); @@ -146,7 +147,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); @@ -163,7 +164,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_Footers_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumn(new TableColumn("Foo").Footer("Oof").RightAligned()); table.AddColumn("Bar"); @@ -182,7 +183,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Left_Align_Table_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.Alignment = Justify.Left; table.AddColumns("Foo", "Bar", "Baz"); @@ -200,7 +201,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Center_Table_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.Alignment = Justify.Center; table.AddColumns("Foo", "Bar", "Baz"); @@ -218,7 +219,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Right_Align_Table_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.Alignment = Justify.Right; table.AddColumns("Foo", "Bar", "Baz"); @@ -236,7 +237,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_Nested_In_Panels_Correctly() { // A simple table - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table() { Border = TableBorder.Rounded }; table.AddColumn("Foo"); table.AddColumn("Bar"); @@ -258,7 +259,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_Column_Justification_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumn(new TableColumn("Foo") { Alignment = Justify.Left }); table.AddColumn(new TableColumn("Bar") { Alignment = Justify.Right }); @@ -277,7 +278,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Expand_Table_To_Available_Space_If_Specified() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table() { Expand = true }; table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); @@ -294,7 +295,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_No_Border_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table { Border = TableBorder.None }; table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux", "Corgi", "Waldo"); @@ -311,7 +312,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_Multiple_Rows_In_Cell_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumns("Foo", "Bar", "Baz"); table.AddRow("Qux\nQuuux", "Corgi", "Waldo"); @@ -328,7 +329,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_Cell_Padding_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumns("Foo", "Bar"); table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 0, 2, 0) }); @@ -346,7 +347,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_Without_Rows() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table(); table.AddColumns("Foo", "Bar"); table.AddColumn(new TableColumn("Baz") { Padding = new Padding(3, 0, 2, 0) }); @@ -362,7 +363,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Not_Draw_Tables_That_Are_Impossible_To_Draw() { // Given - var console = new PlainConsole(width: 25); + var console = new FakeConsole(width: 25); var first = new Table().Border(TableBorder.Rounded).BorderColor(Color.Red); first.AddColumn(new TableColumn("[u]PS1[/]").Centered()); @@ -399,7 +400,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Render_Table_With_Title_And_Caption_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table { Border = TableBorder.Rounded }; table.Title = new TableTitle("Hello World"); table.Caption = new TableTitle("Goodbye World"); @@ -418,7 +419,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Left_Align_Table_With_Title_And_Caption_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table { Border = TableBorder.Rounded }; table.LeftAligned(); table.Title = new TableTitle("Hello World"); @@ -438,7 +439,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Center_Table_With_Title_And_Caption_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table { Border = TableBorder.Rounded }; table.Centered(); table.Title = new TableTitle("Hello World"); @@ -458,7 +459,7 @@ namespace Spectre.Console.Tests.Unit public Task Should_Right_Align_Table_With_Title_And_Caption_Correctly() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var table = new Table { Border = TableBorder.Rounded }; table.RightAligned(); table.Title = new TableTitle("Hello World"); diff --git a/src/Spectre.Console.Tests/Unit/TextTests.cs b/src/Spectre.Console.Tests/Unit/TextTests.cs index ad0184c..220eb01 100644 --- a/src/Spectre.Console.Tests/Unit/TextTests.cs +++ b/src/Spectre.Console.Tests/Unit/TextTests.cs @@ -1,6 +1,7 @@ using System.Text; using Shouldly; using Spectre.Console.Rendering; +using Spectre.Console.Testing; using Xunit; namespace Spectre.Console.Tests.Unit @@ -37,7 +38,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Render_Unstyled_Text_As_Expected() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); var text = new Text("Hello World"); // When @@ -53,7 +54,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Write_Line_Breaks(string input) { // Given - var console = new PlainConsole(width: 5); + var console = new FakeConsole(width: 5); var text = new Text(input); // When @@ -67,7 +68,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Render_Panel_2() { // Given - var console = new PlainConsole(width: 80); + var console = new FakeConsole(width: 80); // When console.Render(new Markup("[b]Hello World[/]\n[yellow]Hello World[/]")); @@ -85,7 +86,7 @@ namespace Spectre.Console.Tests.Unit int width, string input, string expected) { // Given - var console = new PlainConsole(width); + var console = new FakeConsole(width); var text = new Text(input); // When @@ -104,7 +105,7 @@ namespace Spectre.Console.Tests.Unit public void Should_Overflow_Text_Correctly(Overflow overflow, string expected) { // Given - var console = new PlainConsole(14); + var console = new FakeConsole(14); var text = new Text("foo pneumonoultramicroscopicsilicovolcanoconiosis bar qux") .Overflow(overflow); diff --git a/src/Spectre.Console.sln b/src/Spectre.Console.sln index fe3d4c8..c0cde8b 100644 --- a/src/Spectre.Console.sln +++ b/src/Spectre.Console.sln @@ -7,7 +7,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console", "Spectre. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Tests", "Spectre.Console.Tests\Spectre.Console.Tests.csproj", "{9F1AC4C1-766E-4421-8A78-B28F5BCDD94F}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Meta", "Meta", "{20595AD4-8D75-4AF8-B6BC-9C38C160423F}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props @@ -17,25 +17,25 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Examples", "Examples", "{F0575243-121F-4DEE-9F6B-246E26DC0844}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tables", "..\examples\Tables\Tables.csproj", "{94ECCBA8-7EBF-4B53-8379-52EB2327417E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tables", "..\examples\Console\Tables\Tables.csproj", "{94ECCBA8-7EBF-4B53-8379-52EB2327417E}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Panels", "..\examples\Panels\Panels.csproj", "{BFF37228-B376-4ADD-9657-4E501F929713}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Panels", "..\examples\Console\Panels\Panels.csproj", "{BFF37228-B376-4ADD-9657-4E501F929713}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grids", "..\examples\Grids\Grids.csproj", "{C7FF6FDB-FB59-4517-8669-521C96AB7323}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Grids", "..\examples\Console\Grids\Grids.csproj", "{C7FF6FDB-FB59-4517-8669-521C96AB7323}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors", "..\examples\Colors\Colors.csproj", "{1F51C55C-BA4C-4856-9001-0F7924FFB179}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Colors", "..\examples\Console\Colors\Colors.csproj", "{1F51C55C-BA4C-4856-9001-0F7924FFB179}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Columns", "..\examples\Columns\Columns.csproj", "{33357599-C79D-4299-888F-634E2C3EACEF}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Columns", "..\examples\Console\Columns\Columns.csproj", "{33357599-C79D-4299-888F-634E2C3EACEF}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Info", "..\examples\Info\Info.csproj", "{225CE0D4-06AB-411A-8D29-707504FE53B3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Info", "..\examples\Console\Info\Info.csproj", "{225CE0D4-06AB-411A-8D29-707504FE53B3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Borders", "..\examples\Borders\Borders.csproj", "{094245E6-4C94-485D-B5AC-3153E878B112}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Borders", "..\examples\Console\Borders\Borders.csproj", "{094245E6-4C94-485D-B5AC-3153E878B112}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Links", "..\examples\Links\Links.csproj", "{6AF8C93B-AA41-4F44-8B1B-B8D166576174}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Links", "..\examples\Console\Links\Links.csproj", "{6AF8C93B-AA41-4F44-8B1B-B8D166576174}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emojis", "..\examples\Emojis\Emojis.csproj", "{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Emojis", "..\examples\Console\Emojis\Emojis.csproj", "{1EABB956-957F-4C1A-8AC0-FD19C8F3C2F2}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptions", "..\examples\Exceptions\Exceptions.csproj", "{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Exceptions", "..\examples\Console\Exceptions\Exceptions.csproj", "{90C081A7-7C1D-4A4A-82B6-8FF473C3EA32}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB5C-1517-4C75-B59A-93D4E22BEC8D}" ProjectSection(SolutionItems) = preProject @@ -44,25 +44,39 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{C3E2CB ..\.github\workflows\publish.yaml = ..\.github\workflows\publish.yaml EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Calendars", "..\examples\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Calendars", "..\examples\Console\Calendars\Calendars.csproj", "{57691C7D-683D-46E6-AA4F-57A8C5F65D25}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules", "..\examples\Rules\Rules.csproj", "{8622A261-02C6-40CA-9797-E3F01ED87D6B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules", "..\examples\Console\Rules\Rules.csproj", "{8622A261-02C6-40CA-9797-E3F01ED87D6B}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cursor", "..\examples\Cursor\Cursor.csproj", "{75C608C3-ABB4-4168-A229-7F8250B946D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cursor", "..\examples\Console\Cursor\Cursor.csproj", "{75C608C3-ABB4-4168-A229-7F8250B946D1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Prompt\Prompt.csproj", "{6351C70F-F368-46DB-BAED-9B87CCD69353}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Prompt", "..\examples\Console\Prompt\Prompt.csproj", "{6351C70F-F368-46DB-BAED-9B87CCD69353}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Figlet", "..\examples\Console\Figlet\Figlet.csproj", "{45BF6302-6553-4E52-BF0F-B10D1AA9A6D1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Canvas\Canvas.csproj", "{5693761A-754A-40A8-9144-36510D6A4D69}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Canvas", "..\examples\Console\Canvas\Canvas.csproj", "{5693761A-754A-40A8-9144-36510D6A4D69}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.ImageSharp", "Spectre.Console.ImageSharp\Spectre.Console.ImageSharp.csproj", "{0EFE694D-0770-4E71-BF4E-EC2B41362F79}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Progress", "..\examples\Console\Progress\Progress.csproj", "{2B712A52-40F1-4C1C-833E-7C869ACA91F3}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Status", "..\examples\Status\Status.csproj", "{3716AFDF-0904-4635-8422-86E6B9356840}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Status", "..\examples\Console\Status\Status.csproj", "{3716AFDF-0904-4635-8422-86E6B9356840}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Charts", "..\examples\Charts\Charts.csproj", "{0A1AFD26-86A0-4060-B277-D380172C7070}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Charts", "..\examples\Console\Charts\Charts.csproj", "{0A1AFD26-86A0-4060-B277-D380172C7070}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Cli", "Cli", "{42792D7F-0BB6-4EE1-A314-8889305A4C48}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Delegates", "..\examples\Cli\Delegates\Delegates.csproj", "{2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Demo", "..\examples\Cli\Demo\Demo.csproj", "{5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dynamic", "..\examples\Cli\Dynamic\Dynamic.csproj", "{E9C02C5A-710C-4A57-A008-E3EAC89305CC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Injection", "..\examples\Cli\Injection\Injection.csproj", "{F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Spectre.Console.Testing", "Spectre.Console.Testing\Spectre.Console.Testing.csproj", "{7D5F6704-8249-46DD-906C-9E66419F215F}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{E0E45070-123C-4A4D-AA98-2A780308876C}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -338,6 +352,66 @@ Global {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x64.Build.0 = Release|Any CPU {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x86.ActiveCfg = Release|Any CPU {0A1AFD26-86A0-4060-B277-D380172C7070}.Release|x86.Build.0 = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|x64.ActiveCfg = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|x64.Build.0 = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|x86.ActiveCfg = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Debug|x86.Build.0 = Debug|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|Any CPU.Build.0 = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|x64.ActiveCfg = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|x64.Build.0 = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|x86.ActiveCfg = Release|Any CPU + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB}.Release|x86.Build.0 = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|x64.ActiveCfg = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|x64.Build.0 = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|x86.ActiveCfg = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Debug|x86.Build.0 = Debug|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|Any CPU.Build.0 = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|x64.ActiveCfg = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|x64.Build.0 = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|x86.ActiveCfg = Release|Any CPU + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C}.Release|x86.Build.0 = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|x64.ActiveCfg = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|x64.Build.0 = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|x86.ActiveCfg = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Debug|x86.Build.0 = Debug|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|Any CPU.Build.0 = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|x64.ActiveCfg = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|x64.Build.0 = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|x86.ActiveCfg = Release|Any CPU + {E9C02C5A-710C-4A57-A008-E3EAC89305CC}.Release|x86.Build.0 = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|x64.ActiveCfg = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|x64.Build.0 = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|x86.ActiveCfg = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Debug|x86.Build.0 = Debug|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|Any CPU.Build.0 = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|x64.ActiveCfg = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|x64.Build.0 = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|x86.ActiveCfg = Release|Any CPU + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12}.Release|x86.Build.0 = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x64.ActiveCfg = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x64.Build.0 = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x86.ActiveCfg = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Debug|x86.Build.0 = Debug|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|Any CPU.Build.0 = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x64.ActiveCfg = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x64.Build.0 = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x86.ActiveCfg = Release|Any CPU + {7D5F6704-8249-46DD-906C-9E66419F215F}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -360,9 +434,15 @@ Global {6351C70F-F368-46DB-BAED-9B87CCD69353} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {45BF6302-6553-4E52-BF0F-B10D1AA9A6D1} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {5693761A-754A-40A8-9144-36510D6A4D69} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {0EFE694D-0770-4E71-BF4E-EC2B41362F79} = {E0E45070-123C-4A4D-AA98-2A780308876C} {2B712A52-40F1-4C1C-833E-7C869ACA91F3} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {3716AFDF-0904-4635-8422-86E6B9356840} = {F0575243-121F-4DEE-9F6B-246E26DC0844} {0A1AFD26-86A0-4060-B277-D380172C7070} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {42792D7F-0BB6-4EE1-A314-8889305A4C48} = {F0575243-121F-4DEE-9F6B-246E26DC0844} + {2E8E045D-1D9F-43B5-BFBD-7073F60DCEBB} = {42792D7F-0BB6-4EE1-A314-8889305A4C48} + {5734CB0C-CF2A-44E1-B017-AEFA34A4C39C} = {42792D7F-0BB6-4EE1-A314-8889305A4C48} + {E9C02C5A-710C-4A57-A008-E3EAC89305CC} = {42792D7F-0BB6-4EE1-A314-8889305A4C48} + {F83CB4F1-95B8-45A4-A415-6DB5F8CA1E12} = {42792D7F-0BB6-4EE1-A314-8889305A4C48} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {5729B071-67A0-48FB-8B1B-275E6822086C} diff --git a/src/Spectre.Console/Cli/Annotations/CommandArgumentAttribute.cs b/src/Spectre.Console/Cli/Annotations/CommandArgumentAttribute.cs new file mode 100644 index 0000000..319ab3e --- /dev/null +++ b/src/Spectre.Console/Cli/Annotations/CommandArgumentAttribute.cs @@ -0,0 +1,54 @@ +using System; +using Spectre.Console.Cli.Internal; + +namespace Spectre.Console.Cli +{ + /// + /// An attribute representing a command argument. + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class CommandArgumentAttribute : Attribute + { + /// + /// Gets the argument position. + /// + /// The argument position. + public int Position { get; } + + /// + /// Gets the value name of the argument. + /// + /// The value name of the argument. + public string ValueName { get; } + + /// + /// Gets a value indicating whether the argument is required. + /// + /// + /// true if the argument is required; otherwise, false. + /// + public bool IsRequired { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The argument position. + /// The argument template. + public CommandArgumentAttribute(int position, string template) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + // Parse the option template. + var result = TemplateParser.ParseArgumentTemplate(template); + + // Assign the result. + Position = position; + ValueName = result.Value; + IsRequired = result.Required; + } + } +} diff --git a/src/Spectre.Console/Cli/Annotations/CommandOptionAttribute.cs b/src/Spectre.Console/Cli/Annotations/CommandOptionAttribute.cs new file mode 100644 index 0000000..348b03f --- /dev/null +++ b/src/Spectre.Console/Cli/Annotations/CommandOptionAttribute.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Spectre.Console.Cli.Internal; + +namespace Spectre.Console.Cli +{ + /// + /// An attribute representing a command option. + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class CommandOptionAttribute : Attribute + { + /// + /// Gets the long names of the option. + /// + /// The option's long names. + public IReadOnlyList LongNames { get; } + + /// + /// Gets the short names of the option. + /// + /// The option's short names. + public IReadOnlyList ShortNames { get; } + + /// + /// Gets the value name of the option. + /// + /// The option's value name. + public string? ValueName { get; } + + /// + /// Gets a value indicating whether the value is optional. + /// + public bool ValueIsOptional { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The option template. + public CommandOptionAttribute(string template) + { + if (template == null) + { + throw new ArgumentNullException(nameof(template)); + } + + // Parse the option template. + var result = TemplateParser.ParseOptionTemplate(template); + + // Assign the result. + LongNames = result.LongNames; + ShortNames = result.ShortNames; + ValueName = result.Value; + ValueIsOptional = result.ValueIsOptional; + } + + internal bool IsMatch(string name) + { + return + ShortNames.Contains(name, StringComparer.Ordinal) || + LongNames.Contains(name, StringComparer.Ordinal); + } + } +} diff --git a/src/Spectre.Console/Cli/Annotations/PairDeconstructorAttribute.cs b/src/Spectre.Console/Cli/Annotations/PairDeconstructorAttribute.cs new file mode 100644 index 0000000..e842fa6 --- /dev/null +++ b/src/Spectre.Console/Cli/Annotations/PairDeconstructorAttribute.cs @@ -0,0 +1,31 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Specifies what type to use as a pair deconstructor for + /// the property this attribute is bound to. + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = false)] + public sealed class PairDeconstructorAttribute : Attribute + { + /// + /// Gets the that represents the type of the + /// pair deconstructor class to use for data conversion for the + /// object this attribute is bound to. + /// + public Type Type { get; } + + /// + /// Initializes a new instance of the class. + /// + /// + /// A System.Type that represents the type of the pair deconstructor + /// class to use for data conversion for the object this attribute is bound to. + /// + public PairDeconstructorAttribute(Type type) + { + Type = type ?? throw new ArgumentNullException(nameof(type)); + } + } +} diff --git a/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs b/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs new file mode 100644 index 0000000..6aa4465 --- /dev/null +++ b/src/Spectre.Console/Cli/Annotations/ParameterValidationAttribute.cs @@ -0,0 +1,35 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// An base class attribute used for parameter validation. + /// + /// + [AttributeUsage(AttributeTargets.Property, AllowMultiple = true)] + public abstract class ParameterValidationAttribute : Attribute + { + /// + /// Gets the validation error message. + /// + /// The validation error message. + public string ErrorMessage { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The validation error message. + protected ParameterValidationAttribute(string errorMessage) + { + ErrorMessage = errorMessage; + } + + /// + /// Validates the parameter value. + /// + /// The parameter info. + /// The parameter value. + /// The validation result. + public abstract ValidationResult Validate(ICommandParameterInfo info, object? value); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/AsyncCommand.cs b/src/Spectre.Console/Cli/AsyncCommand.cs new file mode 100644 index 0000000..1124ee2 --- /dev/null +++ b/src/Spectre.Console/Cli/AsyncCommand.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Base class for an asynchronous command with no settings. + /// + public abstract class AsyncCommand : ICommand + { + /// + /// Executes the command. + /// + /// The command context. + /// An integer indicating whether or not the command executed successfully. + public abstract Task ExecuteAsync(CommandContext context); + + /// + Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + { + return ExecuteAsync(context); + } + + /// + Task ICommand.Execute(CommandContext context, CommandSettings settings) + { + return ExecuteAsync(context); + } + + /// + ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) + { + return ValidationResult.Success(); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/AsyncCommand`1.cs b/src/Spectre.Console/Cli/AsyncCommand`1.cs new file mode 100644 index 0000000..f4b8729 --- /dev/null +++ b/src/Spectre.Console/Cli/AsyncCommand`1.cs @@ -0,0 +1,51 @@ +using System.Diagnostics; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Base class for an asynchronous command. + /// + /// The settings type. + public abstract class AsyncCommand : ICommand + where TSettings : CommandSettings + { + /// + /// Validates the specified settings and remaining arguments. + /// + /// The command context. + /// The settings. + /// The validation result. + public virtual ValidationResult Validate(CommandContext context, TSettings settings) + { + return ValidationResult.Success(); + } + + /// + /// Executes the command. + /// + /// The command context. + /// The settings. + /// An integer indicating whether or not the command executed successfully. + public abstract Task ExecuteAsync(CommandContext context, TSettings settings); + + /// + ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) + { + return Validate(context, (TSettings)settings); + } + + /// + Task ICommand.Execute(CommandContext context, CommandSettings settings) + { + Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); + return ExecuteAsync(context, (TSettings)settings); + } + + /// + Task ICommand.Execute(CommandContext context, TSettings settings) + { + return ExecuteAsync(context, settings); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/CaseSensitivity.cs b/src/Spectre.Console/Cli/CaseSensitivity.cs new file mode 100644 index 0000000..c9331b6 --- /dev/null +++ b/src/Spectre.Console/Cli/CaseSensitivity.cs @@ -0,0 +1,33 @@ +using System; +using System.Diagnostics.CodeAnalysis; + +namespace Spectre.Console.Cli +{ + /// + /// Represents case sensitivity. + /// + [Flags] + [SuppressMessage("Naming", "CA1714:Flags enums should have plural names")] + public enum CaseSensitivity + { + /// + /// Nothing is case sensitive. + /// + None = 0, + + /// + /// Long options are case sensitive. + /// + LongOptions = 1, + + /// + /// Commands are case sensitive. + /// + Commands = 2, + + /// + /// Everything is case sensitive. + /// + All = LongOptions | Commands, + } +} diff --git a/src/Spectre.Console/Cli/Command.cs b/src/Spectre.Console/Cli/Command.cs new file mode 100644 index 0000000..b706d1c --- /dev/null +++ b/src/Spectre.Console/Cli/Command.cs @@ -0,0 +1,35 @@ +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Base class for a command without settings. + /// + public abstract class Command : ICommand + { + /// + /// Executes the command. + /// + /// The command context. + /// An integer indicating whether or not the command executed successfully. + public abstract int Execute(CommandContext context); + + /// + Task ICommand.Execute(CommandContext context, EmptyCommandSettings settings) + { + return Task.FromResult(Execute(context)); + } + + /// + Task ICommand.Execute(CommandContext context, CommandSettings settings) + { + return Task.FromResult(Execute(context)); + } + + /// + ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) + { + return ValidationResult.Success(); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandApp.cs b/src/Spectre.Console/Cli/CommandApp.cs new file mode 100644 index 0000000..4d051d7 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandApp.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Threading.Tasks; +using Spectre.Console.Cli.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// The entry point for a command line application. + /// + public sealed class CommandApp : ICommandApp + { + private readonly Configurator _configurator; + private readonly CommandExecutor _executor; + private bool _executed; + + /// + /// Initializes a new instance of the class. + /// + /// The registrar. + public CommandApp(ITypeRegistrar? registrar = null) + { + registrar ??= new DefaultTypeRegistrar(); + + _configurator = new Configurator(registrar); + _executor = new CommandExecutor(registrar); + } + + /// + /// Configures the command line application. + /// + /// The configuration. + public void Configure(Action configuration) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + configuration(_configurator); + } + + /// + /// Sets the default command. + /// + /// The command type. + public void SetDefaultCommand() + where TCommand : class, ICommand + { + GetConfigurator().SetDefaultCommand(); + } + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + public int Run(IEnumerable args) + { + return RunAsync(args).GetAwaiter().GetResult(); + } + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + public async Task RunAsync(IEnumerable args) + { + try + { + if (!_executed) + { + // Add built-in (hidden) commands. + _configurator.AddBranch(Constants.Commands.Branch, cli => + { + cli.HideBranch(); + cli.AddCommand(Constants.Commands.Version); + cli.AddCommand(Constants.Commands.XmlDoc); + }); + + _executed = true; + } + + return await _executor + .Execute(_configurator, args) + .ConfigureAwait(false); + } + catch (Exception ex) + { + // Render the exception. + var pretty = GetRenderableErrorMessage(ex); + if (pretty != null) + { + _configurator.Settings.Console.SafeRender(pretty); + } + + // Should we always propagate when debugging? + if (Debugger.IsAttached + && ex is CommandAppException appException + && appException.AlwaysPropagateWhenDebugging) + { + throw; + } + + if (_configurator.Settings.PropagateExceptions) + { + throw; + } + + return -1; + } + } + + internal Configurator GetConfigurator() + { + return _configurator; + } + + private static List? GetRenderableErrorMessage(Exception ex, bool convert = true) + { + if (ex is CommandAppException renderable && renderable.Pretty != null) + { + return new List { renderable.Pretty }; + } + + if (convert) + { + var converted = new List + { + new Composer() + .LineBreak() + .Text("[red]Error:[/]") + .Space().Text(ex.Message.EscapeMarkup()), + }; + + // Got a renderable inner exception? + if (ex.InnerException != null) + { + var innerRenderable = GetRenderableErrorMessage(ex.InnerException, convert: false); + if (innerRenderable != null) + { + converted.AddRange(innerRenderable); + } + } + + return converted; + } + + return null; + } + } +} diff --git a/src/Spectre.Console/Cli/CommandAppException.cs b/src/Spectre.Console/Cli/CommandAppException.cs new file mode 100644 index 0000000..a1fd8df --- /dev/null +++ b/src/Spectre.Console/Cli/CommandAppException.cs @@ -0,0 +1,27 @@ +using System; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// Represents errors that occur during application execution. + /// + public abstract class CommandAppException : Exception + { + internal IRenderable? Pretty { get; } + + internal virtual bool AlwaysPropagateWhenDebugging => false; + + internal CommandAppException(string message, IRenderable? pretty = null) + : base(message) + { + Pretty = pretty; + } + + internal CommandAppException(string message, Exception ex, IRenderable? pretty = null) + : base(message, ex) + { + Pretty = pretty; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/CommandApp`1.cs b/src/Spectre.Console/Cli/CommandApp`1.cs new file mode 100644 index 0000000..76dd347 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandApp`1.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// The entry point for a command line application with a default command. + /// + /// The type of the default command. + public sealed class CommandApp : ICommandApp + where TDefaultCommand : class, ICommand + { + private readonly CommandApp _app; + + /// + /// Initializes a new instance of the class. + /// + /// The registrar. + public CommandApp(ITypeRegistrar? registrar = null) + { + _app = new CommandApp(registrar); + _app.GetConfigurator().SetDefaultCommand(); + } + + /// + /// Configures the command line application. + /// + /// The configuration. + public void Configure(Action configuration) + { + _app.Configure(configuration); + } + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + public int Run(IEnumerable args) + { + return _app.Run(args); + } + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + public Task RunAsync(IEnumerable args) + { + return _app.RunAsync(args); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandConfigurationException.cs b/src/Spectre.Console/Cli/CommandConfigurationException.cs new file mode 100644 index 0000000..de24650 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandConfigurationException.cs @@ -0,0 +1,82 @@ +using System; +using System.Linq; +using Spectre.Console.Cli.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// Represents errors that occur during configuration. + /// + public class CommandConfigurationException : CommandAppException + { + internal override bool AlwaysPropagateWhenDebugging => true; + + internal CommandConfigurationException(string message, IRenderable? pretty = null) + : base(message, pretty) + { + } + + internal CommandConfigurationException(string message, Exception ex, IRenderable? pretty = null) + : base(message, ex, pretty) + { + } + + internal static CommandConfigurationException NoCommandConfigured() + { + return new CommandConfigurationException("No commands have been configured."); + } + + internal static CommandConfigurationException CommandNameConflict(CommandInfo command, string alias) + { + return new CommandConfigurationException($"The alias '{alias}' for '{command.Name}' conflicts with another command."); + } + + internal static CommandConfigurationException DuplicateOption(CommandInfo command, string[] options) + { + var keys = string.Join(", ", options.Select(x => x.Length > 1 ? $"--{x}" : $"-{x}")); + if (options.Length > 1) + { + return new CommandConfigurationException($"Options {keys} are duplicated in command '{command.Name}'."); + } + + return new CommandConfigurationException($"Option {keys} is duplicated in command '{command.Name}'."); + } + + internal static CommandConfigurationException BranchHasNoChildren(CommandInfo command) + { + throw new CommandConfigurationException($"The branch '{command.Name}' does not define any commands."); + } + + internal static CommandConfigurationException TooManyVectorArguments(CommandInfo command) + { + throw new CommandConfigurationException($"The command '{command.Name}' specifies more than one vector argument."); + } + + internal static CommandConfigurationException VectorArgumentNotSpecifiedLast(CommandInfo command) + { + throw new CommandConfigurationException($"The command '{command.Name}' specifies an argument vector that is not the last argument."); + } + + internal static CommandConfigurationException OptionalOptionValueMustBeFlagWithValue(CommandOption option) + { + return new CommandConfigurationException($"The option '{option.GetOptionName()}' has an optional value but does not implement IFlagValue."); + } + + internal static CommandConfigurationException OptionBothHasPairDeconstructorAndTypeParameter(CommandOption option) + { + return new CommandConfigurationException($"The option '{option.GetOptionName()}' is both marked as pair deconstructable and convertable."); + } + + internal static CommandConfigurationException OptionTypeDoesNotSupportDeconstruction(CommandOption option) + { + return new CommandConfigurationException($"The option '{option.GetOptionName()}' is marked as " + + "pair deconstructable, but the underlying type does not support that."); + } + + internal static CommandConfigurationException RequiredArgumentsCannotHaveDefaultValue(CommandArgument option) + { + return new CommandConfigurationException($"The required argument '{option.Value}' cannot have a default value."); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandContext.cs b/src/Spectre.Console/Cli/CommandContext.cs new file mode 100644 index 0000000..13bc96c --- /dev/null +++ b/src/Spectre.Console/Cli/CommandContext.cs @@ -0,0 +1,45 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command context. + /// + public sealed class CommandContext + { + /// + /// Gets the remaining arguments. + /// + /// + /// The remaining arguments. + /// + public IRemainingArguments Remaining { get; } + + /// + /// Gets the name of the command. + /// + /// + /// The name of the command. + /// + public string Name { get; } + + /// + /// Gets the data that was passed to the command during registration (if any). + /// + /// + /// The command data. + /// + public object? Data { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The remaining arguments. + /// The command name. + /// The command data. + public CommandContext(IRemainingArguments remaining, string name, object? data) + { + Remaining = remaining ?? throw new System.ArgumentNullException(nameof(remaining)); + Name = name ?? throw new System.ArgumentNullException(nameof(name)); + Data = data; + } + } +} diff --git a/src/Spectre.Console/Cli/CommandParseException.cs b/src/Spectre.Console/Cli/CommandParseException.cs new file mode 100644 index 0000000..638fbc2 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandParseException.cs @@ -0,0 +1,128 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Spectre.Console.Cli.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// Represents errors that occur during parsing. + /// + public sealed class CommandParseException : CommandRuntimeException + { + internal CommandParseException(string message, IRenderable? pretty = null) + : base(message, pretty) + { + } + + internal static CommandParseException CouldNotCreateSettings(Type settingsType) + { + return new CommandParseException($"Could not create settings of type '{settingsType.FullName}'."); + } + + internal static CommandParseException CouldNotCreateCommand(Type? commandType) + { + if (commandType == null) + { + return new CommandParseException($"Could not create command. Command type is unknown."); + } + + return new CommandParseException($"Could not create command of type '{commandType.FullName}'."); + } + + internal static CommandParseException ExpectedTokenButFoundNull(CommandTreeToken.Kind expected) + { + return new CommandParseException($"Expected to find any token of type '{expected}' but found null instead."); + } + + internal static CommandParseException ExpectedTokenButFoundOther(CommandTreeToken.Kind expected, CommandTreeToken.Kind found) + { + return new CommandParseException($"Expected to find token of type '{expected}' but found '{found}' instead."); + } + + internal static CommandParseException OptionHasNoName(string input, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(input, token, "Option does not have a name.", "Did you forget the option name?"); + } + + internal static CommandParseException OptionValueWasExpected(string input, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(input, token, "Expected an option value.", "Did you forget the option value?"); + } + + internal static CommandParseException OptionHasNoValue(IEnumerable args, CommandTreeToken token, CommandOption option) + { + return CommandLineParseExceptionFactory.Create(args, token, $"Option '{option.GetOptionName()}' is defined but no value has been provided.", "No value provided."); + } + + internal static CommandParseException UnexpectedOption(IEnumerable args, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(args, token, $"Unexpected option '{token.Value}'.", "Did you forget the command?"); + } + + internal static CommandParseException CannotAssignValueToFlag(IEnumerable args, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(args, token, "Flags cannot be assigned a value.", "Can't assign value."); + } + + internal static CommandParseException InvalidShortOptionName(string input, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(input, token, "Short option does not have a valid name.", "Not a valid name for a short option."); + } + + internal static CommandParseException LongOptionNameIsMissing(TextBuffer reader, int position) + { + var token = new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, string.Empty, "--"); + return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", "Did you forget the option name?"); + } + + internal static CommandParseException LongOptionNameIsOneCharacter(TextBuffer reader, int position, string name) + { + var token = new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, name, $"--{name}"); + var reason = $"Did you mean -{name}?"; + return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", reason); + } + + internal static CommandParseException LongOptionNameStartWithDigit(TextBuffer reader, int position, string name) + { + var token = new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, name, $"--{name}"); + return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", "Option names cannot start with a digit."); + } + + internal static CommandParseException LongOptionNameContainSymbol(TextBuffer reader, int position, char character) + { + var name = character.ToString(CultureInfo.InvariantCulture); + var token = new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, name, name); + return CommandLineParseExceptionFactory.Create(reader.Original, token, "Invalid long option name.", "Invalid character."); + } + + internal static CommandParseException UnterminatedQuote(string input, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(input, token, $"Encountered unterminated quoted string '{token.Value}'.", "Did you forget the closing quotation mark?"); + } + + internal static CommandParseException UnknownCommand(CommandModel model, CommandTree? node, IEnumerable args, CommandTreeToken token) + { + var suggestion = CommandSuggestor.Suggest(model, node?.Command, token.Value); + var text = suggestion != null ? $"Did you mean '{suggestion.Name}'?" : "No such command."; + return CommandLineParseExceptionFactory.Create(args, token, $"Unknown command '{token.Value}'.", text); + } + + internal static CommandParseException CouldNotMatchArgument(IEnumerable args, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(args, token, $"Could not match '{token.Value}' with an argument.", "Could not match to argument."); + } + + internal static CommandParseException UnknownOption(IEnumerable args, CommandTreeToken token) + { + return CommandLineParseExceptionFactory.Create(args, token, $"Unknown option '{token.Value}'.", "Unknown option."); + } + + internal static CommandParseException ValueIsNotInValidFormat(string value) + { + var text = $"[red]Error:[/] The value '[white]{value}[/]' is not in a correct format"; + return new CommandParseException("Could not parse value", new Markup(text)); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandRuntimeException.cs b/src/Spectre.Console/Cli/CommandRuntimeException.cs new file mode 100644 index 0000000..d45f764 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandRuntimeException.cs @@ -0,0 +1,59 @@ +using System; +using Spectre.Console.Cli.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// Represents errors that occur during runtime. + /// + public class CommandRuntimeException : CommandAppException + { + internal CommandRuntimeException(string message, IRenderable? pretty = null) + : base(message, pretty) + { + } + + internal CommandRuntimeException(string message, Exception ex, IRenderable? pretty = null) + : base(message, ex, pretty) + { + } + + internal static CommandRuntimeException CouldNotResolveType(Type type, Exception? ex = null) + { + var message = $"Could not resolve type '{type.FullName}'."; + if (ex != null) + { + // TODO: Show internal stuff here. + return new CommandRuntimeException(message, ex); + } + + return new CommandRuntimeException(message); + } + + internal static CommandRuntimeException MissingRequiredArgument(CommandTree node, CommandArgument argument) + { + if (node.Command.Name == Constants.DefaultCommandName) + { + return new CommandRuntimeException($"Missing required argument '{argument.Value}'."); + } + + return new CommandRuntimeException($"Command '{node.Command.Name}' is missing required argument '{argument.Value}'."); + } + + internal static CommandRuntimeException NoConverterFound(CommandParameter parameter) + { + return new CommandRuntimeException($"Could not find converter for type '{parameter.ParameterType.FullName}'."); + } + + internal static CommandRuntimeException ValidationFailed(ValidationResult result) + { + return new CommandRuntimeException(result.Message ?? "Unknown validation error."); + } + + internal static Exception CouldNotGetSettingsType(Type commandType) + { + return new CommandRuntimeException($"Could not get settings type for command of type '{commandType.FullName}'."); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandSettings.cs b/src/Spectre.Console/Cli/CommandSettings.cs new file mode 100644 index 0000000..131b682 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandSettings.cs @@ -0,0 +1,17 @@ +namespace Spectre.Console.Cli +{ + /// + /// Base class for command settings. + /// + public abstract class CommandSettings + { + /// + /// Performs validation of the settings. + /// + /// The validation result. + public virtual ValidationResult Validate() + { + return ValidationResult.Success(); + } + } +} diff --git a/src/Spectre.Console/Cli/CommandTemplateException.cs b/src/Spectre.Console/Cli/CommandTemplateException.cs new file mode 100644 index 0000000..4e89e55 --- /dev/null +++ b/src/Spectre.Console/Cli/CommandTemplateException.cs @@ -0,0 +1,161 @@ +using System.Globalization; +using Spectre.Console.Cli.Internal; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli +{ + /// + /// Represents errors related to parameter templates. + /// + public sealed class CommandTemplateException : CommandConfigurationException + { + /// + /// Gets the template that contains the error. + /// + public string Template { get; } + + internal override bool AlwaysPropagateWhenDebugging => true; + + internal CommandTemplateException(string message, string template, IRenderable pretty) + : base(message, pretty) + { + Template = template; + } + + internal static CommandTemplateException UnexpectedCharacter(string template, int position, char character) + { + return CommandLineTemplateExceptionFactory.Create( + template, + new TemplateToken(TemplateToken.Kind.Unknown, position, $"{character}", $"{character}"), + $"Encountered unexpected character '{character}'.", + "Unexpected character."); + } + + internal static CommandTemplateException UnterminatedValueName(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create( + template, token, + $"Encountered unterminated value name '{token.Value}'.", + "Unterminated value name."); + } + + internal static CommandTemplateException ArgumentCannotContainOptions(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create( + template, token, + "Arguments can not contain options.", + "Not permitted."); + } + + internal static CommandTemplateException MultipleValuesAreNotSupported(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "Multiple values are not supported.", + "Too many values."); + } + + internal static CommandTemplateException ValuesMustHaveName(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "Values without name are not allowed.", + "Missing value name."); + } + + internal static CommandTemplateException OptionsMustHaveName(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "Options without name are not allowed.", + "Missing option name."); + } + + internal static CommandTemplateException OptionNamesCannotStartWithDigit(string template, TemplateToken token) + { + // Rewrite the token to point to the option name instead of the whole string. + token = new TemplateToken( + token.TokenKind, + token.TokenKind == TemplateToken.Kind.ShortName ? token.Position + 1 : token.Position + 2, + token.Value, token.Value); + + return CommandLineTemplateExceptionFactory.Create(template, token, + "Option names cannot start with a digit.", + "Invalid option name."); + } + + internal static CommandTemplateException InvalidCharacterInOptionName(string template, TemplateToken token, char character) + { + // Rewrite the token to point to the invalid character instead of the whole value. + var position = (token.TokenKind == TemplateToken.Kind.ShortName + ? token.Position + 1 + : token.Position + 2) + token.Value.OrdinalIndexOf(character); + + token = new TemplateToken( + token.TokenKind, position, + token.Value, character.ToString(CultureInfo.InvariantCulture)); + + return CommandLineTemplateExceptionFactory.Create(template, token, + $"Encountered invalid character '{character}' in option name.", + "Invalid character."); + } + + internal static CommandTemplateException LongOptionMustHaveMoreThanOneCharacter(string template, TemplateToken token) + { + // Rewrite the token to point to the option name instead of the whole option. + token = new TemplateToken(token.TokenKind, token.Position + 2, token.Value, token.Value); + + return CommandLineTemplateExceptionFactory.Create(template, token, + "Long option names must consist of more than one character.", + "Invalid option name."); + } + + internal static CommandTemplateException MultipleShortOptionNamesNotAllowed(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "Multiple short option names are not supported.", + "Too many short options."); + } + + internal static CommandTemplateException ShortOptionMustOnlyBeOneCharacter(string template, TemplateToken token) + { + // Rewrite the token to point to the option name instead of the whole option. + token = new TemplateToken(token.TokenKind, token.Position + 1, token.Value, token.Value); + + return CommandLineTemplateExceptionFactory.Create(template, token, + "Short option names can not be longer than one character.", + "Invalid option name."); + } + + internal static CommandTemplateException MultipleOptionValuesAreNotSupported(string template, TemplateToken token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "Multiple option values are not supported.", + "Too many option values."); + } + + internal static CommandTemplateException InvalidCharacterInValueName(string template, TemplateToken token, char character) + { + // Rewrite the token to point to the invalid character instead of the whole value. + token = new TemplateToken( + token.TokenKind, + token.Position + 1 + token.Value.OrdinalIndexOf(character), + token.Value, character.ToString(CultureInfo.InvariantCulture)); + + return CommandLineTemplateExceptionFactory.Create(template, token, + $"Encountered invalid character '{character}' in value name.", + "Invalid character."); + } + + internal static CommandTemplateException MissingLongAndShortName(string template, TemplateToken? token) + { + return CommandLineTemplateExceptionFactory.Create(template, token, + "No long or short name for option has been specified.", + "Missing option. Was this meant to be an argument?"); + } + + internal static CommandTemplateException ArgumentsMustHaveValueName(string template) + { + return CommandLineTemplateExceptionFactory.Create(template, null, + "Arguments must have a value name.", + "Missing value name."); + } + } +} diff --git a/src/Spectre.Console/Cli/Command`1.cs b/src/Spectre.Console/Cli/Command`1.cs new file mode 100644 index 0000000..c8928eb --- /dev/null +++ b/src/Spectre.Console/Cli/Command`1.cs @@ -0,0 +1,52 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Base class for a command. + /// + /// The settings type. + public abstract class Command : ICommand + where TSettings : CommandSettings + { + /// + /// Validates the specified settings and remaining arguments. + /// + /// The command context. + /// The settings. + /// The validation result. + public virtual ValidationResult Validate([NotNull] CommandContext context, [NotNull] TSettings settings) + { + return ValidationResult.Success(); + } + + /// + /// Executes the command. + /// + /// The command context. + /// The settings. + /// An integer indicating whether or not the command executed successfully. + public abstract int Execute([NotNull] CommandContext context, [NotNull] TSettings settings); + + /// + ValidationResult ICommand.Validate(CommandContext context, CommandSettings settings) + { + return Validate(context, (TSettings)settings); + } + + /// + Task ICommand.Execute(CommandContext context, CommandSettings settings) + { + Debug.Assert(settings is TSettings, "Command settings is of unexpected type."); + return Task.FromResult(Execute(context, (TSettings)settings)); + } + + /// + Task ICommand.Execute(CommandContext context, TSettings settings) + { + return Task.FromResult(Execute(context, settings)); + } + } +} diff --git a/src/Spectre.Console/Cli/ConfiguratorExtensions.cs b/src/Spectre.Console/Cli/ConfiguratorExtensions.cs new file mode 100644 index 0000000..24a5b09 --- /dev/null +++ b/src/Spectre.Console/Cli/ConfiguratorExtensions.cs @@ -0,0 +1,211 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Contains extensions for + /// and . + /// + public static class ConfiguratorExtensions + { + /// + /// Sets the name of the application. + /// + /// The configurator. + /// The name of the application. + /// A configurator that can be used to configure the application further. + public static IConfigurator SetApplicationName(this IConfigurator configurator, string name) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.ApplicationName = name; + return configurator; + } + + /// + /// Configures the console. + /// + /// The configurator. + /// The console. + /// A configurator that can be used to configure the application further. + public static IConfigurator ConfigureConsole(this IConfigurator configurator, IAnsiConsole console) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.Console = console; + return configurator; + } + + /// + /// Sets the parsing mode to strict. + /// + /// The configurator. + /// A configurator that can be used to configure the application further. + public static IConfigurator UseStrictParsing(this IConfigurator configurator) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.StrictParsing = true; + return configurator; + } + + /// + /// Tells the command line application to propagate all + /// exceptions to the user. + /// + /// The configurator. + /// A configurator that can be used to configure the application further. + public static IConfigurator PropagateExceptions(this IConfigurator configurator) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.PropagateExceptions = true; + return configurator; + } + + /// + /// Configures case sensitivity. + /// + /// The configuration. + /// The case sensitivity. + /// A configurator that can be used to configure the application further. + public static IConfigurator CaseSensitivity(this IConfigurator configurator, CaseSensitivity sensitivity) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.CaseSensitivity = sensitivity; + return configurator; + } + + /// + /// Tells the command line application to validate all + /// examples before running the application. + /// + /// The configurator. + /// A configurator that can be used to configure the application further. + public static IConfigurator ValidateExamples(this IConfigurator configurator) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.ValidateExamples = true; + return configurator; + } + + /// + /// Sets the command interceptor to be used. + /// + /// The configurator. + /// A . + /// A configurator that can be used to configure the application further. + public static IConfigurator SetInterceptor(this IConfigurator configurator, ICommandInterceptor interceptor) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.Settings.Interceptor = interceptor; + return configurator; + } + + /// + /// Adds a command branch. + /// + /// The configurator. + /// The name of the command branch. + /// The command branch configuration. + public static void AddBranch( + this IConfigurator configurator, + string name, + Action> action) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.AddBranch(name, action); + } + + /// + /// Adds a command branch. + /// + /// The command setting type. + /// The configurator. + /// The name of the command branch. + /// The command branch configuration. + public static void AddBranch( + this IConfigurator configurator, + string name, + Action> action) + where TSettings : CommandSettings + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + configurator.AddBranch(name, action); + } + + /// + /// Adds a command without settings that executes a delegate. + /// + /// The configurator. + /// The name of the command. + /// The delegate to execute as part of command execution. + /// A command configurator that can be used to configure the command further. + public static ICommandConfigurator AddDelegate( + this IConfigurator configurator, + string name, + Func func) + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + return configurator.AddDelegate(name, (c, _) => func(c)); + } + + /// + /// Adds a command without settings that executes a delegate. + /// + /// The command setting type. + /// The configurator. + /// The name of the command. + /// The delegate to execute as part of command execution. + /// A command configurator that can be used to configure the command further. + public static ICommandConfigurator AddDelegate( + this IConfigurator configurator, + string name, + Func func) + where TSettings : CommandSettings + { + if (configurator == null) + { + throw new ArgumentNullException(nameof(configurator)); + } + + return configurator.AddDelegate(name, (c, _) => func(c)); + } + } +} diff --git a/src/Spectre.Console/Cli/EmptyCommandSettings.cs b/src/Spectre.Console/Cli/EmptyCommandSettings.cs new file mode 100644 index 0000000..8e1cb4b --- /dev/null +++ b/src/Spectre.Console/Cli/EmptyCommandSettings.cs @@ -0,0 +1,9 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents empty settings. + /// + public sealed class EmptyCommandSettings : CommandSettings + { + } +} diff --git a/src/Spectre.Console/Cli/FlagValue.cs b/src/Spectre.Console/Cli/FlagValue.cs new file mode 100644 index 0000000..a1989bc --- /dev/null +++ b/src/Spectre.Console/Cli/FlagValue.cs @@ -0,0 +1,57 @@ +using System; +using System.Globalization; + +namespace Spectre.Console.Cli +{ + /// + /// Implementation of a flag with an optional value. + /// + /// The flag's element type. + public sealed class FlagValue : IFlagValue + { + /// + /// Gets or sets a value indicating whether or not the flag was set or not. + /// + public bool IsSet { get; set; } + +#pragma warning disable CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + /// + /// Gets or sets the flag's value. + /// + public T Value { get; set; } +#pragma warning restore CS8618 // Non-nullable field is uninitialized. Consider declaring as nullable. + + /// + Type IFlagValue.Type => typeof(T); + + /// + object? IFlagValue.Value + { + get => Value; + set + { +#pragma warning disable CS8601 // Possible null reference assignment. + Value = (T)value; +#pragma warning restore CS8601 // Possible null reference assignment. + } + } + + /// + public override string ToString() + { + var flag = (IFlagValue)this; + if (flag.Value != null) + { + return string.Format( + CultureInfo.InvariantCulture, + "Set={0}, Value={1}", + IsSet, + flag.Value.ToString()); + } + + return string.Format( + CultureInfo.InvariantCulture, + "Set={0}", IsSet); + } + } +} diff --git a/src/Spectre.Console/Cli/ICommand.cs b/src/Spectre.Console/Cli/ICommand.cs new file mode 100644 index 0000000..b494ab2 --- /dev/null +++ b/src/Spectre.Console/Cli/ICommand.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a command. + /// + public interface ICommand + { + /// + /// Validates the specified settings and remaining arguments. + /// + /// The command context. + /// The settings. + /// The validation result. + ValidationResult Validate(CommandContext context, CommandSettings settings); + + /// + /// Executes the command. + /// + /// The command context. + /// The settings. + /// The validation result. + Task Execute(CommandContext context, CommandSettings settings); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/ICommandApp.cs b/src/Spectre.Console/Cli/ICommandApp.cs new file mode 100644 index 0000000..faed0cd --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandApp.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a command line application. + /// + public interface ICommandApp + { + /// + /// Configures the command line application. + /// + /// The configuration. + void Configure(Action configuration); + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + int Run(IEnumerable args); + + /// + /// Runs the command line application with specified arguments. + /// + /// The arguments. + /// The exit code from the executed command. + Task RunAsync(IEnumerable args); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/ICommandAppSettings.cs b/src/Spectre.Console/Cli/ICommandAppSettings.cs new file mode 100644 index 0000000..d355d43 --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandAppSettings.cs @@ -0,0 +1,49 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command line application settings. + /// + public interface ICommandAppSettings + { + /// + /// Gets or sets the application name. + /// + string? ApplicationName { get; set; } + + /// + /// Gets or sets the . + /// + IAnsiConsole? Console { get; set; } + + /// + /// Gets or sets the used + /// to intercept settings before it's being sent to the command. + /// + ICommandInterceptor? Interceptor { get; set; } + + /// + /// Gets the type registrar. + /// + ITypeRegistrarFrontend Registrar { get; } + + /// + /// Gets or sets case sensitivity. + /// + CaseSensitivity CaseSensitivity { get; set; } + + /// + /// Gets or sets a value indicating whether or not parsing is strict. + /// + bool StrictParsing { get; set; } + + /// + /// Gets or sets a value indicating whether or not exceptions should be propagated. + /// + bool PropagateExceptions { get; set; } + + /// + /// Gets or sets a value indicating whether or not examples should be validated. + /// + bool ValidateExamples { get; set; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/ICommandConfigurator.cs b/src/Spectre.Console/Cli/ICommandConfigurator.cs new file mode 100644 index 0000000..22dcccd --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandConfigurator.cs @@ -0,0 +1,44 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command configurator. + /// + public interface ICommandConfigurator + { + /// + /// Adds an example of how to use the command. + /// + /// The example arguments. + /// The same instance so that multiple calls can be chained. + ICommandConfigurator WithExample(string[] args); + + /// + /// Adds an alias (an alternative name) to the command being configured. + /// + /// The alias to add to the command being configured. + /// The same instance so that multiple calls can be chained. + ICommandConfigurator WithAlias(string name); + + /// + /// Sets the description of the command. + /// + /// The command description. + /// The same instance so that multiple calls can be chained. + ICommandConfigurator WithDescription(string description); + + /// + /// Sets data that will be passed to the command via the . + /// + /// The data to pass to the command. + /// The same instance so that multiple calls can be chained. + ICommandConfigurator WithData(object data); + + /// + /// Marks the command as hidden. + /// Hidden commands do not show up in help documentation or + /// generated XML models. + /// + /// The same instance so that multiple calls can be chained. + ICommandConfigurator IsHidden(); + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/ICommandInterceptor.cs b/src/Spectre.Console/Cli/ICommandInterceptor.cs new file mode 100644 index 0000000..3b1888f --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandInterceptor.cs @@ -0,0 +1,17 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command settings interceptor that + /// will intercept command settings before it's + /// passed to a command. + /// + public interface ICommandInterceptor + { + /// + /// Intercepts command information before it's passed to a command. + /// + /// The intercepted . + /// The intercepted . + void Intercept(CommandContext context, CommandSettings settings); + } +} diff --git a/src/Spectre.Console/Cli/ICommandLimiter`1.cs b/src/Spectre.Console/Cli/ICommandLimiter`1.cs new file mode 100644 index 0000000..45ad077 --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandLimiter`1.cs @@ -0,0 +1,12 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command limiter. + /// + /// The type of the settings to limit to. + /// + public interface ICommandLimiter : ICommand + where TSettings : CommandSettings + { + } +} diff --git a/src/Spectre.Console/Cli/ICommandParameterInfo.cs b/src/Spectre.Console/Cli/ICommandParameterInfo.cs new file mode 100644 index 0000000..b8e3def --- /dev/null +++ b/src/Spectre.Console/Cli/ICommandParameterInfo.cs @@ -0,0 +1,20 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a command parameter. + /// + public interface ICommandParameterInfo + { + /// + /// Gets the property name. + /// + /// The property name. + public abstract string PropertyName { get; } + + /// + /// Gets the description. + /// + /// The description. + public abstract string? Description { get; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/ICommand`1.cs b/src/Spectre.Console/Cli/ICommand`1.cs new file mode 100644 index 0000000..a6878b3 --- /dev/null +++ b/src/Spectre.Console/Cli/ICommand`1.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a command. + /// + /// The settings type. + public interface ICommand : ICommandLimiter + where TSettings : CommandSettings + { + /// + /// Executes the command. + /// + /// The command context. + /// The settings. + /// An integer indicating whether or not the command executed successfully. + Task Execute(CommandContext context, TSettings settings); + } +} diff --git a/src/Spectre.Console/Cli/IConfigurator.cs b/src/Spectre.Console/Cli/IConfigurator.cs new file mode 100644 index 0000000..270c54c --- /dev/null +++ b/src/Spectre.Console/Cli/IConfigurator.cs @@ -0,0 +1,49 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a configurator. + /// + public interface IConfigurator + { + /// + /// Gets the command app settings. + /// + public ICommandAppSettings Settings { get; } + + /// + /// Adds an example of how to use the application. + /// + /// The example arguments. + void AddExample(string[] args); + + /// + /// Adds a command. + /// + /// The command type. + /// The name of the command. + /// A command configurator that can be used to configure the command further. + ICommandConfigurator AddCommand(string name) + where TCommand : class, ICommand; + + /// + /// Adds a command that executes a delegate. + /// + /// The command setting type. + /// The name of the command. + /// The delegate to execute as part of command execution. + /// A command configurator that can be used to configure the command further. + ICommandConfigurator AddDelegate(string name, Func func) + where TSettings : CommandSettings; + + /// + /// Adds a command branch. + /// + /// The command setting type. + /// The name of the command branch. + /// The command branch configurator. + void AddBranch(string name, Action> action) + where TSettings : CommandSettings; + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/IConfigurator`1.cs b/src/Spectre.Console/Cli/IConfigurator`1.cs new file mode 100644 index 0000000..198b575 --- /dev/null +++ b/src/Spectre.Console/Cli/IConfigurator`1.cs @@ -0,0 +1,59 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a configurator for specific settings. + /// + /// The command setting type. + public interface IConfigurator + where TSettings : CommandSettings + { + /// + /// Sets the description of the branch. + /// + /// The description of the branch. + void SetDescription(string description); + + /// + /// Adds an example of how to use the branch. + /// + /// The example arguments. + void AddExample(string[] args); + + /// + /// Marks the branch as hidden. + /// Hidden branches do not show up in help documentation or + /// generated XML models. + /// + void HideBranch(); + + /// + /// Adds a command. + /// + /// The command type. + /// The name of the command. + /// A command configurator that can be used to configure the command further. + ICommandConfigurator AddCommand(string name) + where TCommand : class, ICommandLimiter; + + /// + /// Adds a command that executes a delegate. + /// + /// The derived command setting type. + /// The name of the command. + /// The delegate to execute as part of command execution. + /// A command configurator that can be used to configure the command further. + ICommandConfigurator AddDelegate(string name, Func func) + where TDerivedSettings : TSettings; + + /// + /// Adds a command branch. + /// + /// The derived command setting type. + /// The name of the command branch. + /// The command branch configuration. + void AddBranch(string name, Action> action) + where TDerivedSettings : TSettings; + } +} diff --git a/src/Spectre.Console/Cli/IFlagValue.cs b/src/Spectre.Console/Cli/IFlagValue.cs new file mode 100644 index 0000000..a0c56e8 --- /dev/null +++ b/src/Spectre.Console/Cli/IFlagValue.cs @@ -0,0 +1,25 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a flag with an optional value. + /// + public interface IFlagValue + { + /// + /// Gets or sets a value indicating whether or not the flag was set or not. + /// + bool IsSet { get; set; } + + /// + /// Gets the flag's element type. + /// + Type Type { get; } + + /// + /// Gets or sets the flag's value. + /// + object? Value { get; set; } + } +} diff --git a/src/Spectre.Console/Cli/IRemainingArguments.cs b/src/Spectre.Console/Cli/IRemainingArguments.cs new file mode 100644 index 0000000..c13716a --- /dev/null +++ b/src/Spectre.Console/Cli/IRemainingArguments.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli +{ + /// + /// Represents the remaining arguments. + /// + public interface IRemainingArguments + { + /// + /// Gets the parsed remaining arguments. + /// + ILookup Parsed { get; } + + /// + /// Gets the raw, non-parsed remaining arguments. + /// + IReadOnlyList Raw { get; } + } +} diff --git a/src/Spectre.Console/Cli/ITypeRegistrar.cs b/src/Spectre.Console/Cli/ITypeRegistrar.cs new file mode 100644 index 0000000..0881027 --- /dev/null +++ b/src/Spectre.Console/Cli/ITypeRegistrar.cs @@ -0,0 +1,31 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a type registrar. + /// + public interface ITypeRegistrar + { + /// + /// Registers the specified service. + /// + /// The service. + /// The implementation. + void Register(Type service, Type implementation); + + /// + /// Registers the specified instance. + /// + /// The service. + /// The implementation. + void RegisterInstance(Type service, object implementation); + + /// + /// Builds the type resolver representing the registrations + /// specified in the current instance. + /// + /// A type resolver. + ITypeResolver Build(); + } +} diff --git a/src/Spectre.Console/Cli/ITypeRegistrarFrontend.cs b/src/Spectre.Console/Cli/ITypeRegistrarFrontend.cs new file mode 100644 index 0000000..41c8b71 --- /dev/null +++ b/src/Spectre.Console/Cli/ITypeRegistrarFrontend.cs @@ -0,0 +1,32 @@ +namespace Spectre.Console.Cli +{ + /// + /// Represents a user friendly frontend for a . + /// + public interface ITypeRegistrarFrontend + { + /// + /// Registers the type with the type registrar as a singleton. + /// + /// The exposed service type. + /// The implementing type. + void Register() + where TImplementation : TService; + + /// + /// Registers the specified instance with the type registrar as a singleton. + /// + /// The type of the instance. + /// The instance to register. + void RegisterInstance(TImplementation instance); + + /// + /// Registers the specified instance with the type registrar as a singleton. + /// + /// The exposed service type. + /// implementing type. + /// The instance to register. + void RegisterInstance(TImplementation instance) + where TImplementation : TService; + } +} diff --git a/src/Spectre.Console/Cli/ITypeResolver.cs b/src/Spectre.Console/Cli/ITypeResolver.cs new file mode 100644 index 0000000..7f30d10 --- /dev/null +++ b/src/Spectre.Console/Cli/ITypeResolver.cs @@ -0,0 +1,17 @@ +using System; + +namespace Spectre.Console.Cli +{ + /// + /// Represents a type resolver. + /// + public interface ITypeResolver + { + /// + /// Resolves an instance of the specified type. + /// + /// The type to resolve. + /// An instance of the specified type. + object? Resolve(Type? type); + } +} diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandConstructorBinder.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandConstructorBinder.cs new file mode 100644 index 0000000..c32d2c7 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandConstructorBinder.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandConstructorBinder + { + public static CommandSettings CreateSettings(CommandValueLookup lookup, ConstructorInfo constructor, ITypeResolver resolver) + { + if (constructor.DeclaringType == null) + { + throw new InvalidOperationException("Cannot create settings since constructor have no declaring type."); + } + + var parameters = new List(); + var mapped = new HashSet(); + foreach (var parameter in constructor.GetParameters()) + { + if (lookup.TryGetParameterWithName(parameter.Name, out var result)) + { + parameters.Add(result.Value); + mapped.Add(result.Parameter.Id); + } + else + { + var value = resolver.Resolve(parameter.ParameterType); + if (value == null) + { + throw CommandRuntimeException.CouldNotResolveType(parameter.ParameterType); + } + + parameters.Add(value); + } + } + + // Create the settings. + if (!(Activator.CreateInstance(constructor.DeclaringType, parameters.ToArray()) is CommandSettings settings)) + { + throw new InvalidOperationException("Could not create settings"); + } + + // Try to do property injection for parameters that wasn't injected. + foreach (var (parameter, value) in lookup) + { + if (!mapped.Contains(parameter.Id) && parameter.Property.SetMethod != null) + { + parameter.Property.SetValue(settings, value); + } + } + + return settings; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandPropertyBinder.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandPropertyBinder.cs new file mode 100644 index 0000000..f0275a1 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandPropertyBinder.cs @@ -0,0 +1,36 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandPropertyBinder + { + public static CommandSettings CreateSettings(CommandValueLookup lookup, Type settingsType, ITypeResolver resolver) + { + var settings = CreateSettings(resolver, settingsType); + + foreach (var (parameter, value) in lookup) + { + parameter.Property.SetValue(settings, value); + } + + // Validate the settings. + var validationResult = settings.Validate(); + if (!validationResult.Successful) + { + throw CommandRuntimeException.ValidationFailed(validationResult); + } + + return settings; + } + + private static CommandSettings CreateSettings(ITypeResolver resolver, Type settingsType) + { + if (resolver.Resolve(settingsType) is CommandSettings settings) + { + return settings; + } + + throw CommandParseException.CouldNotCreateSettings(settingsType); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandValueBinder.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandValueBinder.cs new file mode 100644 index 0000000..490fbcb --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandValueBinder.cs @@ -0,0 +1,118 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandValueBinder + { + private readonly CommandValueLookup _lookup; + + public CommandValueBinder(CommandValueLookup lookup) + { + _lookup = lookup; + } + + public void Bind(CommandParameter parameter, ITypeResolver resolver, object? value) + { + if (parameter.ParameterKind == ParameterKind.Pair) + { + value = GetLookup(parameter, resolver, value); + } + else if (parameter.ParameterKind == ParameterKind.Vector) + { + value = GetArray(parameter, value); + } + else if (parameter.ParameterKind == ParameterKind.FlagWithValue) + { + value = GetFlag(parameter, value); + } + + _lookup.SetValue(parameter, value); + } + + private object GetLookup(CommandParameter parameter, ITypeResolver resolver, object? value) + { + var genericTypes = parameter.Property.PropertyType.GetGenericArguments(); + + var multimap = (IMultiMap?)_lookup.GetValue(parameter); + if (multimap == null) + { + multimap = Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(genericTypes[0], genericTypes[1])) as IMultiMap; + if (multimap == null) + { + throw new InvalidOperationException("Could not create multimap"); + } + } + + // Create deconstructor. + var deconstructorType = parameter.PairDeconstructor?.Type ?? typeof(DefaultPairDeconstructor); + if (!(resolver.Resolve(deconstructorType) is IPairDeconstructor deconstructor)) + { + if (!(Activator.CreateInstance(deconstructorType) is IPairDeconstructor activatedDeconstructor)) + { + throw new InvalidOperationException($"Could not create pair deconstructor."); + } + + deconstructor = activatedDeconstructor; + } + + // Deconstruct and add to multimap. + var pair = deconstructor.Deconstruct(resolver, genericTypes[0], genericTypes[1], value as string); + if (pair.Key != null) + { + multimap.Add(pair); + } + + return multimap; + } + + private object GetArray(CommandParameter parameter, object? value) + { + // Add a new item to the array + var array = (Array?)_lookup.GetValue(parameter); + Array newArray; + + var elementType = parameter.Property.PropertyType.GetElementType(); + if (elementType == null) + { + throw new InvalidOperationException("Could not get property type."); + } + + if (array == null) + { + newArray = Array.CreateInstance(elementType, 1); + } + else + { + newArray = Array.CreateInstance(elementType, array.Length + 1); + array.CopyTo(newArray, 0); + } + + newArray.SetValue(value, newArray.Length - 1); + return newArray; + } + + private object GetFlag(CommandParameter parameter, object? value) + { + var flagValue = (IFlagValue?)_lookup.GetValue(parameter); + if (flagValue == null) + { + flagValue = (IFlagValue?)Activator.CreateInstance(parameter.ParameterType); + if (flagValue == null) + { + throw new InvalidOperationException("Could not create flag value."); + } + } + + if (value != null) + { + // Null means set, but not with a valid value. + flagValue.Value = value; + } + + // If the parameter was mapped, then it's set. + flagValue.IsSet = true; + + return flagValue; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandValueLookup.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandValueLookup.cs new file mode 100644 index 0000000..7f305ff --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandValueLookup.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandValueLookup : IEnumerable<(CommandParameter Parameter, object? Value)> + { + private readonly Dictionary _lookup; + + public CommandValueLookup() + { + _lookup = new Dictionary(); + } + + public IEnumerator<(CommandParameter Parameter, object? Value)> GetEnumerator() + { + foreach (var pair in _lookup) + { + yield return pair.Value; + } + } + + public bool HasParameterWithName(string? name) + { + if (name == null) + { + return false; + } + + return _lookup.Values.Any(pair => pair.Parameter.PropertyName.Equals(name, StringComparison.OrdinalIgnoreCase)); + } + + public bool TryGetParameterWithName(string? name, out (CommandParameter Parameter, object? Value) result) + { + if (HasParameterWithName(name)) + { + result = _lookup.Values.FirstOrDefault(pair => pair.Parameter.PropertyName.Equals(name, StringComparison.OrdinalIgnoreCase)); + return true; + } + + result = default; + return false; + } + + public object? GetValue(CommandParameter parameter) + { + _lookup.TryGetValue(parameter.Id, out var result); + return result.Value; + } + + public void SetValue(CommandParameter parameter, object? value) + { + _lookup[parameter.Id] = (parameter, value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs b/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs new file mode 100644 index 0000000..92dd946 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Binding/CommandValueResolver.cs @@ -0,0 +1,133 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandValueResolver + { + public static CommandValueLookup GetParameterValues(CommandTree? tree, ITypeResolver resolver) + { + var lookup = new CommandValueLookup(); + var binder = new CommandValueBinder(lookup); + + CommandValidator.ValidateRequiredParameters(tree); + + while (tree != null) + { + // Process unmapped parameters. + foreach (var parameter in tree.Unmapped) + { + if (parameter.IsFlagValue()) + { + // Set the flag value to an empty, not set instance. + var instance = Activator.CreateInstance(parameter.ParameterType); + lookup.SetValue(parameter, instance); + } + else + { + // Is this an option with a default value? + if (parameter.DefaultValue != null) + { + var value = parameter.DefaultValue?.Value; + + // Need to convert the default value? + if (value != null && value.GetType() != parameter.ParameterType) + { + var converter = GetConverter(lookup, binder, resolver, parameter); + if (converter != null) + { + value = converter.ConvertFrom(value); + } + } + + binder.Bind(parameter, resolver, value); + CommandValidator.ValidateParameter(parameter, lookup); + } + } + } + + // Process mapped parameters. + foreach (var mapped in tree.Mapped) + { + if (mapped.Parameter.WantRawValue) + { + // Just try to assign the raw value. + binder.Bind(mapped.Parameter, resolver, mapped.Value); + } + else + { + var converter = GetConverter(lookup, binder, resolver, mapped.Parameter); + if (converter == null) + { + throw CommandRuntimeException.NoConverterFound(mapped.Parameter); + } + + if (mapped.Parameter.IsFlagValue() && mapped.Value == null) + { + if (mapped.Parameter is CommandOption option && option.DefaultValue != null) + { + // Set the default value. + binder.Bind(mapped.Parameter, resolver, option.DefaultValue?.Value); + } + else + { + // Set the flag but not the value. + binder.Bind(mapped.Parameter, resolver, null); + } + } + else + { + // Assign the value to the parameter. + binder.Bind(mapped.Parameter, resolver, converter.ConvertFromInvariantString(mapped.Value)); + } + } + + CommandValidator.ValidateParameter(mapped.Parameter, lookup); + } + + tree = tree.Next; + } + + return lookup; + } + + [SuppressMessage("Style", "IDE0019:Use pattern matching", Justification = "It's OK")] + private static TypeConverter? GetConverter(CommandValueLookup lookup, CommandValueBinder binder, ITypeResolver resolver, CommandParameter parameter) + { + if (parameter.Converter == null) + { + if (parameter.ParameterType.IsArray) + { + // Return a converter for each array item (not the whole array) + return TypeDescriptor.GetConverter(parameter.ParameterType.GetElementType()); + } + + if (parameter.IsFlagValue()) + { + // Is the optional value instanciated? + var value = lookup.GetValue(parameter) as IFlagValue; + if (value == null) + { + // Try to assign it with a null value. + // This will create the optional value instance without a value. + binder.Bind(parameter, resolver, null); + value = lookup.GetValue(parameter) as IFlagValue; + if (value == null) + { + throw new InvalidOperationException("Could not intialize optional value."); + } + } + + // Return a converter for the flag element type. + return TypeDescriptor.GetConverter(value.Type); + } + + return TypeDescriptor.GetConverter(parameter.ParameterType); + } + + var type = Type.GetType(parameter.Converter.ConverterTypeName); + return resolver.Resolve(type) as TypeConverter; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Collections/IMultiMap.cs b/src/Spectre.Console/Cli/Internal/Collections/IMultiMap.cs new file mode 100644 index 0000000..fb6ccd6 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Collections/IMultiMap.cs @@ -0,0 +1,14 @@ +namespace Spectre.Console.Cli.Internal +{ + /// + /// Representation of a multi map. + /// + internal interface IMultiMap + { + /// + /// Adds a key and a value to the multi map. + /// + /// The pair to add. + void Add((object? Key, object? Value) pair); + } +} diff --git a/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs b/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs new file mode 100644 index 0000000..f61654a --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Collections/MultiMap.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] + internal sealed class MultiMap : IMultiMap, ILookup, IDictionary, IReadOnlyDictionary + where TKey : notnull + { + private readonly IDictionary _lookup; + private readonly IDictionary _dictionary; + + public int Count => _lookup.Count; + + public bool IsReadOnly => false; + + public ICollection Keys => _lookup.Keys; + + public ICollection Values => _dictionary.Values; + + IEnumerable IReadOnlyDictionary.Keys => _lookup.Keys; + + IEnumerable IReadOnlyDictionary.Values => _dictionary.Values; + + TValue IReadOnlyDictionary.this[TKey key] => _dictionary[key]; + + TValue IDictionary.this[TKey key] + { + get + { + return _dictionary[key]; + } + set + { + Add(key, value); + } + } + + public IEnumerable this[TKey key] + { + get + { + if (_lookup.TryGetValue(key, out var group)) + { + return group; + } + + return Array.Empty(); + } + } + + public MultiMap() + { + _lookup = new Dictionary(); + _dictionary = new Dictionary(); + } + + private sealed class MultiMapGrouping : IGrouping + { + private readonly List _items; + + public TKey Key { get; } + + public MultiMapGrouping(TKey key, List items) + { + Key = key; + _items = items; + } + + public void Add(TValue value) + { + _items.Add(value); + } + + public IEnumerator GetEnumerator() + { + return _items.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + public bool Contains(TKey key) + { + return _lookup.ContainsKey(key); + } + + public IEnumerator> GetEnumerator() + { + foreach (var group in _lookup.Values) + { + yield return group; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public void Add(TKey key, TValue value) + { + if (!_lookup.ContainsKey(key)) + { + _lookup[key] = new MultiMapGrouping(key, new List()); + } + + _lookup[key].Add(value); + _dictionary[key] = value; + } + + public bool ContainsKey(TKey key) + { + return Contains(key); + } + + public bool Remove(TKey key) + { + return _lookup.Remove(key); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return _dictionary.TryGetValue(key, out value); + } + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Clear() + { + _lookup.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return Contains(item.Key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + _dictionary.CopyTo(array, arrayIndex); + } + + public bool Remove(KeyValuePair item) + { + return Remove(item.Key); + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + return _dictionary.GetEnumerator(); + } + + public void Add((object? Key, object? Value) pair) + { + if (pair.Key != null) + { +#pragma warning disable CS8604 // Possible null reference argument of value. + Add((TKey)pair.Key, (TValue)pair.Value); +#pragma warning restore CS8604 // Possible null reference argument of value. + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/CommandBinder.cs b/src/Spectre.Console/Cli/Internal/CommandBinder.cs new file mode 100644 index 0000000..4549b46 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/CommandBinder.cs @@ -0,0 +1,31 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandBinder + { + public static CommandSettings Bind(CommandTree? tree, Type settingsType, ITypeResolver resolver) + { + var lookup = CommandValueResolver.GetParameterValues(tree, resolver); + + // Got a constructor with at least one name corresponding to a settings? + foreach (var constructor in settingsType.GetConstructors()) + { + var parameters = constructor.GetParameters(); + if (parameters.Length > 0) + { + foreach (var parameter in parameters) + { + if (lookup.HasParameterWithName(parameter?.Name)) + { + // Use constructor injection. + return CommandConstructorBinder.CreateSettings(lookup, constructor, resolver); + } + } + } + } + + return CommandPropertyBinder.CreateSettings(lookup, settingsType, resolver); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/CommandExecutor.cs b/src/Spectre.Console/Cli/Internal/CommandExecutor.cs new file mode 100644 index 0000000..0d3c314 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/CommandExecutor.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandExecutor + { + private readonly ITypeRegistrar _registrar; + + public CommandExecutor(ITypeRegistrar registrar) + { + _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + _registrar.Register(typeof(DefaultPairDeconstructor), typeof(DefaultPairDeconstructor)); + } + + public Task Execute(IConfiguration configuration, IEnumerable args) + { + if (configuration == null) + { + throw new ArgumentNullException(nameof(configuration)); + } + + _registrar.RegisterInstance(typeof(IConfiguration), configuration); + + // Create the command model. + var model = CommandModelBuilder.Build(configuration); + _registrar.RegisterInstance(typeof(CommandModel), model); + _registrar.RegisterDependencies(model); + + // No default command? + if (model.DefaultCommand == null) + { + // Got at least one argument? + var firstArgument = args.FirstOrDefault(); + if (firstArgument != null) + { + // Asking for version? Kind of a hack, but it's alright. + // We should probably make this a bit better in the future. + if (firstArgument.Equals("--version", StringComparison.OrdinalIgnoreCase) || + firstArgument.Equals("-v", StringComparison.OrdinalIgnoreCase)) + { + var console = configuration.Settings.Console.GetConsole(); + console.WriteLine(VersionHelper.GetVersion(Assembly.GetEntryAssembly())); + return Task.FromResult(0); + } + } + } + + // Parse and map the model against the arguments. + var parser = new CommandTreeParser(model, configuration.Settings); + var parsedResult = parser.Parse(args); + _registrar.RegisterInstance(typeof(CommandTreeParserResult), parsedResult); + + // Currently the root? + if (parsedResult.Tree == null) + { + // Display help. + configuration.Settings.Console.SafeRender(HelpWriter.Write(model)); + return Task.FromResult(0); + } + + // Get the command to execute. + var leaf = parsedResult.Tree.GetLeafCommand(); + if (leaf.Command.IsBranch || leaf.ShowHelp) + { + // Branches can't be executed. Show help. + configuration.Settings.Console.SafeRender(HelpWriter.WriteCommand(model, leaf.Command)); + return Task.FromResult(leaf.ShowHelp ? 0 : 1); + } + + // Register the arguments with the container. + _registrar.RegisterInstance(typeof(IRemainingArguments), parsedResult.Remaining); + + // Create the resolver and the context. + var resolver = new TypeResolverAdapter(_registrar.Build()); + var context = new CommandContext(parsedResult.Remaining, leaf.Command.Name, leaf.Command.Data); + + // Execute the command tree. + return Execute(leaf, parsedResult.Tree, context, resolver, configuration); + } + + private static Task Execute( + CommandTree leaf, + CommandTree tree, + CommandContext context, + ITypeResolver resolver, + IConfiguration configuration) + { + // Bind the command tree against the settings. + var settings = CommandBinder.Bind(tree, leaf.Command.SettingsType, resolver); + configuration.Settings.Interceptor?.Intercept(context, settings); + + // Create and validate the command. + var command = leaf.CreateCommand(resolver); + var validationResult = command.Validate(context, settings); + if (!validationResult.Successful) + { + throw CommandRuntimeException.ValidationFailed(validationResult); + } + + // Execute the command. + return command.Execute(context, settings); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/CommandPart.cs b/src/Spectre.Console/Cli/Internal/CommandPart.cs new file mode 100644 index 0000000..f089f17 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/CommandPart.cs @@ -0,0 +1,8 @@ +namespace Spectre.Console.Cli.Internal +{ + internal enum CommandPart + { + CommandName, + LongOption, + } +} diff --git a/src/Spectre.Console/Cli/Internal/CommandSuggestor.cs b/src/Spectre.Console/Cli/Internal/CommandSuggestor.cs new file mode 100644 index 0000000..bf29af0 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/CommandSuggestor.cs @@ -0,0 +1,77 @@ +using System; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandSuggestor + { + private const float SmallestDistance = 2f; + + public static CommandInfo? Suggest(CommandModel model, CommandInfo? command, string name) + { + var result = (CommandInfo?)null; + + var container = command ?? (ICommandContainer)model; + if (command?.IsDefaultCommand ?? false) + { + // Default commands have no children, + // so use the root commands here. + container = model; + } + + var score = float.MaxValue; + foreach (var child in container.Commands.Where(x => !x.IsHidden)) + { + var temp = Score(child.Name, name); + if (temp < score) + { + score = temp; + result = child; + } + } + + if (score <= SmallestDistance) + { + return result; + } + + return null; + } + + private static float Score(string source, string target) + { + source = source.ToUpperInvariant(); + target = target.ToUpperInvariant(); + + var n = source.Length; + var m = target.Length; + + if (n == 0) + { + return m; + } + + if (m == 0) + { + return n; + } + + var d = new int[n + 1, m + 1]; + Enumerable.Range(0, n + 1).ToList().ForEach(i => d[i, 0] = i); + Enumerable.Range(0, m + 1).ToList().ForEach(i => d[0, i] = i); + + for (var sourceIndex = 1; sourceIndex <= n; sourceIndex++) + { + for (var targetIndex = 1; targetIndex <= m; targetIndex++) + { + var cost = (target[targetIndex - 1] == source[sourceIndex - 1]) ? 0 : 1; + d[sourceIndex, targetIndex] = Math.Min( + Math.Min(d[sourceIndex - 1, targetIndex] + 1, d[sourceIndex, targetIndex - 1] + 1), + d[sourceIndex - 1, targetIndex - 1] + cost); + } + } + + return d[n, m]; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/CommandValidator.cs b/src/Spectre.Console/Cli/Internal/CommandValidator.cs new file mode 100644 index 0000000..07fdca1 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/CommandValidator.cs @@ -0,0 +1,45 @@ +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandValidator + { + public static void ValidateRequiredParameters(CommandTree? tree) + { + var node = tree?.GetRootCommand(); + while (node != null) + { + foreach (var parameter in node.Unmapped) + { + if (parameter.Required) + { + switch (parameter) + { + case CommandArgument argument: + throw CommandRuntimeException.MissingRequiredArgument(node, argument); + } + } + } + + node = node.Next; + } + } + + public static void ValidateParameter(CommandParameter parameter, CommandValueLookup settings) + { + var assignedValue = settings.GetValue(parameter); + foreach (var validator in parameter.Validators) + { + var validationResult = validator.Validate(parameter, assignedValue); + if (!validationResult.Successful) + { + // If there is a error message specified in the parameter validator attribute, + // then use that one, otherwise use the validation result. + var result = validator.ErrorMessage != null + ? ValidationResult.Error(validator.ErrorMessage) + : validationResult; + + throw CommandRuntimeException.ValidationFailed(result); + } + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Commands/VersionCommand.cs b/src/Spectre.Console/Cli/Internal/Commands/VersionCommand.cs new file mode 100644 index 0000000..bdf77ff --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Commands/VersionCommand.cs @@ -0,0 +1,34 @@ +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Spectre.Console.Cli.Internal +{ + [Description("Displays the CLI library version")] + [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] + internal sealed class VersionCommand : Command + { + private readonly IAnsiConsole _writer; + + public VersionCommand(IConfiguration configuration) + { + _writer = configuration.Settings.Console.GetConsole(); + } + + public sealed class Settings : CommandSettings + { + } + + public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) + { + _writer.MarkupLine( + "[yellow]Spectre.Cli[/] version [aqua]{0}[/]", + VersionHelper.GetVersion(typeof(VersionCommand)?.Assembly)); + + _writer.MarkupLine( + "[yellow]Spectre.Console[/] version [aqua]{0}[/]", + VersionHelper.GetVersion(typeof(IAnsiConsole)?.Assembly)); + + return 0; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Commands/XmlDocCommand.cs b/src/Spectre.Console/Cli/Internal/Commands/XmlDocCommand.cs new file mode 100644 index 0000000..99aae57 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Commands/XmlDocCommand.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Xml; + +namespace Spectre.Console.Cli.Internal +{ + [Description("Generates an XML representation of the CLI configuration.")] + [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] + internal sealed class XmlDocCommand : Command + { + private readonly CommandModel _model; + private readonly IAnsiConsole _writer; + + public XmlDocCommand(IConfiguration configuration, CommandModel model) + { + _model = model ?? throw new ArgumentNullException(nameof(model)); + _writer = configuration.Settings.Console.GetConsole(); + } + + public sealed class Settings : CommandSettings + { + } + + public override int Execute([NotNull] CommandContext context, [NotNull] Settings settings) + { + _writer.Write(Serialize(_model), Style.Plain); + return 0; + } + + public static string Serialize(CommandModel model) + { + var settings = new XmlWriterSettings + { + Indent = true, + IndentChars = " ", + NewLineChars = "\n", + OmitXmlDeclaration = false, + Encoding = Encoding.UTF8, + }; + + using (var buffer = new StringWriterWithEncoding(Encoding.UTF8)) + using (var xmlWriter = XmlWriter.Create(buffer, settings)) + { + SerializeModel(model).WriteTo(xmlWriter); + xmlWriter.Flush(); + return buffer.GetStringBuilder().ToString(); + } + } + + private static XmlDocument SerializeModel(CommandModel model) + { + var document = new XmlDocument(); + var root = document.CreateElement("Model"); + + if (model.DefaultCommand != null) + { + root.AppendChild(document.CreateComment("DEFAULT COMMAND")); + root.AppendChild(CreateCommandNode(document, model.DefaultCommand, isDefaultCommand: true)); + } + + foreach (var command in model.Commands.Where(x => !x.IsHidden)) + { + root.AppendChild(document.CreateComment(command.Name.ToUpperInvariant())); + root.AppendChild(CreateCommandNode(document, command)); + } + + document.AppendChild(root); + return document; + } + + private static XmlNode CreateCommandNode(XmlDocument doc, CommandInfo command, bool isDefaultCommand = false) + { + var node = doc.CreateElement("Command"); + + // Attributes + node.SetNullableAttribute("Name", command.Name); + node.SetBooleanAttribute("IsBranch", command.IsBranch); + + if (isDefaultCommand) + { + node.SetBooleanAttribute("IsDefault", true); + } + + if (command.CommandType != null) + { + node.SetNullableAttribute("ClrType", command.CommandType?.FullName); + } + + node.SetNullableAttribute("Settings", command.SettingsType?.FullName); + + // Parameters + if (command.Parameters.Count > 0) + { + var parameterRootNode = doc.CreateElement("Parameters"); + foreach (var parameter in CreateParameterNodes(doc, command)) + { + parameterRootNode.AppendChild(parameter); + } + + node.AppendChild(parameterRootNode); + } + + // Commands + foreach (var childCommand in command.Children) + { + node.AppendChild(doc.CreateComment(childCommand.Name.ToUpperInvariant())); + node.AppendChild(CreateCommandNode(doc, childCommand)); + } + + return node; + } + + private static IEnumerable CreateParameterNodes(XmlDocument document, CommandInfo command) + { + // Arguments + foreach (var argument in command.Parameters.OfType().OrderBy(x => x.Position)) + { + var node = document.CreateElement("Argument"); + node.SetNullableAttribute("Name", argument.Value); + node.SetAttribute("Position", argument.Position.ToString(CultureInfo.InvariantCulture)); + node.SetBooleanAttribute("Required", argument.Required); + node.SetEnumAttribute("Kind", argument.ParameterKind); + node.SetNullableAttribute("ClrType", argument.ParameterType?.FullName); + + if (!string.IsNullOrWhiteSpace(argument.Description)) + { + var descriptionNode = document.CreateElement("Description"); + descriptionNode.InnerText = argument.Description; + node.AppendChild(descriptionNode); + } + + if (argument.Validators.Count > 0) + { + var validatorRootNode = document.CreateElement("Validators"); + foreach (var validator in argument.Validators.OrderBy(x => x.GetType().FullName)) + { + var validatorNode = document.CreateElement("Validator"); + validatorNode.SetNullableAttribute("ClrType", validator.GetType().FullName); + validatorNode.SetNullableAttribute("Message", validator.ErrorMessage); + validatorRootNode.AppendChild(validatorNode); + } + + node.AppendChild(validatorRootNode); + } + + yield return node; + } + + // Options + foreach (var option in command.Parameters.OfType() + .OrderBy(x => string.Join(",", x.LongNames)) + .ThenBy(x => string.Join(",", x.ShortNames))) + { + var node = document.CreateElement("Option"); + + if (option.IsShadowed) + { + node.SetBooleanAttribute("Shadowed", true); + } + + node.SetNullableAttribute("Short", option.ShortNames); + node.SetNullableAttribute("Long", option.LongNames); + node.SetNullableAttribute("Value", option.ValueName); + node.SetBooleanAttribute("Required", option.Required); + node.SetEnumAttribute("Kind", option.ParameterKind); + node.SetNullableAttribute("ClrType", option.ParameterType?.FullName); + + if (!string.IsNullOrWhiteSpace(option.Description)) + { + var descriptionNode = document.CreateElement("Description"); + descriptionNode.InnerText = option.Description; + node.AppendChild(descriptionNode); + } + + yield return node; + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Composer.cs b/src/Spectre.Console/Cli/Internal/Composer.cs new file mode 100644 index 0000000..68945fb --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composer.cs @@ -0,0 +1,106 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class Composer : IRenderable + { + private readonly StringBuilder _content; + + public Composer() + { + _content = new StringBuilder(); + } + + public Composer Text(string text) + { + _content.Append(text); + return this; + } + + public Composer Style(string style, string text) + { + _content.Append('[').Append(style).Append(']'); + _content.Append(text.EscapeMarkup()); + _content.Append("[/]"); + return this; + } + + public Composer Style(string style, Action action) + { + _content.Append('[').Append(style).Append(']'); + action(this); + _content.Append("[/]"); + return this; + } + + public Composer Space() + { + return Spaces(1); + } + + public Composer Spaces(int count) + { + return Repeat(' ', count); + } + + public Composer Tab() + { + return Tabs(1); + } + + public Composer Tabs(int count) + { + return Spaces(count * 4); + } + + public Composer Repeat(char character, int count) + { + _content.Append(new string(character, count)); + return this; + } + + public Composer LineBreak() + { + return LineBreaks(1); + } + + public Composer LineBreaks(int count) + { + for (var i = 0; i < count; i++) + { + _content.Append(Environment.NewLine); + } + + return this; + } + + public Composer Join(string separator, IEnumerable composers) + { + if (composers != null) + { + Space(); + Text(string.Join(separator, composers)); + } + + return this; + } + + public Measurement Measure(RenderContext context, int maxWidth) + { + return ((IRenderable)new Markup(_content.ToString())).Measure(context, maxWidth); + } + + public IEnumerable Render(RenderContext context, int maxWidth) + { + return ((IRenderable)new Markup(_content.ToString())).Render(context, maxWidth); + } + + public override string ToString() + { + return _content.ToString(); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Composition/Activators.cs b/src/Spectre.Console/Cli/Internal/Composition/Activators.cs new file mode 100644 index 0000000..86ad9f1 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composition/Activators.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal abstract class ComponentActivator + { + public abstract object Activate(DefaultTypeResolver container); + + public abstract ComponentActivator CreateCopy(); + } + + internal class CachingActivator : ComponentActivator + { + private readonly ComponentActivator _activator; + private object? _result; + + public CachingActivator(ComponentActivator activator) + { + _activator = activator ?? throw new ArgumentNullException(nameof(activator)); + _result = null; + } + + public override object Activate(DefaultTypeResolver container) + { + return _result ??= _activator.Activate(container); + } + + public override ComponentActivator CreateCopy() + { + return new CachingActivator(_activator.CreateCopy()); + } + } + + internal sealed class InstanceActivator : ComponentActivator + { + private readonly object _instance; + + public InstanceActivator(object instance) + { + _instance = instance; + } + + public override object Activate(DefaultTypeResolver container) + { + return _instance; + } + + public override ComponentActivator CreateCopy() + { + return new InstanceActivator(_instance); + } + } + + internal sealed class ReflectionActivator : ComponentActivator + { + private readonly Type _type; + private readonly ConstructorInfo _constructor; + private readonly List _parameters; + + public ReflectionActivator(Type type) + { + _type = type; + _constructor = GetGreediestConstructor(type); + _parameters = new List(); + + foreach (var parameter in _constructor.GetParameters()) + { + _parameters.Add(parameter); + } + } + + public override object Activate(DefaultTypeResolver container) + { + var parameters = new object?[_parameters.Count]; + for (var i = 0; i < _parameters.Count; i++) + { + var parameter = _parameters[i]; + if (parameter.ParameterType == typeof(DefaultTypeResolver)) + { + parameters[i] = container; + } + else + { + var resolved = container.Resolve(parameter.ParameterType); + if (resolved == null) + { + if (!parameter.IsOptional) + { + throw new InvalidOperationException($"Could not find registration for '{parameter.ParameterType.FullName}'."); + } + + parameters[i] = null; + } + else + { + parameters[i] = resolved; + } + } + } + + return _constructor.Invoke(parameters); + } + + public override ComponentActivator CreateCopy() + { + return new ReflectionActivator(_type); + } + + private static ConstructorInfo GetGreediestConstructor(Type type) + { + ConstructorInfo? current = null; + var count = -1; + foreach (var constructor in type.GetTypeInfo().GetConstructors()) + { + var parameters = constructor.GetParameters(); + if (parameters.Length > count) + { + count = parameters.Length; + current = constructor; + } + } + + if (current == null) + { + throw new InvalidOperationException($"Could not find a constructor for '{type.FullName}'."); + } + + return current; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistration.cs b/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistration.cs new file mode 100644 index 0000000..e910950 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistration.cs @@ -0,0 +1,31 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class ComponentRegistration + { + public Type ImplementationType { get; } + public ComponentActivator Activator { get; } + public IReadOnlyList RegistrationTypes { get; } + + public ComponentRegistration(Type type, ComponentActivator activator, IEnumerable? registrationTypes = null) + { + var registrations = new List(registrationTypes ?? Array.Empty()); + if (registrations.Count == 0) + { + // Every registration needs at least one registration type. + registrations.Add(type); + } + + ImplementationType = type; + RegistrationTypes = registrations; + Activator = activator ?? throw new ArgumentNullException(nameof(activator)); + } + + public ComponentRegistration CreateCopy() + { + return new ComponentRegistration(ImplementationType, Activator.CreateCopy(), RegistrationTypes); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistry.cs b/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistry.cs new file mode 100644 index 0000000..c1e0730 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composition/ComponentRegistry.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class ComponentRegistry : IDisposable + { + private readonly Dictionary> _registrations; + + public ComponentRegistry() + { + _registrations = new Dictionary>(); + } + + public ComponentRegistry CreateCopy() + { + var registry = new ComponentRegistry(); + foreach (var registration in _registrations.SelectMany(p => p.Value)) + { + registry.Register(registration.CreateCopy()); + } + + return registry; + } + + public void Dispose() + { + foreach (var registration in _registrations) + { + registration.Value.Clear(); + } + + _registrations.Clear(); + } + + public void Register(ComponentRegistration registration) + { + foreach (var type in new HashSet(registration.RegistrationTypes)) + { + if (!_registrations.ContainsKey(type)) + { + _registrations.Add(type, new HashSet()); + } + + _registrations[type].Add(registration); + } + } + + public ICollection GetRegistrations(Type type) + { + if (_registrations.ContainsKey(type)) + { + return _registrations[type]; + } + + return new List(); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeRegistrar.cs b/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeRegistrar.cs new file mode 100644 index 0000000..f867e0e --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeRegistrar.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class DefaultTypeRegistrar : ITypeRegistrar + { + private readonly Queue> _registry; + + public DefaultTypeRegistrar() + { + _registry = new Queue>(); + } + + public ITypeResolver Build() + { + var container = new DefaultTypeResolver(); + while (_registry.Count > 0) + { + var action = _registry.Dequeue(); + action(container.Registry); + } + + return container; + } + + public void Register(Type service, Type implementation) + { + var registration = new ComponentRegistration(implementation, new ReflectionActivator(implementation), new[] { service }); + _registry.Enqueue(registry => registry.Register(registration)); + } + + public void RegisterInstance(Type service, object implementation) + { + var registration = new ComponentRegistration(service, new CachingActivator(new InstanceActivator(implementation))); + _registry.Enqueue(registry => registry.Register(registration)); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeResolver.cs b/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeResolver.cs new file mode 100644 index 0000000..b253a73 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Composition/DefaultTypeResolver.cs @@ -0,0 +1,67 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class DefaultTypeResolver : IDisposable, ITypeResolver + { + public ComponentRegistry Registry { get; } + + public DefaultTypeResolver() + : this(null) + { + } + + public DefaultTypeResolver(ComponentRegistry? registry) + { + Registry = registry ?? new ComponentRegistry(); + } + + public void Dispose() + { + Registry.Dispose(); + } + + public object? Resolve(Type? type) + { + if (type == null) + { + return null; + } + + var isEnumerable = false; + if (type.IsGenericType) + { + if (type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + isEnumerable = true; + type = type.GenericTypeArguments[0]; + } + } + + var registrations = Registry.GetRegistrations(type); + if (registrations != null) + { + if (isEnumerable) + { + var result = Array.CreateInstance(type, registrations.Count); + for (var index = 0; index < registrations.Count; index++) + { + var registration = registrations.ElementAt(index); + result.SetValue(Resolve(registration), index); + } + + return result; + } + } + + return Resolve(registrations?.LastOrDefault()); + } + + public object? Resolve(ComponentRegistration? registration) + { + return registration?.Activator?.Activate(this); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs b/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs new file mode 100644 index 0000000..9e240e0 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/CommandAppSettings.cs @@ -0,0 +1,44 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandAppSettings : ICommandAppSettings + { + public string? ApplicationName { get; set; } + public IAnsiConsole? Console { get; set; } + public ICommandInterceptor? Interceptor { get; set; } + public ITypeRegistrarFrontend Registrar { get; set; } + public CaseSensitivity CaseSensitivity { get; set; } + public bool PropagateExceptions { get; set; } + public bool ValidateExamples { get; set; } + public bool StrictParsing { get; set; } + + public ParsingMode ParsingMode => + StrictParsing ? ParsingMode.Strict : ParsingMode.Relaxed; + + public CommandAppSettings(ITypeRegistrar registrar) + { + Registrar = new TypeRegistrar(registrar); + CaseSensitivity = CaseSensitivity.All; + } + + public bool IsTrue(Func func, string environmentVariableName) + { + if (func(this)) + { + return true; + } + + var environmentVariable = Environment.GetEnvironmentVariable(environmentVariableName); + if (!string.IsNullOrWhiteSpace(environmentVariable)) + { + if (environmentVariable.Equals("True", StringComparison.OrdinalIgnoreCase)) + { + return true; + } + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Configuration/CommandConfigurator.cs b/src/Spectre.Console/Cli/Internal/Configuration/CommandConfigurator.cs new file mode 100644 index 0000000..a01d011 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/CommandConfigurator.cs @@ -0,0 +1,42 @@ +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandConfigurator : ICommandConfigurator + { + public ConfiguredCommand Command { get; } + + public CommandConfigurator(ConfiguredCommand command) + { + Command = command; + } + + public ICommandConfigurator WithExample(string[] args) + { + Command.Examples.Add(args); + return this; + } + + public ICommandConfigurator WithAlias(string alias) + { + Command.Aliases.Add(alias); + return this; + } + + public ICommandConfigurator WithDescription(string description) + { + Command.Description = description; + return this; + } + + public ICommandConfigurator WithData(object data) + { + Command.Data = data; + return this; + } + + public ICommandConfigurator IsHidden() + { + Command.IsHidden = true; + return this; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/ConfigurationHelper.cs b/src/Spectre.Console/Cli/Internal/Configuration/ConfigurationHelper.cs new file mode 100644 index 0000000..a383ff8 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/ConfigurationHelper.cs @@ -0,0 +1,43 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal static class ConfigurationHelper + { + public static Type? GetSettingsType(Type commandType) + { + if (typeof(ICommand).GetTypeInfo().IsAssignableFrom(commandType) && + GetGenericTypeArguments(commandType, typeof(ICommand<>), out var result)) + { + return result[0]; + } + + return null; + } + + private static bool GetGenericTypeArguments(Type? type, Type genericType, + [NotNullWhen(true)] out Type[]? genericTypeArguments) + { + while (type != null) + { + foreach (var @interface in type.GetTypeInfo().GetInterfaces()) + { + if (!@interface.GetTypeInfo().IsGenericType || @interface.GetGenericTypeDefinition() != genericType) + { + continue; + } + + genericTypeArguments = @interface.GenericTypeArguments; + return true; + } + + type = type.GetTypeInfo().BaseType; + } + + genericTypeArguments = null; + return false; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/Configurator.cs b/src/Spectre.Console/Cli/Internal/Configuration/Configurator.cs new file mode 100644 index 0000000..5bb8e67 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/Configurator.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Cli.Unsafe; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class Configurator : IUnsafeConfigurator, IConfigurator, IConfiguration + { + private readonly ITypeRegistrar _registrar; + + public IList Commands { get; } + public CommandAppSettings Settings { get; } + public ConfiguredCommand? DefaultCommand { get; private set; } + public IList Examples { get; } + + ICommandAppSettings IConfigurator.Settings => Settings; + + public Configurator(ITypeRegistrar registrar) + { + _registrar = registrar; + + Commands = new List(); + Settings = new CommandAppSettings(registrar); + Examples = new List(); + } + + public void AddExample(string[] args) + { + Examples.Add(args); + } + + public void SetDefaultCommand() + where TDefaultCommand : class, ICommand + { + DefaultCommand = ConfiguredCommand.FromType( + Constants.DefaultCommandName, isDefaultCommand: true); + } + + public ICommandConfigurator AddCommand(string name) + where TCommand : class, ICommand + { + var command = Commands.AddAndReturn(ConfiguredCommand.FromType(name, false)); + return new CommandConfigurator(command); + } + + public ICommandConfigurator AddDelegate(string name, Func func) + where TSettings : CommandSettings + { + var command = Commands.AddAndReturn(ConfiguredCommand.FromDelegate( + name, (context, settings) => func(context, (TSettings)settings))); + return new CommandConfigurator(command); + } + + public void AddBranch(string name, Action> action) + where TSettings : CommandSettings + { + var command = ConfiguredCommand.FromBranch(name); + action(new Configurator(command, _registrar)); + Commands.Add(command); + } + + ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) + { + var method = GetType().GetMethod("AddCommand"); + if (method == null) + { + throw new CommandConfigurationException("Could not find AddCommand by reflection."); + } + + method = method.MakeGenericMethod(command); + + if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) + { + throw new CommandConfigurationException("Invoking AddCommand returned null."); + } + + return result; + } + + void IUnsafeConfigurator.AddBranch(string name, Type settings, Action action) + { + var command = ConfiguredCommand.FromBranch(settings, name); + + // Create the configurator. + var configuratorType = typeof(Configurator<>).MakeGenericType(settings); + if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator)) + { + throw new CommandConfigurationException("Could not create configurator by reflection."); + } + + action(configurator); + Commands.Add(command); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/Configurator`1.cs b/src/Spectre.Console/Cli/Internal/Configuration/Configurator`1.cs new file mode 100644 index 0000000..386bfe3 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/Configurator`1.cs @@ -0,0 +1,94 @@ +using System; +using Spectre.Console.Cli.Unsafe; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class Configurator : IUnsafeBranchConfigurator, IConfigurator + where TSettings : CommandSettings + { + private readonly ConfiguredCommand _command; + private readonly ITypeRegistrar? _registrar; + + public Configurator(ConfiguredCommand command, ITypeRegistrar? registrar) + { + _command = command; + _registrar = registrar; + } + + public void SetDescription(string description) + { + _command.Description = description; + } + + public void AddExample(string[] args) + { + _command.Examples.Add(args); + } + + public void HideBranch() + { + _command.IsHidden = true; + } + + public ICommandConfigurator AddCommand(string name) + where TCommand : class, ICommandLimiter + { + var command = ConfiguredCommand.FromType(name); + var configurator = new CommandConfigurator(command); + + _command.Children.Add(command); + return configurator; + } + + public ICommandConfigurator AddDelegate(string name, Func func) + where TDerivedSettings : TSettings + { + var command = ConfiguredCommand.FromDelegate( + name, (context, settings) => func(context, (TDerivedSettings)settings)); + + _command.Children.Add(command); + return new CommandConfigurator(command); + } + + public void AddBranch(string name, Action> action) + where TDerivedSettings : TSettings + { + var command = ConfiguredCommand.FromBranch(name); + action(new Configurator(command, _registrar)); + _command.Children.Add(command); + } + + ICommandConfigurator IUnsafeConfigurator.AddCommand(string name, Type command) + { + var method = GetType().GetMethod("AddCommand"); + if (method == null) + { + throw new CommandConfigurationException("Could not find AddCommand by reflection."); + } + + method = method.MakeGenericMethod(command); + + if (!(method.Invoke(this, new object[] { name }) is ICommandConfigurator result)) + { + throw new CommandConfigurationException("Invoking AddCommand returned null."); + } + + return result; + } + + void IUnsafeConfigurator.AddBranch(string name, Type settings, Action action) + { + var command = ConfiguredCommand.FromBranch(settings, name); + + // Create the configurator. + var configuratorType = typeof(Configurator<>).MakeGenericType(settings); + if (!(Activator.CreateInstance(configuratorType, new object?[] { command, _registrar }) is IUnsafeBranchConfigurator configurator)) + { + throw new CommandConfigurationException("Could not create configurator by reflection."); + } + + action(configurator); + _command.Children.Add(command); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/ConfiguredCommand.cs b/src/Spectre.Console/Cli/Internal/Configuration/ConfiguredCommand.cs new file mode 100644 index 0000000..02f1a35 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/ConfiguredCommand.cs @@ -0,0 +1,69 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class ConfiguredCommand + { + public string Name { get; } + public HashSet Aliases { get; } + public string? Description { get; set; } + public object? Data { get; set; } + public Type? CommandType { get; } + public Type SettingsType { get; } + public Func? Delegate { get; } + public bool IsDefaultCommand { get; } + public bool IsHidden { get; set; } + + public IList Children { get; } + public IList Examples { get; } + + private ConfiguredCommand( + string name, + Type? commandType, + Type settingsType, + Func? @delegate, + bool isDefaultCommand) + { + Name = name; + Aliases = new HashSet(StringComparer.OrdinalIgnoreCase); + CommandType = commandType; + SettingsType = settingsType; + Delegate = @delegate; + IsDefaultCommand = isDefaultCommand; + + Children = new List(); + Examples = new List(); + } + + public static ConfiguredCommand FromBranch(Type settings, string name) + { + return new ConfiguredCommand(name, null, settings, null, false); + } + + public static ConfiguredCommand FromBranch(string name) + where TSettings : CommandSettings + { + return new ConfiguredCommand(name, null, typeof(TSettings), null, false); + } + + public static ConfiguredCommand FromType(string name, bool isDefaultCommand = false) + where TCommand : class, ICommand + { + var settingsType = ConfigurationHelper.GetSettingsType(typeof(TCommand)); + if (settingsType == null) + { + throw CommandRuntimeException.CouldNotGetSettingsType(typeof(TCommand)); + } + + return new ConfiguredCommand(name, typeof(TCommand), settingsType, null, isDefaultCommand); + } + + public static ConfiguredCommand FromDelegate( + string name, Func? @delegate = null) + where TSettings : CommandSettings + { + return new ConfiguredCommand(name, null, typeof(TSettings), @delegate, false); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/IConfiguration.cs b/src/Spectre.Console/Cli/Internal/Configuration/IConfiguration.cs new file mode 100644 index 0000000..799c38b --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/IConfiguration.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + /// + /// Represents a configuration. + /// + internal interface IConfiguration + { + /// + /// Gets the configured commands. + /// + IList Commands { get; } + + /// + /// Gets the settings for the configuration. + /// + CommandAppSettings Settings { get; } + + /// + /// Gets the default command for the configuration. + /// + ConfiguredCommand? DefaultCommand { get; } + + /// + /// Gets all examples for the configuration. + /// + IList Examples { get; } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Configuration/TemplateParser.cs b/src/Spectre.Console/Cli/Internal/Configuration/TemplateParser.cs new file mode 100644 index 0000000..2c15fae --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/TemplateParser.cs @@ -0,0 +1,149 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal static class TemplateParser + { + public sealed class ArgumentResult + { + public string Value { get; set; } + public bool Required { get; set; } + + public ArgumentResult(string value, bool required) + { + Value = value; + Required = required; + } + } + + public sealed class OptionResult + { + public List LongNames { get; set; } + public List ShortNames { get; set; } + public string? Value { get; set; } + public bool ValueIsOptional { get; set; } + + public OptionResult() + { + ShortNames = new List(); + LongNames = new List(); + } + } + + public static ArgumentResult ParseArgumentTemplate(string template) + { + var valueName = default(string); + var required = false; + foreach (var token in TemplateTokenizer.Tokenize(template)) + { + if (token.TokenKind == TemplateToken.Kind.ShortName || + token.TokenKind == TemplateToken.Kind.LongName) + { + throw CommandTemplateException.ArgumentCannotContainOptions(template, token); + } + + if (token.TokenKind == TemplateToken.Kind.OptionalValue || + token.TokenKind == TemplateToken.Kind.RequiredValue) + { + if (!string.IsNullOrWhiteSpace(valueName)) + { + throw CommandTemplateException.MultipleValuesAreNotSupported(template, token); + } + + if (string.IsNullOrWhiteSpace(token.Value)) + { + throw CommandTemplateException.ValuesMustHaveName(template, token); + } + + valueName = token.Value; + required = token.TokenKind == TemplateToken.Kind.RequiredValue; + } + } + + if (valueName == null) + { + throw CommandTemplateException.ArgumentsMustHaveValueName(template); + } + + return new ArgumentResult(valueName, required); + } + + public static OptionResult ParseOptionTemplate(string template) + { + var result = new OptionResult(); + + foreach (var token in TemplateTokenizer.Tokenize(template)) + { + if (token.TokenKind == TemplateToken.Kind.LongName || token.TokenKind == TemplateToken.Kind.ShortName) + { + if (string.IsNullOrWhiteSpace(token.Value)) + { + throw CommandTemplateException.OptionsMustHaveName(template, token); + } + + if (char.IsDigit(token.Value[0])) + { + throw CommandTemplateException.OptionNamesCannotStartWithDigit(template, token); + } + + foreach (var character in token.Value) + { + if (!char.IsLetterOrDigit(character) && character != '-' && character != '_') + { + throw CommandTemplateException.InvalidCharacterInOptionName(template, token, character); + } + } + } + + if (token.TokenKind == TemplateToken.Kind.LongName) + { + if (token.Value.Length == 1) + { + throw CommandTemplateException.LongOptionMustHaveMoreThanOneCharacter(template, token); + } + + result.LongNames.Add(token.Value); + } + + if (token.TokenKind == TemplateToken.Kind.ShortName) + { + if (token.Value.Length > 1) + { + throw CommandTemplateException.ShortOptionMustOnlyBeOneCharacter(template, token); + } + + result.ShortNames.Add(token.Value); + } + + if (token.TokenKind == TemplateToken.Kind.RequiredValue || + token.TokenKind == TemplateToken.Kind.OptionalValue) + { + if (!string.IsNullOrWhiteSpace(result.Value)) + { + throw CommandTemplateException.MultipleOptionValuesAreNotSupported(template, token); + } + + foreach (var character in token.Value) + { + if (!char.IsLetterOrDigit(character) && + character != '=' && character != '-' && character != '_') + { + throw CommandTemplateException.InvalidCharacterInValueName(template, token, character); + } + } + + result.Value = token.Value.ToUpperInvariant(); + result.ValueIsOptional = token.TokenKind == TemplateToken.Kind.OptionalValue; + } + } + + if (result.LongNames.Count == 0 && + result.ShortNames.Count == 0) + { + throw CommandTemplateException.MissingLongAndShortName(template, null); + } + + return result; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Configuration/TemplateToken.cs b/src/Spectre.Console/Cli/Internal/Configuration/TemplateToken.cs new file mode 100644 index 0000000..3ba1a50 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/TemplateToken.cs @@ -0,0 +1,27 @@ +namespace Spectre.Console.Cli.Internal +{ + internal sealed class TemplateToken + { + public Kind TokenKind { get; } + public int Position { get; } + public string Value { get; } + public string Representation { get; } + + public TemplateToken(Kind kind, int position, string value, string representation) + { + TokenKind = kind; + Position = position; + Value = value; + Representation = representation; + } + + public enum Kind + { + Unknown = 0, + LongName, + ShortName, + RequiredValue, + OptionalValue, + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Configuration/TemplateTokenizer.cs b/src/Spectre.Console/Cli/Internal/Configuration/TemplateTokenizer.cs new file mode 100644 index 0000000..c72996d --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Configuration/TemplateTokenizer.cs @@ -0,0 +1,135 @@ +using System.Collections.Generic; +using System.Text; + +namespace Spectre.Console.Cli.Internal +{ + internal static class TemplateTokenizer + { + public static IReadOnlyList Tokenize(string template) + { + using var buffer = new TextBuffer(template); + var result = new List(); + + while (!buffer.ReachedEnd) + { + EatWhitespace(buffer); + + if (!buffer.TryPeek(out var character)) + { + break; + } + + if (character == '-') + { + result.Add(ReadOption(buffer)); + } + else if (character == '|') + { + buffer.Consume('|'); + } + else if (character == '<') + { + result.Add(ReadValue(buffer, true)); + } + else if (character == '[') + { + result.Add(ReadValue(buffer, false)); + } + else + { + throw CommandTemplateException.UnexpectedCharacter(buffer.Original, buffer.Position, character); + } + } + + return result; + } + + private static void EatWhitespace(TextBuffer buffer) + { + while (!buffer.ReachedEnd) + { + var character = buffer.Peek(); + if (!char.IsWhiteSpace(character)) + { + break; + } + + buffer.Read(); + } + } + + private static TemplateToken ReadOption(TextBuffer buffer) + { + var position = buffer.Position; + + buffer.Consume('-'); + if (buffer.IsNext('-')) + { + buffer.Consume('-'); + var longValue = ReadOptionName(buffer); + return new TemplateToken(TemplateToken.Kind.LongName, position, longValue, $"--{longValue}"); + } + + var shortValue = ReadOptionName(buffer); + return new TemplateToken(TemplateToken.Kind.ShortName, position, shortValue, $"-{shortValue}"); + } + + private static string ReadOptionName(TextBuffer buffer) + { + var builder = new StringBuilder(); + while (!buffer.ReachedEnd) + { + var character = buffer.Peek(); + if (char.IsWhiteSpace(character) || character == '|') + { + break; + } + + builder.Append(buffer.Read()); + } + + return builder.ToString(); + } + + private static TemplateToken ReadValue(TextBuffer buffer, bool required) + { + var start = required ? '<' : '['; + var end = required ? '>' : ']'; + + var position = buffer.Position; + var kind = required ? TemplateToken.Kind.RequiredValue : TemplateToken.Kind.OptionalValue; + + // Consume start of value character (< or [). + buffer.Consume(start); + + var builder = new StringBuilder(); + while (!buffer.ReachedEnd) + { + var character = buffer.Peek(); + if (character == end) + { + break; + } + + buffer.Read(); + builder.Append(character); + } + + if (buffer.ReachedEnd) + { + var name = builder.ToString(); + var token = new TemplateToken(kind, position, name, $"{start}{name}"); + throw CommandTemplateException.UnterminatedValueName(buffer.Original, token); + } + + // Consume end of value character (> or ]). + buffer.Consume(end); + + // Get the value (the text within the brackets). + var value = builder.ToString(); + + // Create a token and return it. + return new TemplateToken(kind, position, value, required ? $"<{value}>" : $"[{value}]"); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Constants.cs b/src/Spectre.Console/Cli/Internal/Constants.cs new file mode 100644 index 0000000..42a7e47 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Constants.cs @@ -0,0 +1,22 @@ +namespace Spectre.Console.Cli.Internal +{ + internal static class Constants + { + public const string DefaultCommandName = "__default_command"; + public const string True = "true"; + public const string False = "false"; + + public static string[] AcceptedBooleanValues { get; } = new string[] + { + True, + False, + }; + + public static class Commands + { + public const string Branch = "cli"; + public const string Version = "version"; + public const string XmlDoc = "xmldoc"; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/DefaultPairDeconstructor.cs b/src/Spectre.Console/Cli/Internal/DefaultPairDeconstructor.cs new file mode 100644 index 0000000..64134ef --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/DefaultPairDeconstructor.cs @@ -0,0 +1,78 @@ +using System; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; + +namespace Spectre.Console.Cli.Internal +{ + [SuppressMessage("Performance", "CA1812: Avoid uninstantiated internal classes")] + internal sealed class DefaultPairDeconstructor : IPairDeconstructor + { + /// + (object? Key, object? Value) IPairDeconstructor.Deconstruct( + ITypeResolver resolver, + Type keyType, + Type valueType, + string? value) + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + var keyConverter = GetConverter(keyType); + var valueConverter = GetConverter(valueType); + + var parts = value.Split(new[] { '=' }, StringSplitOptions.None); + if (parts.Length < 1 || parts.Length > 2) + { + throw CommandParseException.ValueIsNotInValidFormat(value); + } + + var stringkey = parts[0]; + var stringValue = parts.Length == 2 ? parts[1] : null; + if (stringValue == null) + { + // Got a default constructor? + if (valueType.IsValueType) + { + // Get the string variant of a default instance. + // Should not get null here, but compiler doesn't know that. + stringValue = Activator.CreateInstance(valueType)?.ToString() ?? string.Empty; + } + else + { + // Try with an empty string. + // Hopefully, the type converter knows how to convert it. + stringValue = string.Empty; + } + } + + return (Parse(keyConverter, keyType, stringkey), + Parse(valueConverter, valueType, stringValue)); + } + + private static object Parse(TypeConverter converter, Type type, string value) + { + try + { + return converter.ConvertTo(value, type); + } + catch + { + // Can't convert something. Just give up and tell the user. + throw CommandParseException.ValueIsNotInValidFormat(value); + } + } + + private static TypeConverter GetConverter(Type type) + { + var converter = TypeDescriptor.GetConverter(type); + if (converter != null) + { + return converter; + } + + throw new CommandConfigurationException($"Could find a type converter for '{type.FullName}'."); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/DelegateCommand.cs b/src/Spectre.Console/Cli/Internal/DelegateCommand.cs new file mode 100644 index 0000000..342e522 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/DelegateCommand.cs @@ -0,0 +1,25 @@ +using System; +using System.Threading.Tasks; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class DelegateCommand : ICommand + { + private readonly Func _func; + + public DelegateCommand(Func func) + { + _func = func; + } + + public Task Execute(CommandContext context, CommandSettings settings) + { + return Task.FromResult(_func(context, settings)); + } + + public ValidationResult Validate(CommandContext context, CommandSettings settings) + { + return ValidationResult.Success(); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineParseExceptionFactory.cs b/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineParseExceptionFactory.cs new file mode 100644 index 0000000..94304d5 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineParseExceptionFactory.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandLineParseExceptionFactory + { + internal static CommandParseException Create(string arguments, CommandTreeToken token, string message, string details) + { + return new CommandParseException(message, CreatePrettyMessage(arguments, token, message, details)); + } + + internal static CommandParseException Create(IEnumerable arguments, CommandTreeToken token, string message, string details) + { + return new CommandParseException(message, CreatePrettyMessage(string.Join(" ", arguments), token, message, details)); + } + + private static IRenderable CreatePrettyMessage(string arguments, CommandTreeToken token, string message, string details) + { + var composer = new Composer(); + + var position = token?.Position ?? 0; + var value = token?.Representation ?? arguments; + + // Header + composer.LineBreak(); + composer.Style("red", "Error:"); + composer.Space().Text(message.EscapeMarkup()); + composer.LineBreak(); + + // Template + composer.LineBreak(); + composer.Spaces(7).Text(arguments.EscapeMarkup()); + + // Error + composer.LineBreak(); + composer.Spaces(7).Spaces(position); + + composer.Style("red", error => + { + error.Repeat('^', value.Length); + error.Space(); + error.Text(details.TrimEnd('.').EscapeMarkup()); + error.LineBreak(); + }); + + composer.LineBreak(); + + return composer; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineTemplateExceptionFactory.cs b/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineTemplateExceptionFactory.cs new file mode 100644 index 0000000..1ba7373 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Exceptions/CommandLineTemplateExceptionFactory.cs @@ -0,0 +1,57 @@ +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandLineTemplateExceptionFactory + { + internal static CommandTemplateException Create(string template, TemplateToken? token, string message, string details) + { + return new CommandTemplateException(message, template, CreatePrettyMessage(template, token, message, details)); + } + + private static IRenderable CreatePrettyMessage(string template, TemplateToken? token, string message, string details) + { + var composer = new Composer(); + + var position = token?.Position ?? 0; + var value = token?.Representation ?? template; + + // Header + composer.LineBreak(); + composer.Style("red", "Error:"); + composer.Space().Text("An error occured when parsing template."); + composer.LineBreak(); + composer.Spaces(7).Style("yellow", message.EscapeMarkup()); + composer.LineBreak(); + + if (string.IsNullOrWhiteSpace(template)) + { + // Error + composer.LineBreak(); + composer.Style("red", message.EscapeMarkup()); + composer.LineBreak(); + } + else + { + // Template + composer.LineBreak(); + composer.Spaces(7).Text(template.EscapeMarkup()); + + // Error + composer.LineBreak(); + composer.Spaces(7).Spaces(position); + composer.Style("red", error => + { + error.Repeat('^', value.Length); + error.Space(); + error.Text(details.TrimEnd('.').EscapeMarkup()); + error.LineBreak(); + }); + } + + composer.LineBreak(); + + return composer; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/AnsiConsoleExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/AnsiConsoleExtensions.cs new file mode 100644 index 0000000..47d28ac --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/AnsiConsoleExtensions.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Internal +{ + internal static class AnsiConsoleExtensions + { + private static readonly Lazy _console; + + static AnsiConsoleExtensions() + { + _console = new Lazy(() => AnsiConsole.Console); + } + + public static IAnsiConsole GetConsole(this IAnsiConsole? console) + { + return console ?? _console.Value; + } + + public static void SafeRender(this IAnsiConsole? console, IRenderable? renderable) + { + if (renderable != null) + { + console ??= _console.Value; + console.Render(renderable); + } + } + + public static void SafeRender(this IAnsiConsole? console, IEnumerable renderables) + { + console ??= _console.Value; + foreach (var renderable in renderables) + { + if (renderable != null) + { + console.Render(renderable); + } + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/CaseSensitivityExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/CaseSensitivityExtensions.cs new file mode 100644 index 0000000..3b7e1fd --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/CaseSensitivityExtensions.cs @@ -0,0 +1,35 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CaseSensitivityExtensions + { + public static StringComparison GetStringComparison(this CaseSensitivity caseSensitivity, CommandPart part) + { + if (part == CommandPart.CommandName && (caseSensitivity & CaseSensitivity.Commands) == 0) + { + return StringComparison.OrdinalIgnoreCase; + } + else if (part == CommandPart.LongOption && (caseSensitivity & CaseSensitivity.LongOptions) == 0) + { + return StringComparison.OrdinalIgnoreCase; + } + + return StringComparison.Ordinal; + } + + public static StringComparer GetStringComparer(this CaseSensitivity caseSensitivity, CommandPart part) + { + if (part == CommandPart.CommandName && (caseSensitivity & CaseSensitivity.Commands) == 0) + { + return StringComparer.OrdinalIgnoreCase; + } + else if (part == CommandPart.LongOption && (caseSensitivity & CaseSensitivity.LongOptions) == 0) + { + return StringComparer.OrdinalIgnoreCase; + } + + return StringComparer.Ordinal; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/ListExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/ListExtensions.cs new file mode 100644 index 0000000..82fb62f --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/ListExtensions.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal static class ListExtensions + { + public static void ForEach(this IEnumerable source, Action action) + { + if (source != null && action != null) + { + foreach (var item in source) + { + action(item); + } + } + } + + public static T AddAndReturn(this IList source, T item) + where T : class + { + source.Add(item); + return item; + } + + public static void AddIfNotNull(this IList source, T? item) + where T : class + { + if (item != null) + { + source.Add(item); + } + } + + public static void AddRangeIfNotNull(this IList source, IEnumerable items) + where T : class + { + foreach (var item in items) + { + if (item != null) + { + source.Add(item); + } + } + } + + public static void AddRange(this IList source, IEnumerable items) + { + foreach (var item in items) + { + source.Add(item); + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/StringExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/StringExtensions.cs new file mode 100644 index 0000000..da6426f --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/StringExtensions.cs @@ -0,0 +1,18 @@ +#if NET5_0 +using System; +#endif + +namespace Spectre.Console.Cli +{ + internal static class StringExtensions + { + internal static int OrdinalIndexOf(this string text, char token) + { +#if NET5_0 + return text.IndexOf(token, StringComparison.Ordinal); +#else + return text.IndexOf(token); +#endif + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/TypeExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/TypeExtensions.cs new file mode 100644 index 0000000..b192542 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/TypeExtensions.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class TypeExtensions + { + public static bool IsPairDeconstructable(this Type type) + { + if (type.IsGenericType) + { + if (type.GetGenericTypeDefinition() == typeof(ILookup<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>)) + { + return true; + } + } + + return false; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/TypeRegistrarExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/TypeRegistrarExtensions.cs new file mode 100644 index 0000000..b0aeb79 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/TypeRegistrarExtensions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Spectre.Console.Cli.Internal +{ + internal static class TypeRegistrarExtensions + { + public static void RegisterDependencies(this ITypeRegistrar registrar, CommandModel model) + { + var stack = new Stack(); + model.Commands.ForEach(c => stack.Push(c)); + if (model.DefaultCommand != null) + { + stack.Push(model.DefaultCommand); + } + + while (stack.Count > 0) + { + var command = stack.Pop(); + + if (command.SettingsType == null) + { + // TODO: Error message + throw new InvalidOperationException("Command setting type cannot be null."); + } + + if (command.CommandType != null) + { + registrar?.Register(typeof(ICommand), command.CommandType); + registrar?.Register(command.CommandType, command.CommandType); + } + + if (!command.SettingsType.IsAbstract) + { + registrar?.Register(command.SettingsType, command.SettingsType); + } + + foreach (var parameter in command.Parameters) + { + var pairDeconstructor = parameter?.PairDeconstructor?.Type; + if (pairDeconstructor != null) + { + registrar?.Register(pairDeconstructor, pairDeconstructor); + } + + var typeConverterTypeName = parameter?.Converter?.ConverterTypeName; + if (!string.IsNullOrWhiteSpace(typeConverterTypeName)) + { + var typeConverterType = Type.GetType(typeConverterTypeName); + Debug.Assert(typeConverterType != null, "Could not create type"); + registrar?.Register(typeConverterType, typeConverterType); + } + } + + foreach (var child in command.Children) + { + stack.Push(child); + } + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Extensions/XmlElementExtensions.cs b/src/Spectre.Console/Cli/Internal/Extensions/XmlElementExtensions.cs new file mode 100644 index 0000000..b317001 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Extensions/XmlElementExtensions.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; +using System.Xml; + +namespace Spectre.Console.Cli.Internal +{ + internal static class XmlElementExtensions + { + public static void SetNullableAttribute(this XmlElement element, string name, string? value) + { + element.SetAttribute(name, value ?? "NULL"); + } + + public static void SetNullableAttribute(this XmlElement element, string name, IEnumerable? values) + { + if (values?.Any() != true) + { + element.SetAttribute(name, "NULL"); + } + + element.SetAttribute(name, string.Join(",", values ?? Enumerable.Empty())); + } + + public static void SetBooleanAttribute(this XmlElement element, string name, bool value) + { + element.SetAttribute(name, value ? "true" : "false"); + } + + public static void SetEnumAttribute(this XmlElement element, string name, Enum value) + { + var field = value.GetType().GetField(value.ToString()); + if (field != null) + { + var attribute = field.GetCustomAttribute(false); + if (attribute == null) + { + throw new InvalidOperationException("Enum is missing description."); + } + + element.SetAttribute(name, attribute.Description); + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/HelpWriter.cs b/src/Spectre.Console/Cli/Internal/HelpWriter.cs new file mode 100644 index 0000000..44c5465 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/HelpWriter.cs @@ -0,0 +1,382 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Spectre.Console.Rendering; + +namespace Spectre.Console.Cli.Internal +{ + internal static class HelpWriter + { + private sealed class HelpArgument + { + public string Name { get; } + public bool Required { get; } + public string? Description { get; } + + public HelpArgument(string name, bool required, string? description) + { + Name = name; + Required = required; + Description = description; + } + + public static IReadOnlyList Get(CommandInfo? command) + { + var arguments = new List(); + arguments.AddRange(command?.Parameters?.OfType()?.Select( + x => new HelpArgument(x.Value, x.Required, x.Description)) + ?? Array.Empty()); + return arguments; + } + } + + private sealed class HelpOption + { + public string Short { get; } + public string Long { get; } + public string? Value { get; } + public bool? ValueIsOptional { get; } + public string? Description { get; } + + public HelpOption(string @short, string @long, string? @value, bool? valueIsOptional, string? description) + { + Short = @short; + Long = @long; + Value = value; + ValueIsOptional = valueIsOptional; + Description = description; + } + + public static IReadOnlyList Get(CommandModel model, CommandInfo? command) + { + var parameters = new List(); + parameters.Add(new HelpOption("h", "help", null, null, "Prints help information")); + + // At the root and no default command? + if (command == null && model?.DefaultCommand == null) + { + parameters.Add(new HelpOption("v", "version", null, null, "Prints version information")); + } + + parameters.AddRange(command?.Parameters?.OfType()?.Select(o => + new HelpOption( + o.ShortNames.FirstOrDefault(), o.LongNames.FirstOrDefault(), + o.ValueName, o.ValueIsOptional, o.Description)) + ?? Array.Empty()); + return parameters; + } + } + + public static IEnumerable Write(CommandModel model) + { + return WriteCommand(model, null); + } + + public static IEnumerable WriteCommand(CommandModel model, CommandInfo? command) + { + var container = command as ICommandContainer ?? model; + var isDefaultCommand = command?.IsDefaultCommand ?? false; + + var result = new List(); + result.AddRange(GetUsage(model, command)); + result.AddRange(GetExamples(model, command)); + result.AddRange(GetArguments(command)); + result.AddRange(GetOptions(model, command)); + result.AddRange(GetCommands(model, container, isDefaultCommand)); + + return result; + } + + private static IEnumerable GetUsage(CommandModel model, CommandInfo? command) + { + var composer = new Composer(); + composer.Style("yellow", "USAGE:").LineBreak(); + composer.Tab().Text(model.GetApplicationName()); + + var parameters = new Stack(); + + if (command == null) + { + parameters.Push("[aqua][/]"); + parameters.Push("[grey][[OPTIONS]][/]"); + } + else + { + var current = command; + if (command.IsBranch) + { + parameters.Push("[aqua][/]"); + } + + while (current != null) + { + var isCurrent = current == command; + if (isCurrent) + { + parameters.Push("[grey][[OPTIONS]][/]"); + } + + if (current.Parameters.OfType().Any()) + { + var optionalArguments = current.Parameters.OfType().Where(x => !x.Required).ToArray(); + if (optionalArguments.Length > 0 || !isCurrent) + { + foreach (var optionalArgument in optionalArguments) + { + parameters.Push($"[silver][[{optionalArgument.Value.EscapeMarkup()}]][/]"); + } + } + + if (isCurrent) + { + foreach (var argument in current.Parameters.OfType() + .Where(a => a.Required).OrderBy(a => a.Position).ToArray()) + { + parameters.Push($"[aqua]<{argument.Value.EscapeMarkup()}>[/]"); + } + } + } + + if (!current.IsDefaultCommand) + { + if (isCurrent) + { + parameters.Push($"[underline]{current.Name.EscapeMarkup()}[/]"); + } + else + { + parameters.Push($"{current.Name.EscapeMarkup()}"); + } + } + + current = current.Parent; + } + } + + composer.Join(" ", parameters); + composer.LineBreaks(2); + + return new[] + { + composer, + }; + } + + private static IEnumerable GetExamples(CommandModel model, CommandInfo? command) + { + var maxExamples = int.MaxValue; + + var examples = command?.Examples ?? model.Examples ?? new List(); + if (examples.Count == 0) + { + // Since we're not checking direct examples, + // make sure that we limit the number of examples. + maxExamples = 5; + + // Get the current root command. + var root = command ?? (ICommandContainer)model; + var queue = new Queue(new[] { root }); + + // Traverse the command tree and look for examples. + // As soon as a node contains commands, bail. + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + foreach (var cmd in current.Commands) + { + if (cmd.Examples.Count > 0) + { + examples.AddRange(cmd.Examples); + } + + queue.Enqueue(cmd); + } + + if (examples.Count >= maxExamples) + { + break; + } + } + } + + if (examples.Count > 0) + { + var composer = new Composer(); + composer.Style("yellow", "EXAMPLES:").LineBreak(); + + for (var index = 0; index < Math.Min(maxExamples, examples.Count); index++) + { + var args = string.Join(" ", examples[index]); + composer.Tab().Text(model.GetApplicationName()).Space().Style("grey", args); + composer.LineBreak(); + } + + composer.LineBreak(); + return new[] { composer }; + } + + return Array.Empty(); + } + + private static IEnumerable GetArguments(CommandInfo? command) + { + var arguments = HelpArgument.Get(command); + if (arguments.Count == 0) + { + return Array.Empty(); + } + + var result = new List + { + new Markup("[yellow]ARGUMENTS:[/]"), + new Markup("\n"), + }; + + var grid = new Grid(); + grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); + grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); + + foreach (var argument in arguments) + { + if (argument.Required) + { + grid.AddRow( + $"[silver]<{argument.Name.EscapeMarkup()}>[/]", + argument.Description?.TrimEnd('.') ?? string.Empty); + } + else + { + grid.AddRow( + $"[grey][[{argument.Name.EscapeMarkup()}]][/]", + argument.Description?.TrimEnd('.') ?? string.Empty); + } + } + + grid.AddEmptyRow(); + result.Add(grid); + + return result; + } + + private static IEnumerable GetOptions(CommandModel model, CommandInfo? command) + { + // Collect all options into a single structure. + var parameters = HelpOption.Get(model, command); + if (parameters.Count == 0) + { + return Array.Empty(); + } + + var result = new List + { + new Markup("[yellow]OPTIONS:[/]"), + new Markup("\n"), + }; + + var grid = new Grid(); + grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); + grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); + + static string GetOptionParts(HelpOption option) + { + var builder = new StringBuilder(); + if (option.Short != null) + { + builder.Append('-').Append(option.Short.EscapeMarkup()); + if (option.Long != null) + { + builder.Append(", "); + } + } + else + { + builder.Append(" "); + if (option.Long != null) + { + builder.Append(" "); + } + } + + if (option.Long != null) + { + builder.Append("--").Append(option.Long.EscapeMarkup()); + } + + if (option.Value != null) + { + builder.Append(' '); + if (option.ValueIsOptional ?? false) + { + builder.Append("[grey][[").Append(option.Value.EscapeMarkup()).Append("]][/]"); + } + else + { + builder.Append("[silver]<").Append(option.Value.EscapeMarkup()).Append(">[/]"); + } + } + + return builder.ToString(); + } + + foreach (var option in parameters.ToArray()) + { + grid.AddRow( + GetOptionParts(option), + option.Description?.TrimEnd('.') ?? string.Empty); + } + + grid.AddEmptyRow(); + result.Add(grid); + + return result; + } + + private static IEnumerable GetCommands( + CommandModel model, + ICommandContainer command, + bool isDefaultCommand) + { + var commands = isDefaultCommand ? model.Commands : command.Commands; + commands = commands.Where(x => !x.IsHidden).ToList(); + + if (commands.Count == 0) + { + return Array.Empty(); + } + + var result = new List + { + new Markup("[yellow]COMMANDS:[/]"), + new Markup("\n"), + }; + + var grid = new Grid(); + grid.AddColumn(new GridColumn { Padding = new Padding(4, 4), NoWrap = true }); + grid.AddColumn(new GridColumn { Padding = new Padding(0, 0) }); + + foreach (var child in commands) + { + var arguments = new Composer(); + arguments.Style("silver", child.Name.EscapeMarkup()); + arguments.Space(); + + foreach (var argument in HelpArgument.Get(child).Where(a => a.Required)) + { + arguments.Style("silver", $"<{argument.Name.EscapeMarkup()}>"); + arguments.Space(); + } + + grid.AddRow( + arguments.ToString().TrimEnd(), + child.Description?.TrimEnd('.') ?? string.Empty); + } + + grid.AddEmptyRow(); + result.Add(grid); + + return result; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/IPairDeconstructor.cs b/src/Spectre.Console/Cli/Internal/IPairDeconstructor.cs new file mode 100644 index 0000000..943a5a4 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/IPairDeconstructor.cs @@ -0,0 +1,24 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + /// + /// Represents a pair deconstructor. + /// + internal interface IPairDeconstructor + { + /// + /// Deconstructs the specified value into its components. + /// + /// The type resolver to use. + /// The key type. + /// The value type. + /// The value to deconstruct. + /// A deconstructed value. + (object? Key, object? Value) Deconstruct( + ITypeResolver resolver, + Type keyType, + Type valueType, + string? value); + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs new file mode 100644 index 0000000..7efe1e5 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandArgument.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandArgument : CommandParameter + { + public string Value { get; } + public int Position { get; set; } + + public CommandArgument( + Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, + TypeConverterAttribute? converter, DefaultValueAttribute? defaultValue, + CommandArgumentAttribute argument, IEnumerable validators) + : base(parameterType, parameterKind, property, description, converter, defaultValue, + null, validators, argument.IsRequired) + { + Value = argument.ValueName; + Position = argument.Position; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandContainerExtensions.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandContainerExtensions.cs new file mode 100644 index 0000000..6ca1ef7 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandContainerExtensions.cs @@ -0,0 +1,21 @@ +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandContainerExtensions + { + public static CommandInfo? FindCommand(this ICommandContainer root, string name, CaseSensitivity sensitivity) + { + var result = root.Commands.FirstOrDefault( + c => c.Name.Equals(name, sensitivity.GetStringComparison(CommandPart.CommandName))); + + if (result == null) + { + result = root.Commands.FirstOrDefault( + c => c.Aliases.Contains(name, sensitivity.GetStringComparer(CommandPart.CommandName))); + } + + return result; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs new file mode 100644 index 0000000..2ea39d0 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandInfo.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandInfo : ICommandContainer + { + public string Name { get; } + public HashSet Aliases { get; } + public string? Description { get; } + public object? Data { get; } + public Type? CommandType { get; } + public Type SettingsType { get; } + public Func? Delegate { get; } + public bool IsDefaultCommand { get; } + public bool IsHidden { get; } + public CommandInfo? Parent { get; } + public IList Children { get; } + public IList Parameters { get; } + public IList Examples { get; } + + public bool IsBranch => CommandType == null && Delegate == null; + IList ICommandContainer.Commands => Children; + + public CommandInfo(CommandInfo? parent, ConfiguredCommand prototype) + { + Parent = parent; + + Name = prototype.Name; + Aliases = new HashSet(prototype.Aliases); + Description = prototype.Description; + Data = prototype.Data; + CommandType = prototype.CommandType; + SettingsType = prototype.SettingsType; + Delegate = prototype.Delegate; + IsDefaultCommand = prototype.IsDefaultCommand; + IsHidden = prototype.IsHidden; + + Children = new List(); + Parameters = new List(); + Examples = prototype.Examples ?? new List(); + + if (CommandType != null && string.IsNullOrWhiteSpace(Description)) + { + var description = CommandType.GetCustomAttribute(); + if (description != null) + { + Description = description.Description; + } + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandInfoExtensions.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandInfoExtensions.cs new file mode 100644 index 0000000..ce2c361 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandInfoExtensions.cs @@ -0,0 +1,79 @@ +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandInfoExtensions + { + public static bool HaveParentWithOption(this CommandInfo command, CommandOption option) + { + var parent = command?.Parent; + while (parent != null) + { + foreach (var parentOption in parent.Parameters.OfType()) + { + if (option.HaveSameBackingPropertyAs(parentOption)) + { + return true; + } + } + + parent = parent.Parent; + } + + return false; + } + + public static bool AllowParentOption(this CommandInfo command, CommandOption option) + { + // Got an immediate parent? + if (command?.Parent != null) + { + // Is the current node's settings type the same as the previous one? + if (command.SettingsType == command.Parent.SettingsType) + { + var parameters = command.Parent.Parameters.OfType().ToArray(); + if (parameters.Length > 0) + { + foreach (var parentOption in parameters) + { + // Is this the same one? + if (option.HaveSameBackingPropertyAs(parentOption)) + { + // Is it part of the same settings class. + if (option.Property.DeclaringType == command.SettingsType) + { + // Allow it. + return true; + } + + // Don't allow it. + return false; + } + } + } + } + } + + return false; + } + + public static bool HaveParentWithArgument(this CommandInfo command, CommandArgument argument) + { + var parent = command?.Parent; + while (parent != null) + { + foreach (var parentOption in parent.Parameters.OfType()) + { + if (argument.HaveSameBackingPropertyAs(parentOption)) + { + return true; + } + } + + parent = parent.Parent; + } + + return false; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs new file mode 100644 index 0000000..bb7a55c --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandModel : ICommandContainer + { + public string? ApplicationName { get; } + public ParsingMode ParsingMode { get; } + public CommandInfo? DefaultCommand { get; } + public IList Commands { get; } + public IList Examples { get; } + + public CommandModel( + CommandAppSettings settings, + CommandInfo? defaultCommand, + IEnumerable commands, + IEnumerable examples) + { + ApplicationName = settings.ApplicationName; + ParsingMode = settings.ParsingMode; + DefaultCommand = defaultCommand; + Commands = new List(commands ?? Array.Empty()); + Examples = new List(examples ?? Array.Empty()); + } + + public string GetApplicationName() + { + return ApplicationName ?? Path.GetFileName(Assembly.GetEntryAssembly()?.Location) ?? "?"; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs new file mode 100644 index 0000000..5c7f293 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelBuilder.cs @@ -0,0 +1,249 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandModelBuilder + { + // Consider removing this in favor for value tuples at some point. + private sealed class OrderedProperties + { + public int Level { get; } + public int SortOrder { get; } + public PropertyInfo[] Properties { get; } + + public OrderedProperties(int level, int sortOrder, PropertyInfo[] properties) + { + Level = level; + SortOrder = sortOrder; + Properties = properties; + } + } + + public static CommandModel Build(IConfiguration configuration) + { + var result = new List(); + foreach (var command in configuration.Commands) + { + result.Add(Build(null, command)); + } + + var defaultCommand = default(CommandInfo); + if (configuration.DefaultCommand != null) + { + // Add the examples from the configuration to the default command. + configuration.DefaultCommand.Examples.AddRange(configuration.Examples); + + // Build the default command. + defaultCommand = Build(null, configuration.DefaultCommand); + } + + // Create the command model and validate it. + var model = new CommandModel(configuration.Settings, defaultCommand, result, configuration.Examples); + CommandModelValidator.Validate(model, configuration.Settings); + + return model; + } + + private static CommandInfo Build(CommandInfo? parent, ConfiguredCommand command) + { + var info = new CommandInfo(parent, command); + + foreach (var parameter in GetParameters(info)) + { + info.Parameters.Add(parameter); + } + + foreach (var childCommand in command.Children) + { + var child = Build(info, childCommand); + info.Children.Add(child); + } + + // Normalize argument positions. + var index = 0; + foreach (var argument in info.Parameters.OfType() + .OrderBy(argument => argument.Position)) + { + argument.Position = index; + index++; + } + + return info; + } + + private static IEnumerable GetParameters(CommandInfo command) + { + var result = new List(); + var argumentPosition = 0; + + // We need to get parameters in order of the class where they were defined. + // We assign each inheritance level a value that is used to properly sort the + // arguments when iterating over them. + IEnumerable GetPropertiesInOrder() + { + var current = command.SettingsType; + var level = 0; + var sortOrder = 0; + while (current.BaseType != null) + { + yield return new OrderedProperties(level, sortOrder, current.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public)); + current = current.BaseType; + + // Things get a little bit complicated now. + // Only consider a setting's base type part of the + // setting, if there isn't a parent command that implements + // the setting's base type. This might come back to bite us :) + var currentCommand = command.Parent; + while (currentCommand != null) + { + if (currentCommand.SettingsType == current) + { + level--; + break; + } + + currentCommand = currentCommand.Parent; + } + + sortOrder--; + } + } + + var groups = GetPropertiesInOrder(); + foreach (var group in groups.OrderBy(x => x.Level).ThenBy(x => x.SortOrder)) + { + var parameters = new List(); + + foreach (var property in group.Properties) + { + if (property.IsDefined(typeof(CommandOptionAttribute))) + { + var attribute = property.GetCustomAttribute(); + if (attribute != null) + { + var option = BuildOptionParameter(property, attribute); + + // Any previous command has this option defined? + if (command.HaveParentWithOption(option)) + { + // Do we allow it to exist on this command as well? + if (command.AllowParentOption(option)) + { + option.IsShadowed = true; + parameters.Add(option); + } + } + else + { + // No parent have this option. + parameters.Add(option); + } + } + } + else if (property.IsDefined(typeof(CommandArgumentAttribute))) + { + var attribute = property.GetCustomAttribute(); + if (attribute != null) + { + var argument = BuildArgumentParameter(property, attribute); + + // Any previous command has this argument defined? + // In that case, we should not assign the parameter to this command. + if (!command.HaveParentWithArgument(argument)) + { + parameters.Add(argument); + } + } + } + } + + // Update the position for the parameters. + foreach (var argument in parameters.OfType().OrderBy(x => x.Position)) + { + argument.Position = argumentPosition++; + } + + // Add all parameters to the result. + foreach (var groupResult in parameters) + { + result.Add(groupResult); + } + } + + return result; + } + + private static CommandOption BuildOptionParameter(PropertyInfo property, CommandOptionAttribute attribute) + { + var description = property.GetCustomAttribute(); + var converter = property.GetCustomAttribute(); + var deconstructor = property.GetCustomAttribute(); + var validators = property.GetCustomAttributes(true); + var defaultValue = property.GetCustomAttribute(); + + var kind = GetOptionKind(property.PropertyType, attribute, deconstructor, converter); + + if (defaultValue == null && property.PropertyType == typeof(bool)) + { + defaultValue = new DefaultValueAttribute(false); + } + + return new CommandOption(property.PropertyType, kind, + property, description?.Description, converter, deconstructor, + attribute, validators, defaultValue, attribute.ValueIsOptional); + } + + private static CommandArgument BuildArgumentParameter(PropertyInfo property, CommandArgumentAttribute attribute) + { + var description = property.GetCustomAttribute(); + var converter = property.GetCustomAttribute(); + var defaultValue = property.GetCustomAttribute(); + var validators = property.GetCustomAttributes(true); + + var kind = GetParameterKind(property.PropertyType); + + return new CommandArgument( + property.PropertyType, kind, property, + description?.Description, converter, + defaultValue, attribute, validators); + } + + private static ParameterKind GetOptionKind( + Type type, + CommandOptionAttribute attribute, + PairDeconstructorAttribute? deconstructor, + TypeConverterAttribute? converter) + { + if (attribute.ValueIsOptional) + { + return ParameterKind.FlagWithValue; + } + + if (type.IsPairDeconstructable() && (deconstructor != null || converter == null)) + { + return ParameterKind.Pair; + } + + return GetParameterKind(type); + } + + private static ParameterKind GetParameterKind(Type type) + { + if (type == typeof(bool)) + { + return ParameterKind.Flag; + } + + if (type.IsArray) + { + return ParameterKind.Vector; + } + + return ParameterKind.Scalar; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandModelValidator.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelValidator.cs new file mode 100644 index 0000000..04ba058 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandModelValidator.cs @@ -0,0 +1,191 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandModelValidator + { + public static void Validate(CommandModel model, CommandAppSettings settings) + { + if (model is null) + { + throw new ArgumentNullException(nameof(model)); + } + + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + if (model.Commands.Count == 0 && model.DefaultCommand == null) + { + throw CommandConfigurationException.NoCommandConfigured(); + } + + foreach (var command in model.Commands) + { + // Alias collision? + foreach (var alias in command.Aliases) + { + if (model.Commands.Any(x => x.Name.Equals(alias, StringComparison.OrdinalIgnoreCase))) + { + throw CommandConfigurationException.CommandNameConflict(command, alias); + } + } + } + + Validate(model.DefaultCommand); + foreach (var command in model.Commands) + { + Validate(command); + } + + if (settings.ValidateExamples) + { + ValidateExamples(model, settings); + } + } + + private static void Validate(CommandInfo? command) + { + if (command == null) + { + return; + } + + // Get duplicate options for command. + var duplicateOptions = GetDuplicates(command); + if (duplicateOptions.Length > 0) + { + throw CommandConfigurationException.DuplicateOption(command, duplicateOptions); + } + + // No children? + if (command.IsBranch && command.Children.Count == 0) + { + throw CommandConfigurationException.BranchHasNoChildren(command); + } + + // Multiple vector arguments? + var arguments = command.Parameters.OfType(); + if (arguments.Any(x => x.ParameterKind == ParameterKind.Vector)) + { + // Multiple vector arguments for command? + if (arguments.Count(x => x.ParameterKind == ParameterKind.Vector) > 1) + { + throw CommandConfigurationException.TooManyVectorArguments(command); + } + + // Make sure that vector arguments are specified last. + if (arguments.Last().ParameterKind != ParameterKind.Vector) + { + throw CommandConfigurationException.VectorArgumentNotSpecifiedLast(command); + } + } + + // Arguments + var argumnets = command.Parameters.OfType(); + foreach (var argument in arguments) + { + if (argument.Required && argument.DefaultValue != null) + { + throw CommandConfigurationException.RequiredArgumentsCannotHaveDefaultValue(argument); + } + } + + // Options + var options = command.Parameters.OfType(); + foreach (var option in options) + { + // Pair deconstructable? + if (option.Property.PropertyType.IsPairDeconstructable()) + { + if (option.PairDeconstructor != null && option.Converter != null) + { + throw CommandConfigurationException.OptionBothHasPairDeconstructorAndTypeParameter(option); + } + } + else if (option.PairDeconstructor != null) + { + throw CommandConfigurationException.OptionTypeDoesNotSupportDeconstruction(option); + } + + // Optional options that are not flags? + if (option.ParameterKind == ParameterKind.FlagWithValue && !option.IsFlagValue()) + { + throw CommandConfigurationException.OptionalOptionValueMustBeFlagWithValue(option); + } + } + + // Validate child commands. + foreach (var childCommand in command.Children) + { + Validate(childCommand); + } + } + + private static void ValidateExamples(CommandModel model, CommandAppSettings settings) + { + var examples = new List(); + examples.AddRangeIfNotNull(model.Examples); + + // Get all examples. + var queue = new Queue(new[] { model }); + while (queue.Count > 0) + { + var current = queue.Dequeue(); + + foreach (var command in current.Commands) + { + examples.AddRangeIfNotNull(command.Examples); + queue.Enqueue(command); + } + } + + // Validate all examples. + foreach (var example in examples) + { + try + { + var parser = new CommandTreeParser(model, settings, ParsingMode.Strict); + parser.Parse(example); + } + catch (Exception ex) + { + throw new CommandConfigurationException("Validation of examples failed.", ex); + } + } + } + + private static string[] GetDuplicates(CommandInfo command) + { + var result = new Dictionary(StringComparer.Ordinal); + + void AddToResult(IEnumerable keys) + { + foreach (var key in keys) + { + if (!string.IsNullOrWhiteSpace(key)) + { + if (!result.ContainsKey(key)) + { + result.Add(key, 0); + } + + result[key]++; + } + } + } + + foreach (var option in command.Parameters.OfType()) + { + AddToResult(option.ShortNames); + AddToResult(option.LongNames); + } + + return result.Where(x => x.Value > 1) + .Select(x => x.Key).ToArray(); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs new file mode 100644 index 0000000..ec50c0f --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandOption.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandOption : CommandParameter + { + public IReadOnlyList LongNames { get; } + public IReadOnlyList ShortNames { get; } + public string? ValueName { get; } + public bool ValueIsOptional { get; } + public bool IsShadowed { get; set; } + + public CommandOption( + Type parameterType, ParameterKind parameterKind, PropertyInfo property, string? description, + TypeConverterAttribute? converter, PairDeconstructorAttribute? deconstructor, + CommandOptionAttribute optionAttribute, IEnumerable validators, + DefaultValueAttribute? defaultValue, bool valueIsOptional) + : base(parameterType, parameterKind, property, description, converter, + defaultValue, deconstructor, validators, false) + { + LongNames = optionAttribute.LongNames; + ShortNames = optionAttribute.ShortNames; + ValueName = optionAttribute.ValueName; + ValueIsOptional = valueIsOptional; + } + + public string GetOptionName() + { + return LongNames.Count > 0 ? LongNames[0] : ShortNames[0]; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs new file mode 100644 index 0000000..f423c67 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameter.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal abstract class CommandParameter : ICommandParameterInfo + { + public Guid Id { get; } + public Type ParameterType { get; } + public ParameterKind ParameterKind { get; } + public PropertyInfo Property { get; } + public string? Description { get; } + public DefaultValueAttribute? DefaultValue { get; } + public TypeConverterAttribute? Converter { get; } + public PairDeconstructorAttribute? PairDeconstructor { get; } + public List Validators { get; } + public bool Required { get; set; } + public string PropertyName => Property.Name; + + public virtual bool WantRawValue => ParameterType.IsPairDeconstructable() + && (PairDeconstructor != null || Converter == null); + + protected CommandParameter( + Type parameterType, ParameterKind parameterKind, PropertyInfo property, + string? description, TypeConverterAttribute? converter, + DefaultValueAttribute? defaultValue, + PairDeconstructorAttribute? deconstuctor, + IEnumerable validators, bool required) + { + Id = Guid.NewGuid(); + ParameterType = parameterType; + ParameterKind = parameterKind; + Property = property; + Description = description; + Converter = converter; + DefaultValue = defaultValue; + PairDeconstructor = deconstuctor; + Validators = new List(validators ?? Array.Empty()); + Required = required; + } + + public bool IsFlagValue() + { + return ParameterType.GetInterfaces().Any(i => i == typeof(IFlagValue)); + } + + public bool HaveSameBackingPropertyAs(CommandParameter other) + { + return CommandParameterComparer.ByBackingProperty.Equals(this, other); + } + + public void Assign(CommandSettings settings, ITypeResolver resolver, object? value) + { + // Is the property pair deconstructable? + // TODO: This needs to be better defined + if (Property.PropertyType.IsPairDeconstructable() && WantRawValue) + { + var genericTypes = Property.PropertyType.GetGenericArguments(); + + var multimap = (IMultiMap?)Property.GetValue(settings); + if (multimap == null) + { + multimap = Activator.CreateInstance(typeof(MultiMap<,>).MakeGenericType(genericTypes[0], genericTypes[1])) as IMultiMap; + if (multimap == null) + { + throw new InvalidOperationException("Could not create multimap"); + } + } + + // Create deconstructor. + var deconstructorType = PairDeconstructor?.Type ?? typeof(DefaultPairDeconstructor); + if (!(resolver.Resolve(deconstructorType) is IPairDeconstructor deconstructor)) + { + if (!(Activator.CreateInstance(deconstructorType) is IPairDeconstructor activatedDeconstructor)) + { + throw new InvalidOperationException($"Could not create pair deconstructor."); + } + + deconstructor = activatedDeconstructor; + } + + // Deconstruct and add to multimap. + var pair = deconstructor.Deconstruct(resolver, genericTypes[0], genericTypes[1], value as string); + if (pair.Key != null) + { + multimap.Add(pair); + } + + value = multimap; + } + else if (Property.PropertyType.IsArray) + { + // Add a new item to the array + var array = (Array?)Property.GetValue(settings); + Array newArray; + + var elementType = Property.PropertyType.GetElementType(); + if (elementType == null) + { + throw new InvalidOperationException("Could not get property type."); + } + + if (array == null) + { + newArray = Array.CreateInstance(elementType, 1); + } + else + { + newArray = Array.CreateInstance(elementType, array.Length + 1); + array.CopyTo(newArray, 0); + } + + newArray.SetValue(value, newArray.Length - 1); + value = newArray; + } + else if (IsFlagValue()) + { + var flagValue = (IFlagValue?)Property.GetValue(settings); + if (flagValue == null) + { + flagValue = (IFlagValue?)Activator.CreateInstance(ParameterType); + if (flagValue == null) + { + throw new InvalidOperationException("Could not create flag value."); + } + } + + if (value != null) + { + // Null means set, but not with a valid value. + flagValue.Value = value; + } + + // If the parameter was mapped, then it's set. + flagValue.IsSet = true; + + value = flagValue; + } + + Property.SetValue(settings, value); + } + + public object? Get(CommandSettings settings) + { + return Property.GetValue(settings); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Modelling/CommandParameterComparer.cs b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameterComparer.cs new file mode 100644 index 0000000..827f1d2 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/CommandParameterComparer.cs @@ -0,0 +1,32 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandParameterComparer + { + public static readonly ByBackingPropertyComparer ByBackingProperty = new ByBackingPropertyComparer(); + + public sealed class ByBackingPropertyComparer : IEqualityComparer + { + public bool Equals(CommandParameter? x, CommandParameter? y) + { + if (x is null || y is null) + { + return false; + } + + if (ReferenceEquals(x, y)) + { + return true; + } + + return x.Property.MetadataToken == y.Property.MetadataToken; + } + + public int GetHashCode(CommandParameter? obj) + { + return obj?.Property?.MetadataToken.GetHashCode() ?? 0; + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/ICommandContainer.cs b/src/Spectre.Console/Cli/Internal/Modelling/ICommandContainer.cs new file mode 100644 index 0000000..aa64ee3 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/ICommandContainer.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + /// + /// Represents a command container. + /// + internal interface ICommandContainer + { + /// + /// Gets all commands in the container. + /// + IList Commands { get; } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs b/src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs new file mode 100644 index 0000000..b3ed8b0 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Modelling/ParameterKind.cs @@ -0,0 +1,22 @@ +using System.ComponentModel; + +namespace Spectre.Console.Cli.Internal +{ + internal enum ParameterKind + { + [Description("flag")] + Flag = 0, + + [Description("scalar")] + Scalar = 1, + + [Description("vector")] + Vector = 2, + + [Description("flagvalue")] + FlagWithValue = 3, + + [Description("pair")] + Pair = 4, + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTree.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTree.cs new file mode 100644 index 0000000..c7f554a --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTree.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandTree + { + public CommandInfo Command { get; } + public List Mapped { get; } + public List Unmapped { get; } + public CommandTree? Parent { get; } + public CommandTree? Next { get; set; } + public bool ShowHelp { get; set; } + + public CommandTree(CommandTree? parent, CommandInfo command) + { + Parent = parent; + Command = command; + Mapped = new List(); + Unmapped = new List(); + } + + public ICommand CreateCommand(ITypeResolver resolver) + { + if (Command.Delegate != null) + { + return new DelegateCommand(Command.Delegate); + } + + if (resolver.Resolve(Command.CommandType) is ICommand command) + { + return command; + } + + throw CommandParseException.CouldNotCreateCommand(Command.CommandType); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeExtensions.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeExtensions.cs new file mode 100644 index 0000000..e349f91 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeExtensions.cs @@ -0,0 +1,70 @@ +using System; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandTreeExtensions + { + public static CommandTree? GetRootCommand(this CommandTree node) + { + while (node.Parent != null) + { + node = node.Parent; + } + + return node; + } + + public static CommandTree GetLeafCommand(this CommandTree node) + { + while (node.Next != null) + { + node = node.Next; + } + + return node; + } + + public static bool HasArguments(this CommandTree tree) + { + return tree.Command.Parameters.OfType().Any(); + } + + public static CommandArgument? FindArgument(this CommandTree tree, int position) + { + return tree.Command.Parameters + .OfType() + .FirstOrDefault(c => c.Position == position); + } + + public static CommandOption? FindOption(this CommandTree tree, string name, bool longOption, CaseSensitivity sensitivity) + { + return tree.Command.Parameters + .OfType() + .FirstOrDefault(o => longOption + ? o.LongNames.Contains(name, sensitivity.GetStringComparer(CommandPart.LongOption)) + : o.ShortNames.Contains(name, StringComparer.Ordinal)); + } + + public static bool IsOptionMappedWithParent(this CommandTree tree, string name, bool longOption) + { + var node = tree.Parent; + while (node != null) + { + var option = node.Command?.Parameters.OfType() + .FirstOrDefault(o => longOption + ? o.LongNames.Contains(name, StringComparer.Ordinal) + : o.ShortNames.Contains(name, StringComparer.Ordinal)); + + if (option != null) + { + return node.Mapped.Any(p => p.Parameter == option); + } + + node = node.Parent; + } + + return false; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParser.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParser.cs new file mode 100644 index 0000000..974d80d --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParser.cs @@ -0,0 +1,388 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal class CommandTreeParser + { + private readonly CommandModel _configuration; + private readonly ParsingMode _parsingMode; + private readonly CommandOptionAttribute _help; + + public CaseSensitivity CaseSensitivity { get; } + + public enum State + { + Normal = 0, + Remaining = 1, + } + + public CommandTreeParser(CommandModel configuration, ICommandAppSettings settings, ParsingMode? parsingMode = null) + { + if (settings is null) + { + throw new ArgumentNullException(nameof(settings)); + } + + _configuration = configuration; + _parsingMode = parsingMode ?? _configuration.ParsingMode; + _help = new CommandOptionAttribute("-h|--help"); + + CaseSensitivity = settings.CaseSensitivity; + } + + public CommandTreeParserResult Parse(IEnumerable args) + { + var context = new CommandTreeParserContext(args, _parsingMode); + + var tokenizerResult = CommandTreeTokenizer.Tokenize(context.Arguments); + var tokens = tokenizerResult.Tokens; + var rawRemaining = tokenizerResult.Remaining; + + var result = default(CommandTree); + if (tokens.Count > 0) + { + // Not a command? + var token = tokens.Current; + if (token == null) + { + // Should not happen, but the compiler isn't + // smart enough to realize this... + throw new CommandRuntimeException("Could not get current token."); + } + + if (token.TokenKind != CommandTreeToken.Kind.String) + { + // Got a default command? + if (_configuration.DefaultCommand != null) + { + result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens); + return new CommandTreeParserResult( + result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining)); + } + + // Show help? + if (_help?.IsMatch(token.Value) == true) + { + return new CommandTreeParserResult( + null, new RemainingArguments(context.GetRemainingArguments(), rawRemaining)); + } + + // Unexpected option. + throw CommandParseException.UnexpectedOption(context.Arguments, token); + } + + // Does the token value match a command? + var command = _configuration.FindCommand(token.Value, CaseSensitivity); + if (command == null) + { + if (_configuration.DefaultCommand != null) + { + result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens); + return new CommandTreeParserResult( + result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining)); + } + } + + // Parse the command. + result = ParseCommand(context, _configuration, null, tokens); + } + else + { + // Is there a default command? + if (_configuration.DefaultCommand != null) + { + result = ParseCommandParameters(context, _configuration.DefaultCommand, null, tokens); + } + } + + return new CommandTreeParserResult( + result, new RemainingArguments(context.GetRemainingArguments(), rawRemaining)); + } + + private CommandTree ParseCommand( + CommandTreeParserContext context, + ICommandContainer current, + CommandTree? parent, + CommandTreeTokenStream stream) + { + // Find the command. + var commandToken = stream.Consume(CommandTreeToken.Kind.String); + if (commandToken == null) + { + throw new CommandRuntimeException("Could not consume token when parsing command."); + } + + var command = current.FindCommand(commandToken.Value, CaseSensitivity); + if (command == null) + { + throw CommandParseException.UnknownCommand(_configuration, parent, context.Arguments, commandToken); + } + + return ParseCommandParameters(context, command, parent, stream); + } + + private CommandTree ParseCommandParameters( + CommandTreeParserContext context, + CommandInfo command, + CommandTree? parent, + CommandTreeTokenStream stream) + { + context.ResetArgumentPosition(); + + var node = new CommandTree(parent, command); + while (stream.Peek() != null) + { + var token = stream.Peek(); + if (token == null) + { + // Should not happen, but the compiler isn't + // smart enough to realize this... + throw new CommandRuntimeException("Could not get the next token."); + } + + switch (token.TokenKind) + { + case CommandTreeToken.Kind.LongOption: + // Long option + ParseOption(context, stream, token, node, true); + break; + case CommandTreeToken.Kind.ShortOption: + // Short option + ParseOption(context, stream, token, node, false); + break; + case CommandTreeToken.Kind.String: + // Command + ParseString(context, stream, node); + break; + case CommandTreeToken.Kind.Remaining: + // Remaining + stream.Consume(CommandTreeToken.Kind.Remaining); + context.State = State.Remaining; + break; + default: + throw new InvalidOperationException($"Encountered unknown token ({token.TokenKind})."); + } + } + + // Add unmapped parameters. + foreach (var parameter in node.Command.Parameters) + { + if (node.Mapped.All(m => m.Parameter != parameter)) + { + node.Unmapped.Add(parameter); + } + } + + return node; + } + + private void ParseString( + CommandTreeParserContext context, + CommandTreeTokenStream stream, + CommandTree node) + { + if (context.State == State.Remaining) + { + stream.Consume(CommandTreeToken.Kind.String); + return; + } + + var token = stream.Expect(CommandTreeToken.Kind.String); + + // Command? + var command = node.Command.FindCommand(token.Value, CaseSensitivity); + if (command != null) + { + if (context.State == State.Normal) + { + node.Next = ParseCommand(context, node.Command, node, stream); + } + + return; + } + + // Current command has no arguments? + if (!node.HasArguments()) + { + throw CommandParseException.UnknownCommand(_configuration, node, context.Arguments, token); + } + + // Argument? + var parameter = node.FindArgument(context.CurrentArgumentPosition); + if (parameter == null) + { + // No parameters left. Any commands after this? + if (node.Command.Children.Count > 0 || node.Command.IsDefaultCommand) + { + throw CommandParseException.UnknownCommand(_configuration, node, context.Arguments, token); + } + + throw CommandParseException.CouldNotMatchArgument(context.Arguments, token); + } + + // Yes, this was an argument. + if (parameter.ParameterKind == ParameterKind.Vector) + { + // Vector + var current = stream.Current; + while (current?.TokenKind == CommandTreeToken.Kind.String) + { + var value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + node.Mapped.Add(new MappedCommandParameter(parameter, value)); + current = stream.Current; + } + } + else + { + // Scalar + var value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + node.Mapped.Add(new MappedCommandParameter(parameter, value)); + context.IncreaseArgumentPosition(); + } + } + + private void ParseOption( + CommandTreeParserContext context, + CommandTreeTokenStream stream, + CommandTreeToken token, + CommandTree node, + bool isLongOption) + { + // Consume the option token. + stream.Consume(isLongOption ? CommandTreeToken.Kind.LongOption : CommandTreeToken.Kind.ShortOption); + + if (context.State == State.Normal) + { + // Find the option. + var option = node.FindOption(token.Value, isLongOption, CaseSensitivity); + if (option != null) + { + node.Mapped.Add(new MappedCommandParameter( + option, ParseOptionValue(context, stream, token, node, option))); + + return; + } + + // Help? + if (_help?.IsMatch(token.Value) == true) + { + node.ShowHelp = true; + return; + } + } + + if (context.State == State.Remaining) + { + ParseOptionValue(context, stream, token, node, null); + return; + } + + if (context.ParsingMode == ParsingMode.Strict) + { + throw CommandParseException.UnknownOption(context.Arguments, token); + } + else + { + ParseOptionValue(context, stream, token, node, null); + } + } + + private string? ParseOptionValue( + CommandTreeParserContext context, + CommandTreeTokenStream stream, + CommandTreeToken token, + CommandTree current, + CommandParameter? parameter) + { + var value = default(string); + + // Parse the value of the token (if any). + var valueToken = stream.Peek(); + if (valueToken?.TokenKind == CommandTreeToken.Kind.String) + { + var parseValue = true; + if (token.TokenKind == CommandTreeToken.Kind.ShortOption && token.IsGrouped) + { + parseValue = false; + } + + if (context.State == State.Normal && parseValue) + { + // Is this a command? + if (current.Command.FindCommand(valueToken.Value, CaseSensitivity) == null) + { + if (parameter != null) + { + if (parameter.ParameterKind == ParameterKind.Flag) + { + if (!Constants.AcceptedBooleanValues.Contains(valueToken.Value, StringComparer.OrdinalIgnoreCase)) + { + // Flags cannot be assigned a value. + throw CommandParseException.CannotAssignValueToFlag(context.Arguments, token); + } + } + + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + } + else + { + // Unknown parameter value. + value = stream.Consume(CommandTreeToken.Kind.String)?.Value; + + // In relaxed parsing mode? + if (context.ParsingMode == ParsingMode.Relaxed) + { + context.AddRemainingArgument(token.Value, value); + } + } + } + } + else + { + context.AddRemainingArgument(token.Value, parseValue ? valueToken.Value : null); + } + } + else + { + if (context.State == State.Remaining || context.ParsingMode == ParsingMode.Relaxed) + { + context.AddRemainingArgument(token.Value, null); + } + } + + // No value? + if (context.State == State.Normal) + { + if (value == null && parameter != null) + { + if (parameter.ParameterKind == ParameterKind.Flag) + { + value = "true"; + } + else + { + if (parameter is CommandOption option) + { + if (parameter.IsFlagValue()) + { + return null; + } + + throw CommandParseException.OptionHasNoValue(context.Arguments, token, option); + } + else + { + // This should not happen at all. If it does, it's because we've added a new + // option type which isn't a CommandOption for some reason. + throw new InvalidOperationException($"Found invalid parameter type '{parameter.GetType().FullName}'."); + } + } + } + } + + return value; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserContext.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserContext.cs new file mode 100644 index 0000000..1c98bd3 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserContext.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal class CommandTreeParserContext + { + private readonly List _args; + private readonly Dictionary> _remaining; + + public IReadOnlyList Arguments => _args; + public int CurrentArgumentPosition { get; private set; } + public CommandTreeParser.State State { get; set; } + public ParsingMode ParsingMode { get; } + + public CommandTreeParserContext(IEnumerable args, ParsingMode parsingMode) + { + _args = new List(args); + _remaining = new Dictionary>(StringComparer.Ordinal); + + ParsingMode = parsingMode; + } + + public void ResetArgumentPosition() + { + CurrentArgumentPosition = 0; + } + + public void IncreaseArgumentPosition() + { + CurrentArgumentPosition++; + } + + public void AddRemainingArgument(string key, string? value) + { + if (State == CommandTreeParser.State.Remaining || ParsingMode == ParsingMode.Relaxed) + { + if (!_remaining.ContainsKey(key)) + { + _remaining.Add(key, new List()); + } + + _remaining[key].Add(value); + } + } + + [SuppressMessage("Style", "IDE0004:Remove Unnecessary Cast", Justification = "Bug in analyzer?")] + public ILookup GetRemainingArguments() + { + return _remaining + .SelectMany(pair => pair.Value, (pair, value) => new { pair.Key, value }) + .ToLookup(pair => pair.Key, pair => (string?)pair.value); + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserResult.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserResult.cs new file mode 100644 index 0000000..b3132b1 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeParserResult.cs @@ -0,0 +1,15 @@ +namespace Spectre.Console.Cli.Internal +{ + // Consider removing this in favor for value tuples at some point. + internal sealed class CommandTreeParserResult + { + public CommandTree? Tree { get; } + public IRemainingArguments Remaining { get; } + + public CommandTreeParserResult(CommandTree? tree, IRemainingArguments remaining) + { + Tree = tree; + Remaining = remaining; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeToken.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeToken.cs new file mode 100644 index 0000000..a65d603 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeToken.cs @@ -0,0 +1,27 @@ +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandTreeToken + { + public Kind TokenKind { get; } + public int Position { get; } + public string Value { get; } + public string Representation { get; } + public bool IsGrouped { get; set; } + + public enum Kind + { + String, + LongOption, + ShortOption, + Remaining, + } + + public CommandTreeToken(Kind kind, int position, string value, string representation) + { + TokenKind = kind; + Position = position; + Value = value; + Representation = representation; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenStream.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenStream.cs new file mode 100644 index 0000000..5521d85 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenStream.cs @@ -0,0 +1,90 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandTreeTokenStream : IReadOnlyList + { + private readonly List _tokens; + private int _position; + + public int Count => _tokens.Count; + + public CommandTreeToken this[int index] => _tokens[index]; + + public CommandTreeToken? Current + { + get + { + if (_position >= Count) + { + return null; + } + + return _tokens[_position]; + } + } + + public CommandTreeTokenStream(IEnumerable tokens) + { + _tokens = new List(tokens ?? Enumerable.Empty()); + _position = 0; + } + + public CommandTreeToken? Peek(int index = 0) + { + var position = _position + index; + if (position >= Count) + { + return null; + } + + return _tokens[position]; + } + + public CommandTreeToken? Consume() + { + if (_position >= Count) + { + return null; + } + + var token = _tokens[_position]; + _position++; + return token; + } + + public CommandTreeToken? Consume(CommandTreeToken.Kind type) + { + Expect(type); + return Consume(); + } + + public CommandTreeToken Expect(CommandTreeToken.Kind expected) + { + if (Current == null) + { + throw CommandParseException.ExpectedTokenButFoundNull(expected); + } + + var found = Current.TokenKind; + if (expected != found) + { + throw CommandParseException.ExpectedTokenButFoundOther(expected, found); + } + + return Current; + } + + public IEnumerator GetEnumerator() + { + return _tokens.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizer.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizer.cs new file mode 100644 index 0000000..bce85c8 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizer.cs @@ -0,0 +1,289 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; + +namespace Spectre.Console.Cli.Internal +{ + internal static class CommandTreeTokenizer + { + public enum Mode + { + Normal = 0, + Remaining = 1, + } + + // Consider removing this in favor for value tuples at some point. + public sealed class CommandTreeTokenizerResult + { + public CommandTreeTokenStream Tokens { get; } + public IReadOnlyList Remaining { get; } + + public CommandTreeTokenizerResult(CommandTreeTokenStream tokens, IReadOnlyList remaining) + { + Tokens = tokens; + Remaining = remaining; + } + } + + public static CommandTreeTokenizerResult Tokenize(IEnumerable args) + { + var tokens = new List(); + var position = 0; + var previousReader = default(TextBuffer); + var context = new CommandTreeTokenizerContext(); + + foreach (var arg in args) + { + var start = position; + var reader = new TextBuffer(previousReader, arg); + + // Parse the token. + position = ParseToken(context, reader, position, start, tokens); + context.FlushRemaining(); + + previousReader = reader; + } + + previousReader?.Dispose(); + + return new CommandTreeTokenizerResult( + new CommandTreeTokenStream(tokens), + context.Remaining); + } + + private static int ParseToken(CommandTreeTokenizerContext context, TextBuffer reader, int position, int start, List tokens) + { + while (reader.Peek() != -1) + { + if (reader.ReachedEnd) + { + position += reader.Position - start; + break; + } + + var character = reader.Peek(); + if (!char.IsWhiteSpace(character)) + { + if (character == '-') + { + tokens.AddRange(ScanOptions(context, reader)); + } + else + { + tokens.Add(ScanString(context, reader)); + } + } + } + + return position; + } + + private static CommandTreeToken ScanString( + CommandTreeTokenizerContext context, + TextBuffer reader, + char[]? stop = null) + { + if (reader.TryPeek(out var character)) + { + // Is this a quoted string? + if (character == '\"') + { + return ScanQuotedString(context, reader); + } + } + + var position = reader.Position; + var builder = new StringBuilder(); + while (!reader.ReachedEnd) + { + var current = reader.Peek(); + if (stop?.Contains(current) ?? false) + { + break; + } + + reader.Read(); // Consume + context.AddRemaining(current); + builder.Append(current); + } + + var value = builder.ToString(); + return new CommandTreeToken(CommandTreeToken.Kind.String, position, value.Trim(), value); + } + + private static CommandTreeToken ScanQuotedString(CommandTreeTokenizerContext context, TextBuffer reader) + { + var position = reader.Position; + + reader.Consume('\"'); + context.AddRemaining('\"'); + + var builder = new StringBuilder(); + while (!reader.ReachedEnd) + { + var character = reader.Peek(); + if (character == '\"') + { + break; + } + + context.AddRemaining(character); + builder.Append(reader.Read()); + } + + if (reader.Peek() != '\"') + { + var unterminatedQuote = builder.ToString(); + var token = new CommandTreeToken(CommandTreeToken.Kind.String, position, unterminatedQuote, $"\"{unterminatedQuote}"); + throw CommandParseException.UnterminatedQuote(reader.Original, token); + } + + reader.Read(); + context.AddRemaining('\"'); + + var value = builder.ToString(); + return new CommandTreeToken(CommandTreeToken.Kind.String, position, value, $"\"{value}\""); + } + + private static IEnumerable ScanOptions(CommandTreeTokenizerContext context, TextBuffer reader) + { + var result = new List(); + + var position = reader.Position; + + reader.Consume('-'); + context.AddRemaining('-'); + + if (!reader.TryPeek(out var character)) + { + var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, "-", "-"); + throw CommandParseException.OptionHasNoName(reader.Original, token); + } + + switch (character) + { + case '-': + var option = ScanLongOption(context, reader, position); + if (option != null) + { + result.Add(option); + } + + break; + default: + result.AddRange(ScanShortOptions(context, reader, position)); + break; + } + + if (reader.TryPeek(out character)) + { + // Encountered a separator? + if (character == '=' || character == ':') + { + reader.Read(); // Consume + context.AddRemaining(character); + + if (!reader.TryPeek(out _)) + { + var token = new CommandTreeToken(CommandTreeToken.Kind.String, reader.Position, "=", "="); + throw CommandParseException.OptionValueWasExpected(reader.Original, token); + } + + result.Add(ScanString(context, reader)); + } + } + + return result; + } + + private static IEnumerable ScanShortOptions(CommandTreeTokenizerContext context, TextBuffer reader, int position) + { + var result = new List(); + while (!reader.ReachedEnd) + { + var current = reader.Peek(); + if (char.IsWhiteSpace(current)) + { + break; + } + + // Encountered a separator? + if (current == '=' || current == ':') + { + break; + } + + if (char.IsLetter(current)) + { + context.AddRemaining(current); + reader.Read(); // Consume + + var value = current.ToString(CultureInfo.InvariantCulture); + result.Add(result.Count == 0 + ? new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position, value, $"-{value}") + : new CommandTreeToken(CommandTreeToken.Kind.ShortOption, position + result.Count, value, value)); + } + else + { + // Create a token representing the short option. + var tokenPosition = position + 1 + result.Count; + var represntation = current.ToString(CultureInfo.InvariantCulture); + var token = new CommandTreeToken(CommandTreeToken.Kind.ShortOption, tokenPosition, represntation, represntation); + throw CommandParseException.InvalidShortOptionName(reader.Original, token); + } + } + + if (result.Count > 1) + { + foreach (var item in result) + { + item.IsGrouped = true; + } + } + + return result; + } + + private static CommandTreeToken ScanLongOption(CommandTreeTokenizerContext context, TextBuffer reader, int position) + { + reader.Consume('-'); + context.AddRemaining('-'); + + if (reader.ReachedEnd) + { + // Rest of the arguments are remaining ones. + context.Mode = Mode.Remaining; + return new CommandTreeToken(CommandTreeToken.Kind.Remaining, position, "--", "--"); + } + + var name = ScanString(context, reader, new[] { '=', ':' }); + + // Perform validation of the name. + if (name.Value.Length == 0) + { + throw CommandParseException.LongOptionNameIsMissing(reader, position); + } + + if (name.Value.Length == 1) + { + throw CommandParseException.LongOptionNameIsOneCharacter(reader, position, name.Value); + } + + if (char.IsDigit(name.Value[0])) + { + throw CommandParseException.LongOptionNameStartWithDigit(reader, position, name.Value); + } + + for (var index = 0; index < name.Value.Length; index++) + { + if (!char.IsLetterOrDigit(name.Value[index]) && name.Value[index] != '-' && name.Value[index] != '_') + { + throw CommandParseException.LongOptionNameContainSymbol(reader, position + 2 + index, name.Value[index]); + } + } + + return new CommandTreeToken(CommandTreeToken.Kind.LongOption, position, name.Value, $"--{name.Value}"); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizerContext.cs b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizerContext.cs new file mode 100644 index 0000000..8210f59 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/CommandTreeTokenizerContext.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Text; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class CommandTreeTokenizerContext + { + private readonly StringBuilder _builder; + private readonly List _remaining; + + public CommandTreeTokenizer.Mode Mode { get; set; } + public IReadOnlyList Remaining => _remaining; + + public CommandTreeTokenizerContext() + { + _builder = new StringBuilder(); + _remaining = new List(); + } + + public void AddRemaining(char character) + { + if (Mode == CommandTreeTokenizer.Mode.Remaining) + { + if (char.IsWhiteSpace(character)) + { + FlushRemaining(); + } + else + { + _builder.Append(character); + } + } + } + + public void FlushRemaining() + { + if (Mode == CommandTreeTokenizer.Mode.Remaining) + { + if (_builder.Length > 0) + { + _remaining.Add(_builder.ToString()); + _builder.Clear(); + } + } + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/Parsing/MappedCommandParameter.cs b/src/Spectre.Console/Cli/Internal/Parsing/MappedCommandParameter.cs new file mode 100644 index 0000000..51a6d6d --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/Parsing/MappedCommandParameter.cs @@ -0,0 +1,15 @@ +namespace Spectre.Console.Cli.Internal +{ + // Consider removing this in favor for value tuples at some point. + internal sealed class MappedCommandParameter + { + public CommandParameter Parameter { get; } + public string? Value { get; } + + public MappedCommandParameter(CommandParameter parameter, string? value) + { + Parameter = parameter; + Value = value; + } + } +} \ No newline at end of file diff --git a/src/Spectre.Console/Cli/Internal/ParsingMode.cs b/src/Spectre.Console/Cli/Internal/ParsingMode.cs new file mode 100644 index 0000000..83e6fe8 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/ParsingMode.cs @@ -0,0 +1,8 @@ +namespace Spectre.Console.Cli.Internal +{ + internal enum ParsingMode + { + Relaxed = 0, + Strict = 1, + } +} diff --git a/src/Spectre.Console/Cli/Internal/RemainingArguments.cs b/src/Spectre.Console/Cli/Internal/RemainingArguments.cs new file mode 100644 index 0000000..c5951db --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/RemainingArguments.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; +using System.Linq; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class RemainingArguments : IRemainingArguments + { + public IReadOnlyList Raw { get; } + public ILookup Parsed { get; } + + public RemainingArguments( + ILookup remaining, + IReadOnlyList raw) + { + Parsed = remaining; + Raw = raw; + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/StringWriterWithEncoding.cs b/src/Spectre.Console/Cli/Internal/StringWriterWithEncoding.cs new file mode 100644 index 0000000..89efbb1 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/StringWriterWithEncoding.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using System.Text; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class StringWriterWithEncoding : StringWriter + { + public override Encoding Encoding { get; } + + public StringWriterWithEncoding(Encoding encoding) + { + Encoding = encoding ?? throw new ArgumentNullException(nameof(encoding)); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/TextBuffer.cs b/src/Spectre.Console/Cli/Internal/TextBuffer.cs new file mode 100644 index 0000000..6237337 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/TextBuffer.cs @@ -0,0 +1,89 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.IO; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class TextBuffer : IDisposable + { + // There is some kind of bug + [SuppressMessage("Usage", "CA2213:Disposable fields should be disposed", Justification = "VS bug")] + private readonly StringReader _reader; + + public bool ReachedEnd => _reader.Peek() == -1; + public string Original { get; } + public int Position { get; private set; } + + public TextBuffer(string text) + { + _reader = new StringReader(text); + Original = text; + Position = 0; + } + + public TextBuffer(TextBuffer? buffer, string text) + { + _reader = new StringReader(text); + Original = buffer != null ? buffer.Original + " " + text : text; + Position = buffer?.Position + 1 ?? 0; + } + + public void Dispose() + { + _reader.Dispose(); + } + + public char Peek() + { + return (char)_reader.Peek(); + } + + public bool TryPeek(out char character) + { + var value = _reader.Peek(); + if (value == -1) + { + character = '\0'; + return false; + } + + character = (char)value; + return true; + } + + public void Consume(char character) + { + EnsureNotAtEnd(); + if (Read() != character) + { + throw new InvalidOperationException($"Expected '{character}' token."); + } + } + + public bool IsNext(char character) + { + if (TryPeek(out var result)) + { + return result == character; + } + + return false; + } + + public char Read() + { + EnsureNotAtEnd(); + var result = (char)_reader.Read(); + Position++; + return result; + } + + private void EnsureNotAtEnd() + { + if (ReachedEnd) + { + throw new InvalidOperationException("Can't read past the end of the buffer."); + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/TypeRegistrar.cs b/src/Spectre.Console/Cli/Internal/TypeRegistrar.cs new file mode 100644 index 0000000..00213d7 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/TypeRegistrar.cs @@ -0,0 +1,41 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class TypeRegistrar : ITypeRegistrarFrontend + { + private readonly ITypeRegistrar _registrar; + + internal TypeRegistrar(ITypeRegistrar registrar) + { + _registrar = registrar ?? throw new ArgumentNullException(nameof(registrar)); + } + + public void Register() + where TImplementation : TService + { + _registrar.Register(typeof(TService), typeof(TImplementation)); + } + + public void RegisterInstance(TImplementation instance) + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + _registrar.RegisterInstance(typeof(TImplementation), instance); + } + + public void RegisterInstance(TImplementation instance) + where TImplementation : TService + { + if (instance == null) + { + throw new ArgumentNullException(nameof(instance)); + } + + _registrar.RegisterInstance(typeof(TService), instance); + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/TypeResolverAdapter.cs b/src/Spectre.Console/Cli/Internal/TypeResolverAdapter.cs new file mode 100644 index 0000000..85c5f54 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/TypeResolverAdapter.cs @@ -0,0 +1,47 @@ +using System; + +namespace Spectre.Console.Cli.Internal +{ + internal sealed class TypeResolverAdapter : ITypeResolver + { + private readonly ITypeResolver? _resolver; + + public TypeResolverAdapter(ITypeResolver? resolver) + { + _resolver = resolver; + } + + public object? Resolve(Type? type) + { + if (type == null) + { + throw new CommandRuntimeException("Cannot resolve null type."); + } + + try + { + if (_resolver != null) + { + var obj = _resolver.Resolve(type); + if (obj == null) + { + throw CommandRuntimeException.CouldNotResolveType(type); + } + + return obj; + } + + // Fall back to use the activator. + return Activator.CreateInstance(type); + } + catch (CommandAppException) + { + throw; + } + catch (Exception ex) + { + throw CommandRuntimeException.CouldNotResolveType(type, ex); + } + } + } +} diff --git a/src/Spectre.Console/Cli/Internal/VersionHelper.cs b/src/Spectre.Console/Cli/Internal/VersionHelper.cs new file mode 100644 index 0000000..dcd50b4 --- /dev/null +++ b/src/Spectre.Console/Cli/Internal/VersionHelper.cs @@ -0,0 +1,28 @@ +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Spectre.Console.Cli.Internal +{ + internal static class VersionHelper + { + [SuppressMessage("Design", "CA1031:Do not catch general exception types")] + public static string GetVersion(Assembly? assembly) + { + if (assembly == null) + { + return "?"; + } + + try + { + var info = FileVersionInfo.GetVersionInfo(assembly.Location); + return info.ProductVersion ?? assembly?.GetName()?.Version?.ToString() ?? "?"; + } + catch + { + return "?"; + } + } + } +} diff --git a/src/Spectre.Console/Cli/PairDeconstuctor.cs b/src/Spectre.Console/Cli/PairDeconstuctor.cs new file mode 100644 index 0000000..6fd8653 --- /dev/null +++ b/src/Spectre.Console/Cli/PairDeconstuctor.cs @@ -0,0 +1,31 @@ +using System; +using Spectre.Console.Cli.Internal; + +namespace Spectre.Console.Cli +{ + /// + /// Base class for a pair deconstructor. + /// + /// The key type. + /// The value type. + public abstract class PairDeconstuctor : IPairDeconstructor + { + /// + /// Deconstructs the provided into a pair. + /// + /// The string to deconstruct into a pair. + /// The deconstructed pair. + protected abstract (TKey Key, TValue Value) Deconstruct(string? value); + + /// + (object? Key, object? Value) IPairDeconstructor.Deconstruct(ITypeResolver resolver, Type keyType, Type valueType, string? value) + { + if (!keyType.IsAssignableFrom(typeof(TKey)) || !valueType.IsAssignableFrom(typeof(TValue))) + { + throw new InvalidOperationException("Pair destructor is not compatible."); + } + + return Deconstruct(value); + } + } +} diff --git a/src/Spectre.Console/Cli/Unsafe/IUnsafeBranchConfigurator.cs b/src/Spectre.Console/Cli/Unsafe/IUnsafeBranchConfigurator.cs new file mode 100644 index 0000000..03ae985 --- /dev/null +++ b/src/Spectre.Console/Cli/Unsafe/IUnsafeBranchConfigurator.cs @@ -0,0 +1,20 @@ +namespace Spectre.Console.Cli.Unsafe +{ + /// + /// Represents an unsafe configurator for a branch. + /// + public interface IUnsafeBranchConfigurator : IUnsafeConfigurator + { + /// + /// Sets the description of the branch. + /// + /// The description of the branch. + void SetDescription(string description); + + /// + /// Adds an example of how to use the branch. + /// + /// The example arguments. + void AddExample(string[] args); + } +} diff --git a/src/Spectre.Console/Cli/Unsafe/IUnsafeConfigurator.cs b/src/Spectre.Console/Cli/Unsafe/IUnsafeConfigurator.cs new file mode 100644 index 0000000..5aad769 --- /dev/null +++ b/src/Spectre.Console/Cli/Unsafe/IUnsafeConfigurator.cs @@ -0,0 +1,26 @@ +using System; + +namespace Spectre.Console.Cli.Unsafe +{ + /// + /// Represents an unsafe configurator. + /// + public interface IUnsafeConfigurator + { + /// + /// Adds a command. + /// + /// The name of the command. + /// The command type. + /// A command configurator that can be used to configure the command further. + ICommandConfigurator AddCommand(string name, Type command); + + /// + /// Adds a command branch. + /// + /// The name of the command branch. + /// The command setting type. + /// The command branch configurator. + void AddBranch(string name, Type settings, Action action); + } +} diff --git a/src/Spectre.Console/Cli/Unsafe/UnsafeConfiguratorExtensions.cs b/src/Spectre.Console/Cli/Unsafe/UnsafeConfiguratorExtensions.cs new file mode 100644 index 0000000..84e89a5 --- /dev/null +++ b/src/Spectre.Console/Cli/Unsafe/UnsafeConfiguratorExtensions.cs @@ -0,0 +1,83 @@ +using System.Linq; + +namespace Spectre.Console.Cli.Unsafe +{ + /// + /// Contains unsafe extensions for . + /// + public static class UnsafeConfiguratorExtensions + { + /// + /// Gets an that allows + /// composition of commands without type safety. + /// + /// The configurator. + /// An . + public static IUnsafeConfigurator SafetyOff(this IConfigurator configurator) + { + if (!(configurator is IUnsafeConfigurator @unsafe)) + { + throw new CommandConfigurationException("Configurator does not support manual configuration"); + } + + return @unsafe; + } + + /// + /// Converts an to + /// a configurator with type safety. + /// + /// The configurator. + /// An . + public static IConfigurator SafetyOn(this IUnsafeConfigurator configurator) + { + if (!(configurator is IConfigurator safe)) + { + throw new CommandConfigurationException("Configurator cannot be converted to a safe configurator."); + } + + return safe; + } + + /// + /// Gets an that allows + /// composition of commands without type safety. + /// + /// The command settings. + /// The configurator. + /// An . + public static IUnsafeConfigurator SafetyOff(this IConfigurator configurator) + where TSettings : CommandSettings + { + if (!(configurator is IUnsafeConfigurator @unsafe)) + { + throw new CommandConfigurationException("Configurator does not support manual configuration"); + } + + return @unsafe; + } + + /// + /// Converts an to + /// a configurator with type safety. + /// + /// The command settings. + /// The configurator. + /// An . + public static IConfigurator SafetyOn(this IUnsafeBranchConfigurator configurator) + where TSettings : CommandSettings + { + if (!(configurator is IConfigurator safe)) + { + throw new CommandConfigurationException($"Configurator cannot be converted to a safe configurator of type '{typeof(TSettings).Name}'."); + } + + if (safe.GetType().GetGenericArguments().First() != typeof(TSettings)) + { + throw new CommandConfigurationException($"Configurator cannot be converted to a safe configurator of type '{typeof(TSettings).Name}'."); + } + + return safe; + } + } +} diff --git a/src/Spectre.Console/Color.cs b/src/Spectre.Console/Color.cs index 7e19d68..c97bc00 100644 --- a/src/Spectre.Console/Color.cs +++ b/src/Spectre.Console/Color.cs @@ -182,7 +182,9 @@ namespace Spectre.Console } // Should not happen, but this will make things easier if we mess things up... - Debug.Assert(color.Number >= 0 && color.Number < 16, "Color does not fall inside the standard palette range."); + Debug.Assert( + color.Number >= 0 && color.Number < 16, + "Color does not fall inside the standard palette range."); return color.Number.Value switch { diff --git a/src/Spectre.Console/CursorDirection.cs b/src/Spectre.Console/CursorDirection.cs index bfea9ea..3ce1f5e 100644 --- a/src/Spectre.Console/CursorDirection.cs +++ b/src/Spectre.Console/CursorDirection.cs @@ -6,22 +6,22 @@ namespace Spectre.Console public enum CursorDirection { /// - /// Up + /// Moves cursor up. /// Up, /// - /// Down + /// Moves cursor down. /// Down, /// - /// Left + /// Moves cursor left. /// Left, /// - /// Right + /// Moves cursor right. /// Right, } diff --git a/src/Spectre.Console/Spectre.Console.csproj b/src/Spectre.Console/Spectre.Console.csproj index 4db6bb2..e1e05e2 100644 --- a/src/Spectre.Console/Spectre.Console.csproj +++ b/src/Spectre.Console/Spectre.Console.csproj @@ -2,28 +2,33 @@ net5.0;netstandard2.0 - 9.0 enable + true - + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - + + + +