├── SampleApp ├── Validations │ ├── CommandOptionsValidator.cs │ └── OptionsValidator.cs ├── DependencyInjection │ ├── DICommandOptions.cs │ ├── CommandWithInjectedServices.cs │ └── ICustomInjectedService.cs ├── SampleApp.csproj └── Commands │ └── StartServerCommand.cs ├── CommandLineParser ├── Abstractions │ ├── ICommandLineParser.cs │ ├── Validations │ │ ├── IValidationResult.cs │ │ ├── IValidator`T.cs │ │ ├── IValidator.cs │ │ ├── ValidationConfigurationBase.cs │ │ └── IValidatorsContainer.cs │ ├── IArgument.cs │ ├── Parsing │ │ ├── IArgumentResolver.cs │ │ ├── UnusedArgumentModel.cs │ │ ├── Collections │ │ │ ├── IListResolver.cs │ │ │ ├── ISetResolver.cs │ │ │ └── IArrayResolver.cs │ │ ├── IParser.cs │ │ ├── BaseArgumentResolver.cs │ │ ├── ICommandLineArgumentResolver.cs │ │ ├── IParserResult.cs │ │ ├── Command │ │ │ └── ICommandParserResult.cs │ │ └── IArgumentManager.cs │ ├── Usage │ │ ├── IEnvironmentVariablesService.cs │ │ ├── ISuggestionProvider.cs │ │ ├── IConsole.cs │ │ ├── IUsageBuilder.cs │ │ └── IUsagePrinter.cs │ ├── Command │ │ ├── ICommandLineCommandParser.cs │ │ ├── ICommandDiscoverer.cs │ │ ├── ICommandLineCommand.cs │ │ ├── Command.cs │ │ ├── ICommandLineCommandContainer.cs │ │ ├── Command`TOptions.cs │ │ ├── ICommandConfigurationBuilder.cs │ │ ├── Command`TOptions`TCmdOptions.cs │ │ ├── ICommandBuilder'.cs │ │ ├── ICommandConfigurationBuilder`.cs │ │ ├── ICommandBuilder.cs │ │ └── ICommandExecutor.cs │ ├── IOptionConfigurator.cs │ ├── Models │ │ ├── IModelInitializer.cs │ │ └── ArgumentModel.cs │ ├── ICommandLineOption.cs │ ├── IOptionBuilder.cs │ └── IOptionBuilder`.cs ├── Core │ ├── Attributes │ │ ├── BaseAttribute.cs │ │ ├── DefaultValueAttribute.cs │ │ ├── DescriptionAttribute.cs │ │ ├── RequiredAttribute.cs │ │ ├── OptionOrderAttribute.cs │ │ └── NameAttribute.cs │ ├── Exceptions │ │ ├── ValidationException.cs │ │ ├── CommandNotFoundException.cs │ │ ├── CommandExecutionFailedException.cs │ │ ├── BaseParserException.cs │ │ ├── OptionNotFoundException.cs │ │ ├── OptionParseException.cs │ │ └── CommandParseException.cs │ ├── Parsing │ │ ├── Resolvers │ │ │ ├── StringResolver.cs │ │ │ ├── IntResolver.cs │ │ │ ├── FloatResolver.cs │ │ │ ├── DoubleResolver.cs │ │ │ ├── DecimalResolver.cs │ │ │ ├── BoolResolver.cs │ │ │ └── Collections │ │ │ │ ├── SetResolver.cs │ │ │ │ ├── ListResolver.cs │ │ │ │ └── ArrayResolver.cs │ │ ├── Command │ │ │ ├── CommandNotFoundParserResult.cs │ │ │ └── CommandParserResult.cs │ │ └── ParseResult'.cs │ ├── Usage │ │ ├── EnvironmentVariableService.cs │ │ ├── SystemConsole.cs │ │ └── UsagePrinter.cs │ ├── ReadOnlyCollectionWrapper.cs │ ├── Command │ │ ├── CommandLineCommandBase.cs │ │ └── CommandDiscoverer.cs │ ├── Models │ │ └── ModelInitializer.cs │ ├── Validations │ │ └── ValidatorsContainer.cs │ ├── CommandLineOption`.cs │ ├── TypedInstanceCache.cs │ └── CommandLineOptionBase.cs ├── DependencyInjectionExtensions.cs ├── CommandLineParser.csproj ├── CommandLineParserOptions.cs └── CommandLineParser.cs ├── Tests └── TestAssembly │ ├── TestAssembly.csproj │ └── NonGenericDiscoverableCommand.cs ├── Extensions ├── Tests │ └── FluentValidationsExtensions.Tests │ │ ├── Models │ │ ├── EmailModel.cs │ │ └── FirstModel.cs │ │ ├── XUnitExtensions.cs │ │ ├── Validators │ │ ├── EmailModelValidator.cs │ │ ├── FirstModelValidator.cs │ │ └── ValidatorWithDependency.cs │ │ ├── Commands │ │ └── CommandWithModel.cs │ │ └── FluentValidationsExtensions.Tests.csproj └── FluentValidationsExtensions │ ├── Core │ ├── FluentValidationResult.cs │ ├── FluentTypeValidatorCollection`1.cs │ └── FluentValidationConfiguration.cs │ ├── FluentValidationsExtensions.cs │ ├── FluentValidationsExtensions.nuspec │ ├── FluentValidationsExtensions.csproj │ └── CommandLineParser.FluentValidations.xml ├── SECURITY.md ├── codecov.yml ├── .github ├── dependabot.yml └── workflows │ ├── dotnet-core.yml │ └── codeql-analysis.yml ├── CommandLineParser.Tests ├── Parsing │ ├── Resolvers │ │ ├── BaseResolverTests.cs │ │ ├── IntResolverTests.cs │ │ ├── StringResovlerTests.cs │ │ ├── BoolResolverTests.cs │ │ ├── EnumResolverTests.cs │ │ ├── DoubleResolverTests.cs │ │ └── DefaultResolverTests.cs │ ├── ParserResultTest.cs │ └── Validation │ │ └── ValidationAbstractionTests.cs ├── Command │ ├── RegisterCommandTests.cs │ ├── MultipleCommandTests.cs │ └── CommandInModelTests.cs ├── CommandLineParser.Tests.csproj ├── XUnitExtensions.cs ├── TestBase.cs ├── Core │ └── TypedInstanceCacheTests.cs ├── OptionBuilderTest.cs ├── Utils │ └── ApiTests.cs ├── CommandLineModelTests.cs ├── BasicDITests.cs ├── Usage │ └── NoColorOutputTests.cs └── CustomerReportedTests.cs ├── LICENSE └── README.md /SampleApp/Validations/CommandOptionsValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace SampleApp.Validations 4 | { 5 | public class CommandOptionsValidator : AbstractValidator 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/ICommandLineParser.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions 2 | { 3 | /// 4 | /// Command line parser 5 | /// 6 | public interface ICommandLineParser : ICommandLineParser 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Validations/IValidationResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Validations 4 | { 5 | public interface IValidationResult 6 | { 7 | bool IsValid { get; } 8 | Exception Error { get; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/BaseAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MatthiWare.CommandLine.Core.Attributes 4 | { 5 | [AttributeUsage(AttributeTargets.Property, AllowMultiple = true, Inherited = true)] 6 | public abstract class BaseAttribute : Attribute 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Tests/TestAssembly/TestAssembly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/ValidationException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MatthiWare.CommandLine.Core.Exceptions 4 | { 5 | public class ValidationException : Exception 6 | { 7 | public ValidationException(Exception innerException) 8 | : base(innerException.Message, innerException) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/IArgument.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions 4 | { 5 | /// 6 | /// Represents an argument 7 | /// See and for more info. 8 | /// 9 | public interface IArgument { } 10 | } 11 | -------------------------------------------------------------------------------- /SampleApp/DependencyInjection/DICommandOptions.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core.Attributes; 2 | 3 | namespace SampleApp.DependencyInjection 4 | { 5 | public class DICommandOptions 6 | { 7 | [Name("p", "print"), Description("Prints all services"), Required(false), DefaultValue(false)] 8 | public bool PrintRegisteredServices { get; set; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Models/EmailModel.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core.Attributes; 2 | 3 | namespace FluentValidationsExtensions.Tests.Models 4 | { 5 | public class EmailModel 6 | { 7 | [Required, Name("e")] 8 | public string Email { get; set; } 9 | 10 | [Required, Name("i")] 11 | public int Id { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Models/FirstModel.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core.Attributes; 2 | 3 | namespace FluentValidationsExtensions.Tests.Models 4 | { 5 | public class FirstModel 6 | { 7 | [Required, Name("f")] 8 | public string FirstName { get; set; } 9 | 10 | [Required, Name("l")] 11 | public string LastName { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SampleApp/Validations/OptionsValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | 3 | namespace SampleApp.Validations 4 | { 5 | public class OptionsValidator : AbstractValidator 6 | { 7 | public OptionsValidator() 8 | { 9 | RuleFor(o => o.MyInt).InclusiveBetween(5, 10); 10 | RuleFor(o => o.MyString).MinimumLength(10).MaximumLength(20); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/IArgumentResolver.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Parsing 2 | { 3 | /// 4 | /// Argument resolver 5 | /// 6 | /// Argument type 7 | public interface IArgumentResolver 8 | : ICommandLineArgumentResolver, ICommandLineArgumentResolver 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/StringResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Parsing; 2 | 3 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 4 | { 5 | internal class StringResolver : BaseArgumentResolver 6 | { 7 | public override bool CanResolve(string value) => value != null; 8 | 9 | public override string Resolve(string value) => value; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/XUnitExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace MatthiWare.CommandLine.Tests 5 | { 6 | public static class XUnitExtensions 7 | { 8 | public static LambdaExpression CreateLambda(Expression> expression) 9 | { 10 | return expression; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Use this section to tell people about which versions of your project are 6 | currently being supported with security updates. 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | 0.5.x | :white_check_mark: | 11 | | < 0.5 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Send an email to security@matthiware.be 16 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/UnusedArgumentModel.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Parsing 2 | { 3 | public readonly struct UnusedArgumentModel 4 | { 5 | public IArgument Argument { get; } 6 | public string Key { get; } 7 | 8 | public UnusedArgumentModel(string key, IArgument argument) 9 | { 10 | Key = key; 11 | Argument = argument; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Usage/IEnvironmentVariablesService.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Usage 2 | { 3 | /// 4 | /// Environment variables 5 | /// 6 | public interface IEnvironmentVariablesService 7 | { 8 | /// 9 | /// Inidicates if NO_COLOR environment variable has been set 10 | /// https://no-color.org/ 11 | /// 12 | bool NoColorRequested { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Validations/IValidator`T.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Validations 2 | { 3 | /// 4 | public interface IValidator : IValidator 5 | { 6 | /// 7 | /// Validates an object 8 | /// 9 | /// Item to validate 10 | /// 11 | IValidationResult Validate(T @object); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | codecov: 2 | notify: 3 | require_ci_to_pass: yes 4 | 5 | coverage: 6 | precision: 2 7 | round: down 8 | range: "25...75" 9 | 10 | status: 11 | project: yes 12 | patch: yes 13 | changes: no 14 | 15 | parsers: 16 | gcov: 17 | branch_detection: 18 | conditional: yes 19 | loop: yes 20 | method: no 21 | macro: no 22 | 23 | comment: 24 | layout: "header, diff, changes, sunburst, uncovered" 25 | behavior: default 26 | require_changes: no -------------------------------------------------------------------------------- /CommandLineParser/Core/Usage/EnvironmentVariableService.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Usage; 2 | using System; 3 | 4 | namespace MatthiWare.CommandLine.Core.Usage 5 | { 6 | /// 7 | public class EnvironmentVariableService : IEnvironmentVariablesService 8 | { 9 | private const string NoColorId = "NO_COLOR"; 10 | 11 | /// 12 | public bool NoColorRequested => Environment.GetEnvironmentVariable(NoColorId) != null; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Validators/EmailModelValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FluentValidationsExtensions.Tests.Models; 3 | 4 | namespace FluentValidationsExtensions.Tests.Validators 5 | { 6 | public class EmailModelValidator : AbstractValidator 7 | { 8 | public EmailModelValidator() 9 | { 10 | RuleFor(model => model.Email).EmailAddress().NotEmpty(); 11 | RuleFor(model => model.Id).NotEmpty().InclusiveBetween(5, 100); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Validators/FirstModelValidator.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FluentValidationsExtensions.Tests.Models; 3 | 4 | namespace FluentValidationsExtensions.Tests.Validators 5 | { 6 | public class FirstModelValidator : AbstractValidator 7 | { 8 | public FirstModelValidator() 9 | { 10 | RuleFor(model => model.FirstName).MaximumLength(30).MinimumLength(10).NotEmpty(); 11 | RuleFor(model => model.LastName).MaximumLength(30).MinimumLength(10).NotEmpty(); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /SampleApp/SampleApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net5.0 6 | 7.3 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Validators/ValidatorWithDependency.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using FluentValidationsExtensions.Tests.Models; 3 | 4 | namespace FluentValidationsExtensions.Tests.Validators 5 | { 6 | public class ValidatorWithDependency : AbstractValidator 7 | { 8 | public ValidatorWithDependency(IValidationDependency dependency) 9 | { 10 | RuleFor(_ => _.Email).Must(dependency.IsValid); 11 | } 12 | } 13 | 14 | public interface IValidationDependency 15 | { 16 | bool IsValid(string input); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | # reviewers 13 | reviewers: 14 | - "Matthiee" 15 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/Commands/CommandWithModel.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | 3 | namespace FluentValidationsExtensions.Tests.Commands 4 | { 5 | public class CommandWithModel : Command 6 | where TBaseModel : class, new() 7 | where TCommandModel : class, new() 8 | { 9 | public override void OnConfigure(ICommandConfigurationBuilder builder) 10 | { 11 | base.OnConfigure(builder); 12 | 13 | builder.Name("cmd"); 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/Collections/IListResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Parsing.Collections 5 | { 6 | /// 7 | public interface IListResolver : ICommandLineArgumentResolver 8 | { 9 | /// 10 | /// Resolves the argument from the model 11 | /// 12 | /// Argument model 13 | /// The resolved type 14 | new List Resolve(ArgumentModel model); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/Collections/ISetResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using System.Collections.Generic; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Parsing.Collections 5 | { 6 | /// 7 | public interface ISetResolver : ICommandLineArgumentResolver 8 | { 9 | /// 10 | /// Resolves the argument from the model 11 | /// 12 | /// Argument model 13 | /// The resolved type 14 | new HashSet Resolve(ArgumentModel model); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/Collections/IArrayResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Parsing.Collections 4 | { 5 | /// 6 | /// Resolve array types 7 | /// 8 | public interface IArrayResolver : ICommandLineArgumentResolver 9 | { 10 | /// 11 | /// Resolves the argument from the model 12 | /// 13 | /// Argument model 14 | /// The resolved type 15 | new TModel[] Resolve(ArgumentModel model); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandLineCommandParser.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Command 6 | { 7 | /// 8 | /// Parser for a command line command 9 | /// 10 | public interface ICommandLineCommandParser 11 | { 12 | /// 13 | /// Parses the arguments 14 | /// 15 | /// 16 | Task ParseAsync(CancellationToken cancellationToken); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/BaseResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using Xunit.Abstractions; 5 | 6 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 7 | { 8 | public abstract class BaseResolverTests : TestBase 9 | { 10 | public IServiceProvider ServiceProvider { get; } 11 | 12 | public BaseResolverTests(ITestOutputHelper outputHelper) 13 | : base(outputHelper) 14 | { 15 | Services.AddDefaultResolvers(); 16 | 17 | ServiceProvider = Services.BuildServiceProvider(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/IntResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Parsing; 2 | 3 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 4 | { 5 | internal class IntResolver : BaseArgumentResolver 6 | { 7 | public override bool CanResolve(string value) 8 | { 9 | if (value is null) 10 | { 11 | return false; 12 | } 13 | 14 | return int.TryParse(value, out _); 15 | } 16 | 17 | public override int Resolve(string value) 18 | { 19 | int.TryParse(value, out int result); 20 | 21 | return result; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/DefaultValueAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Core.Attributes 2 | { 3 | /// 4 | /// Specifies the default value of the property 5 | /// 6 | public sealed class DefaultValueAttribute : BaseAttribute 7 | { 8 | /// 9 | /// Default value 10 | /// 11 | public object DefaultValue { get; private set; } 12 | 13 | /// 14 | /// Constructor 15 | /// 16 | /// default value 17 | public DefaultValueAttribute(object defaultValue) => DefaultValue = defaultValue; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/DescriptionAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Core.Attributes 2 | { 3 | /// 4 | /// Specifies the description of the options 5 | /// 6 | public sealed class DescriptionAttribute : BaseAttribute 7 | { 8 | /// 9 | /// Description 10 | /// 11 | public string Description { get; private set; } 12 | 13 | /// 14 | /// Specifies the description of the options 15 | /// 16 | /// description text 17 | public DescriptionAttribute(string description) => Description = description; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Validations/IValidator.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Validations 5 | { 6 | /// 7 | /// Validator 8 | /// 9 | public interface IValidator 10 | { 11 | /// 12 | /// Validates an object async 13 | /// 14 | /// Item to validate 15 | /// Cancellation token 16 | /// 17 | Task ValidateAsync(object @object, CancellationToken cancellationToken); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/RequiredAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Core.Attributes 2 | { 3 | /// 4 | /// Specified if the command/option is required 5 | /// 6 | public sealed class RequiredAttribute : BaseAttribute 7 | { 8 | /// 9 | /// Is it required? 10 | /// 11 | public bool Required { get; private set; } 12 | 13 | /// 14 | /// Specifies if the command/option is required 15 | /// 16 | /// True if required, false if not 17 | public RequiredAttribute(bool required = true) => Required = required; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/IOptionConfigurator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions 5 | { 6 | /// 7 | /// Allows options to be configured 8 | /// 9 | /// Source type that contains the option 10 | public interface IOptionConfigurator 11 | { 12 | /// 13 | /// Configures if the command options 14 | /// 15 | /// Property to configure 16 | /// 17 | IOptionBuilder Configure(Expression> selector); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Usage/ISuggestionProvider.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System.Collections.Generic; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Usage 5 | { 6 | /// 7 | /// Creates suggestions based on input 8 | /// 9 | public interface ISuggestionProvider 10 | { 11 | /// 12 | /// Gets a list of matching suggestions 13 | /// 14 | /// The wrongly typed input 15 | /// The current command context 16 | /// A sorted list of suggestions, first item being the best match. 17 | IEnumerable GetSuggestions(string input, ICommandLineCommandContainer command); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Models/IModelInitializer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Models 4 | { 5 | /// 6 | /// Tool used to initialize based on a model 7 | /// 8 | public interface IModelInitializer 9 | { 10 | /// 11 | /// Configure options and register commands from the option model 12 | /// 13 | /// Option model type 14 | /// Caller instance 15 | /// configure method name 16 | /// register method name 17 | void InitializeModel(Type optionType, object caller, string configureMethodName, string registerMethodName); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/OptionOrderAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Core.Attributes 2 | { 3 | /// 4 | /// Give an order to options 5 | /// 6 | /// app.exe move "first/argument/path" "second/argument/path" 7 | public sealed class OptionOrderAttribute : BaseAttribute 8 | { 9 | /// 10 | /// Order in which the options will be parsed (Ascending). 11 | /// 12 | public int Order { get; set; } 13 | 14 | /// 15 | /// Assign an order to options 16 | /// 17 | /// Order in which options will be parsed (Ascending) 18 | public OptionOrderAttribute(int order) 19 | { 20 | Order = order; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandDiscoverer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reflection; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Command 6 | { 7 | /// 8 | /// Allows to discover 's from assemblies 9 | /// 10 | public interface ICommandDiscoverer 11 | { 12 | /// 13 | /// Discover commands 14 | /// 15 | /// Only commands with this option type are valid, or non generic commands 16 | /// List of assemblies to scan 17 | /// A list of valid commands that need to be registered 18 | IReadOnlyList DiscoverCommandTypes(Type optionType, Assembly[] assemblies); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandLineCommand.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Command 2 | { 3 | /// 4 | /// Command configuration options 5 | /// 6 | public interface ICommandLineCommand : IArgument 7 | { 8 | /// 9 | /// Name of the command 10 | /// 11 | string Name { get; } 12 | 13 | /// 14 | /// Indicates if the command is required or not 15 | /// 16 | bool IsRequired { get; } 17 | 18 | /// 19 | /// Description of the command 20 | /// 21 | string Description { get; } 22 | 23 | /// 24 | /// Auto executes the command if set to true 25 | /// 26 | bool AutoExecute { get; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/IParser.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Parsing 4 | { 5 | /// 6 | /// API for parsing arguments 7 | /// 8 | public interface IParser 9 | { 10 | /// 11 | /// Checks if the argument can be parsed 12 | /// 13 | /// 14 | /// True if the arguments can be parsed, false if not. 15 | bool CanParse(ArgumentModel model); 16 | 17 | /// 18 | /// Parses the model 19 | /// Check to see if this method will succeed. 20 | /// 21 | /// 22 | void Parse(ArgumentModel model); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Usage/SystemConsole.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Usage; 2 | using System; 3 | 4 | namespace MatthiWare.CommandLine.Core.Usage 5 | { 6 | /// 7 | public class SystemConsole : IConsole 8 | { 9 | /// 10 | public ConsoleColor ForegroundColor 11 | { 12 | get => Console.ForegroundColor; 13 | set => Console.ForegroundColor = value; 14 | } 15 | 16 | /// 17 | public void ErrorWriteLine(string text) => Console.Error.WriteLine(text); 18 | 19 | /// 20 | public void ResetColor() => Console.ResetColor(); 21 | 22 | /// 23 | public void WriteLine(string text) => Console.WriteLine(text); 24 | 25 | /// 26 | public void WriteLine() => Console.WriteLine(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/CommandNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | 3 | namespace MatthiWare.CommandLine.Core.Exceptions 4 | { 5 | /// 6 | /// Indicitates that a configured required command is not found. 7 | /// 8 | public class CommandNotFoundException : BaseParserException 9 | { 10 | /// 11 | /// The command that was not found 12 | /// 13 | public ICommandLineCommand Command => (ICommandLineCommand)Argument; 14 | 15 | /// 16 | /// Creates a new command not found exception 17 | /// 18 | /// The command that was not found 19 | public CommandNotFoundException(ICommandLineCommand cmd) 20 | : base(cmd, $"Required command '{cmd.Name}' not found!") 21 | { } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Validations/ValidationConfigurationBase.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Validations 2 | { 3 | /// 4 | /// Base validation configuration provider. 5 | /// Needs to be inherited when implementing a specific validations provider 6 | /// 7 | public abstract class ValidationConfigurationBase 8 | { 9 | /// 10 | /// Gets a container of all validators 11 | /// 12 | protected IValidatorsContainer Validators { get; private set; } 13 | 14 | /// 15 | /// Create instance of the validation configuration base 16 | /// 17 | /// validator container 18 | public ValidationConfigurationBase(IValidatorsContainer validators) 19 | { 20 | Validators = validators; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Command 5 | { 6 | /// 7 | /// Defines a command 8 | /// 9 | public abstract class Command 10 | { 11 | /// 12 | /// Configures the command 13 | /// for more info. 14 | /// 15 | /// 16 | public virtual void OnConfigure(ICommandConfigurationBuilder builder) { } 17 | 18 | /// 19 | /// Executes the command 20 | /// 21 | public virtual void OnExecute() { } 22 | 23 | /// 24 | /// Executes the command async 25 | /// 26 | public virtual Task OnExecuteAsync(CancellationToken cancellationToken) => Task.CompletedTask; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/FloatResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Parsing; 2 | using System.Globalization; 3 | 4 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 5 | { 6 | internal class FloatResolver : BaseArgumentResolver 7 | { 8 | public override bool CanResolve(string value) 9 | { 10 | if (value is null) 11 | { 12 | return false; 13 | } 14 | 15 | return TryResolve(value, out _); 16 | } 17 | 18 | public override float Resolve(string value) 19 | { 20 | TryResolve(value, out float result); 21 | 22 | return result; 23 | } 24 | 25 | private bool TryResolve(string value, out float result) 26 | { 27 | return float.TryParse(value, NumberStyles.AllowExponent | NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out result); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/DoubleResolver.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | 4 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 5 | { 6 | internal class DoubleResolver : BaseArgumentResolver 7 | { 8 | public override bool CanResolve(string value) 9 | { 10 | if (value is null) 11 | { 12 | return false; 13 | } 14 | 15 | return TryResolve(value, out _); 16 | } 17 | 18 | public override double Resolve(string value) 19 | { 20 | TryResolve(value, out double result); 21 | 22 | return result; 23 | } 24 | 25 | private bool TryResolve(string value, out double result) 26 | { 27 | return double.TryParse(value, NumberStyles.AllowExponent | NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out result); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/DecimalResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Parsing; 2 | using System.Globalization; 3 | 4 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 5 | { 6 | internal class DecimalResolver : BaseArgumentResolver 7 | { 8 | public override bool CanResolve(string value) 9 | { 10 | if (value is null) 11 | { 12 | return false; 13 | } 14 | 15 | return TryResolve(value, out _); 16 | } 17 | 18 | public override decimal Resolve(string value) 19 | { 20 | TryResolve(value, out decimal result); 21 | 22 | return result; 23 | } 24 | 25 | private bool TryResolve(string value, out decimal result) 26 | { 27 | return decimal.TryParse(value, NumberStyles.AllowExponent | NumberStyles.Number | NumberStyles.Float, CultureInfo.InvariantCulture, out result); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandLineCommandContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Command 6 | { 7 | /// 8 | /// Container that holds options and subcommands. 9 | /// 10 | public interface ICommandLineCommandContainer 11 | { 12 | /// 13 | /// Read-only list of available sub-commands 14 | /// to configure or add an command 15 | /// 16 | IReadOnlyList Commands { get; } 17 | 18 | /// 19 | /// Read-only list of available options for this command 20 | /// to configure or add an option 21 | /// 22 | IReadOnlyList Options { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/CommandExecutionFailedException.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System; 3 | 4 | namespace MatthiWare.CommandLine.Core.Exceptions 5 | { 6 | /// 7 | /// Command failed to execute exception 8 | /// 9 | public class CommandExecutionFailedException : Exception 10 | { 11 | /// 12 | /// Command failed to execute exception 13 | /// 14 | /// Command that failed 15 | /// Actual exception 16 | public CommandExecutionFailedException(ICommandLineCommand command, Exception innerException) 17 | : base(CreateMessage(command, innerException), innerException) 18 | { 19 | } 20 | 21 | private static string CreateMessage(ICommandLineCommand command, Exception exception) 22 | => $"Command '{command.Name}' failed to execute because: {exception.Message}"; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/Core/FluentValidationResult.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation.Results; 2 | using MatthiWare.CommandLine.Abstractions.Validations; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace MatthiWare.CommandLine.Extensions.FluentValidations.Core 7 | { 8 | internal sealed class FluentValidationsResult : IValidationResult 9 | { 10 | private FluentValidationsResult(IEnumerable errors = null) 11 | { 12 | if (errors != null) 13 | { 14 | Error = new FluentValidation.ValidationException(errors); 15 | } 16 | 17 | IsValid = Error == null; 18 | } 19 | 20 | public bool IsValid { get; } 21 | 22 | public Exception Error { get; } 23 | 24 | public static FluentValidationsResult Succes() => new FluentValidationsResult(); 25 | 26 | public static FluentValidationsResult Failure(IEnumerable errors) => new FluentValidationsResult(errors); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Tests/TestAssembly/NonGenericDiscoverableCommand.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Abstractions.Models; 3 | using MatthiWare.CommandLine.Abstractions.Parsing; 4 | 5 | namespace TestAssembly 6 | { 7 | public class NonGenericDiscoverableCommand : Command 8 | { 9 | private readonly IArgumentResolver argResolver; 10 | 11 | public NonGenericDiscoverableCommand(IArgumentResolver argResolver) 12 | { 13 | this.argResolver = argResolver; 14 | } 15 | 16 | public override void OnConfigure(ICommandConfigurationBuilder builder) 17 | { 18 | builder 19 | .Name("cmd") 20 | .Required(true) 21 | .AutoExecute(true); 22 | } 23 | 24 | public override void OnExecute() 25 | { 26 | argResolver.CanResolve(new ArgumentModel(nameof(NonGenericDiscoverableCommand), string.Empty)); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/Command`TOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Command 5 | { 6 | /// 7 | /// Defines a command 8 | /// 9 | /// Base options of the command 10 | public abstract class Command 11 | : Command 12 | where TOptions : class, new() 13 | { 14 | /// 15 | /// Executes the command 16 | /// 17 | /// Parsed options 18 | public virtual void OnExecute(TOptions options) 19 | { 20 | OnExecute(); 21 | } 22 | 23 | /// 24 | /// Executes the command 25 | /// 26 | /// Parsed options 27 | public virtual Task OnExecuteAsync(TOptions options, CancellationToken cancellationToken) 28 | { 29 | return OnExecuteAsync(cancellationToken); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/FluentValidationsExtensions.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Extensions.FluentValidations.Core; 3 | using System; 4 | 5 | namespace MatthiWare.CommandLine.Extensions.FluentValidations 6 | { 7 | /// 8 | /// FluentValidations Extensions for CommandLineParser 9 | /// 10 | public static class FluentValidationsExtensions 11 | { 12 | /// 13 | /// Extensions to configure FluentValidations for the Parser 14 | /// 15 | /// 16 | /// 17 | /// Configuration action 18 | public static void UseFluentValidations(this ICommandLineParser parser, Action configAction) 19 | where T : class, new() 20 | { 21 | var config = new FluentValidationConfiguration(parser.Validators, parser.Services); 22 | 23 | configAction(config); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Command/RegisterCommandTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace MatthiWare.CommandLine.Tests.Command 7 | { 8 | public class RegisterCommandTests : TestBase 9 | { 10 | private readonly CommandLineParser parser; 11 | 12 | public RegisterCommandTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 13 | { 14 | parser = new CommandLineParser(Services); 15 | } 16 | 17 | [Fact] 18 | public void RegisterCommandWithNoneCommandTypeThrowsException() 19 | { 20 | Assert.Throws(() => parser.RegisterCommand(typeof(object))); 21 | } 22 | 23 | [Fact] 24 | public void RegisterCommandWithWrongParentTypeThrowsException() 25 | { 26 | Assert.Throws(() => parser.RegisterCommand(typeof(MyWrongCommand))); 27 | } 28 | 29 | private class MyWrongCommand : Command 30 | { 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 MatthiWare 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/BaseArgumentResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using System.Linq; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Parsing 5 | { 6 | /// 7 | /// Class to resolve arguments 8 | /// 9 | /// Argument type 10 | public abstract class BaseArgumentResolver 11 | : IArgumentResolver 12 | { 13 | /// 14 | public virtual bool CanResolve(ArgumentModel model) => CanResolve(model.Values.FirstOrDefault()); 15 | 16 | /// 17 | public abstract bool CanResolve(string value); 18 | 19 | /// 20 | public virtual TArgument Resolve(ArgumentModel model) => Resolve(model.Values.FirstOrDefault()); 21 | 22 | /// 23 | public abstract TArgument Resolve(string value); 24 | 25 | object ICommandLineArgumentResolver.Resolve(ArgumentModel model) => Resolve(model); 26 | 27 | object ICommandLineArgumentResolver.Resolve(string value) => Resolve(value); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Usage/IConsole.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Usage 4 | { 5 | /// 6 | /// 7 | /// 8 | public interface IConsole 9 | { 10 | /// 11 | /// 12 | /// 13 | void WriteLine(); 14 | 15 | /// 16 | /// 17 | /// 18 | /// Input text 19 | void WriteLine(string text); 20 | 21 | /// 22 | /// 23 | /// 24 | /// Input text 25 | void ErrorWriteLine(string text); 26 | 27 | /// 28 | /// 29 | /// 30 | ConsoleColor ForegroundColor { get; set; } 31 | 32 | /// 33 | /// 34 | /// 35 | void ResetColor(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Attributes/NameAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Core.Attributes 2 | { 3 | /// 4 | /// Specifies the name of the option/command 5 | /// 6 | public sealed class NameAttribute : BaseAttribute 7 | { 8 | /// 9 | /// Short version 10 | /// 11 | public string ShortName { get; private set; } 12 | 13 | /// 14 | /// Long version 15 | /// 16 | public string LongName { get; private set; } 17 | 18 | /// 19 | /// Specifies the name 20 | /// 21 | /// short name 22 | public NameAttribute(string shortName) 23 | : this(shortName, string.Empty) 24 | { } 25 | 26 | /// 27 | /// Specified the name 28 | /// 29 | /// short name 30 | /// long name 31 | public NameAttribute(string shortName, string longName) 32 | { 33 | ShortName = shortName; 34 | LongName = longName; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.github/workflows/dotnet-core.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 5.0.408 20 | - name: Clean 21 | run: dotnet clean -c Release 22 | - name: Install dependencies 23 | run: dotnet restore 24 | - name: Build 25 | run: dotnet build --configuration Release --no-restore 26 | - name: Test 27 | run: dotnet test --no-restore --verbosity normal 28 | - name: Upload Release Build Artifacts 29 | uses: actions/upload-artifact@v2.2.3 30 | with: 31 | name: Release Build 32 | path: /home/runner/work/CommandLineParser.Core/CommandLineParser.Core/CommandLineParser/bin/Release/**/* 33 | - name: Upload Release Build Artifacts 34 | uses: actions/upload-artifact@v2.2.3 35 | with: 36 | name: Release Build Extensions 37 | path: /home/runner/work/CommandLineParser.Core/CommandLineParser.Core/Extensions/FluentValidationsExtensions/bin/Release/**/* 38 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/BaseParserException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using MatthiWare.CommandLine.Abstractions; 3 | using MatthiWare.CommandLine.Abstractions.Command; 4 | 5 | namespace MatthiWare.CommandLine.Core.Exceptions 6 | { 7 | /// 8 | /// Base exception class that exposes the this exception is about. 9 | /// 10 | public abstract class BaseParserException : Exception 11 | { 12 | /// 13 | /// The argument this exception is for. 14 | /// and 15 | /// 16 | public IArgument Argument { get; private set; } 17 | 18 | /// 19 | /// Creates a new CLI Parser Exception for a given argument 20 | /// 21 | /// The argument 22 | /// The exception message 23 | /// Optional inner exception 24 | public BaseParserException(IArgument argument, string message, Exception innerException = null) 25 | : base(message, innerException) 26 | { 27 | this.Argument = argument; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Command/CommandNotFoundParserResult.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Command; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace MatthiWare.CommandLine.Core.Parsing.Command 10 | { 11 | internal class CommandNotFoundParserResult : ICommandParserResult 12 | { 13 | public bool Found => false; 14 | 15 | public IArgument HelpRequestedFor => null; 16 | 17 | public bool HelpRequested => false; 18 | 19 | public IReadOnlyCollection SubCommands => new List(); 20 | 21 | public ICommandLineCommand Command { get; } 22 | 23 | public bool HasErrors => false; 24 | 25 | public IReadOnlyCollection Errors => new List(); 26 | 27 | public bool Executed => false; 28 | 29 | public void ExecuteCommand() { } 30 | 31 | public Task ExecuteCommandAsync(CancellationToken cancellationToken) => Task.CompletedTask; 32 | 33 | public CommandNotFoundParserResult(ICommandLineCommand cmd) => Command = cmd; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /CommandLineParser/Core/ReadOnlyCollectionWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace MatthiWare.CommandLine.Core 6 | { 7 | internal class ReadOnlyCollectionWrapper : IReadOnlyList 8 | { 9 | private readonly Dictionary.ValueCollection values; 10 | 11 | public ReadOnlyCollectionWrapper(Dictionary.ValueCollection values) 12 | { 13 | this.values = values; 14 | } 15 | 16 | public TValue this[int index] => FindFirstIndex(GetEnumerator(), index); 17 | 18 | public int Count => values.Count; 19 | 20 | public IEnumerator GetEnumerator() => values.GetEnumerator(); 21 | 22 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); 23 | 24 | private TValue FindFirstIndex(IEnumerator enumerator, int index) 25 | { 26 | int i = 0; 27 | while (enumerator.MoveNext()) 28 | { 29 | if (i == index) 30 | { 31 | return enumerator.Current; 32 | } 33 | 34 | i++; 35 | } 36 | 37 | throw new IndexOutOfRangeException(); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /SampleApp/DependencyInjection/CommandWithInjectedServices.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System; 3 | using static SampleApp.Program; 4 | 5 | namespace SampleApp.DependencyInjection 6 | { 7 | public class CommandWithInjectedServices : Command 8 | { 9 | private readonly ICustomInjectedService customService; 10 | 11 | public CommandWithInjectedServices(ICustomInjectedService customService) 12 | { 13 | this.customService = customService ?? throw new ArgumentNullException(nameof(customService)); 14 | } 15 | 16 | public override void OnConfigure(ICommandConfigurationBuilder builder) 17 | { 18 | builder 19 | .Name("di") 20 | .Required(false) 21 | .Description("Example using Dependency Injection"); 22 | } 23 | 24 | public override void OnExecute(Options options, DICommandOptions commandOptions) 25 | { 26 | if (!commandOptions.PrintRegisteredServices) 27 | { 28 | customService.Execute($"Unable to execute because {nameof(commandOptions.PrintRegisteredServices)} was set to false"); 29 | return; 30 | } 31 | 32 | customService.PrintServices(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /Extensions/Tests/FluentValidationsExtensions.Tests/FluentValidationsExtensions.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | all 12 | runtime; build; native; contentfiles; analyzers 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/CommandLineParser.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net5.0 5 | 6 | false 7 | 8 | MatthiWare.CommandLine.Tests 9 | 10 | 7.3 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /SampleApp/DependencyInjection/ICustomInjectedService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | 4 | namespace SampleApp.DependencyInjection 5 | { 6 | public interface ICustomInjectedService 7 | { 8 | void Execute(string input); 9 | void PrintServices(); 10 | } 11 | 12 | public class CustomInjectedService : ICustomInjectedService 13 | { 14 | private readonly IServiceCollection services; 15 | 16 | public CustomInjectedService(IServiceCollection services) 17 | { 18 | this.services = services ?? throw new ArgumentNullException(nameof(services)); 19 | } 20 | 21 | public void Execute(string input) 22 | { 23 | Console.WriteLine($"Injected Service: {input}"); 24 | } 25 | 26 | public void PrintServices() 27 | { 28 | Console.WriteLine("Injeced Service: All available services\n"); 29 | 30 | foreach (var service in services) 31 | { 32 | Console.WriteLine($" -> {service.ServiceType.Name } implemented by {service.ImplementationType?.Name ?? service.ImplementationInstance?.GetType().Name ?? service.ImplementationFactory.ToString()} with lifetime of {service.Lifetime}"); 33 | } 34 | 35 | Console.WriteLine("\n"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/XUnitExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using MatthiWare.CommandLine.Abstractions.Parsing; 4 | using Xunit; 5 | 6 | namespace MatthiWare.CommandLine.Tests 7 | { 8 | public static class XUnitExtensions 9 | { 10 | public static LambdaExpression CreateLambda(Expression> expression) 11 | { 12 | return expression; 13 | } 14 | 15 | public static bool AssertNoErrors(this IParserResult result, bool shouldThrow = true) 16 | { 17 | if (result == null) 18 | { 19 | throw new NullReferenceException("Parsing result was null"); 20 | } 21 | 22 | foreach (var err in result.Errors) 23 | { 24 | if (shouldThrow) 25 | { 26 | throw err; 27 | } 28 | else 29 | { 30 | return true; 31 | } 32 | } 33 | 34 | return false; 35 | } 36 | } 37 | 38 | #pragma warning disable SA1402 // FileMayOnlyContainASingleType 39 | [CollectionDefinition("Non-Parallel Collection", DisableParallelization = true)] 40 | public class NonParallelCollection 41 | { 42 | } 43 | #pragma warning restore SA1402 // FileMayOnlyContainASingleType 44 | } 45 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/IntResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 8 | { 9 | public class IntResolverTests 10 | : BaseResolverTests 11 | { 12 | public IntResolverTests(ITestOutputHelper outputHelper) : base(outputHelper) 13 | { 14 | } 15 | 16 | [Theory] 17 | [InlineData(true, "-m", "5")] 18 | [InlineData(false, "-m", "false")] 19 | public void TestCanResolve(bool expected, string key, string value) 20 | { 21 | var resolver = ServiceProvider.GetRequiredService>(); 22 | var model = new ArgumentModel(key, value); 23 | 24 | Assert.Equal(expected, resolver.CanResolve(model)); 25 | } 26 | 27 | [Theory] 28 | [InlineData(5, "-m", "5")] 29 | [InlineData(-5, "-m", "-5")] 30 | public void TestResolve(int expected, string key, string value) 31 | { 32 | var resolver = ServiceProvider.GetRequiredService>(); 33 | var model = new ArgumentModel(key, value); 34 | 35 | Assert.Equal(expected, resolver.Resolve(model)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/StringResovlerTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 8 | { 9 | public class StringResovlerTests 10 | : BaseResolverTests 11 | { 12 | public StringResovlerTests(ITestOutputHelper outputHelper) : base(outputHelper) 13 | { 14 | } 15 | 16 | [Theory] 17 | [InlineData(true, "-m", "test")] 18 | [InlineData(true, "-m", "my string")] 19 | public void TestCanResolve(bool expected, string key, string value) 20 | { 21 | var resolver = ServiceProvider.GetRequiredService>(); 22 | var model = new ArgumentModel(key, value); 23 | 24 | Assert.Equal(expected, resolver.CanResolve(model)); 25 | } 26 | 27 | [Theory] 28 | [InlineData("test", "-m", "test")] 29 | [InlineData("my string", "-m", "my string")] 30 | public void TestResolve(string expected, string key, string value) 31 | { 32 | var resolver = ServiceProvider.GetRequiredService>(); 33 | var model = new ArgumentModel(key, value); 34 | 35 | Assert.Equal(expected, resolver.Resolve(model)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/BoolResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 8 | { 9 | public class BoolResolverTests 10 | : BaseResolverTests 11 | { 12 | public BoolResolverTests(ITestOutputHelper outputHelper) : base(outputHelper) 13 | { 14 | } 15 | 16 | [Theory] 17 | [InlineData("yes")] 18 | [InlineData("1")] 19 | [InlineData("true")] 20 | [InlineData("")] 21 | [InlineData(null)] 22 | public void TestResolveTrue(string input) 23 | { 24 | var resolver = ServiceProvider.GetRequiredService>(); 25 | 26 | var result = resolver.Resolve(new ArgumentModel(string.Empty, input)); 27 | 28 | Assert.True(result); 29 | } 30 | 31 | [Theory] 32 | [InlineData("no")] 33 | [InlineData("0")] 34 | [InlineData("false")] 35 | public void TestResolveFalse(string input) 36 | { 37 | var resolver = ServiceProvider.GetRequiredService>(); 38 | 39 | var result = resolver.Resolve(new ArgumentModel(string.Empty, input)); 40 | 41 | Assert.False(result); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Models/ArgumentModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Diagnostics; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Models 5 | { 6 | /// 7 | /// Model for command line arguments 8 | /// 9 | [DebuggerDisplay("Argument key: {Key} values({Values.Count}): {string.Join(\", \", Values)}")] 10 | public class ArgumentModel 11 | { 12 | /// 13 | /// Argument identifier 14 | /// 15 | public string Key { get; set; } 16 | 17 | /// 18 | /// Value of the argument 19 | /// 20 | public List Values { get; } = new List(); 21 | 22 | /// 23 | /// Checks if an value has been provided in the model 24 | /// 25 | public bool HasValue => Values.Count > 0; 26 | 27 | public ArgumentModel(string key) 28 | { 29 | this.Key = key; 30 | } 31 | 32 | /// 33 | /// Creates a new instance of the argument model 34 | /// 35 | /// model identifier 36 | /// model value 37 | public ArgumentModel(string key, string value) 38 | { 39 | this.Key = key; 40 | 41 | if (!string.IsNullOrEmpty(value)) 42 | { 43 | Values.Add(value); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Command/MultipleCommandTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core.Attributes; 2 | using Xunit; 3 | using Xunit.Abstractions; 4 | 5 | namespace MatthiWare.CommandLine.Tests.Command 6 | { 7 | public class MultipleCommandTests : TestBase 8 | { 9 | public MultipleCommandTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 10 | { 11 | } 12 | 13 | [Theory] 14 | [InlineData(new string[] { "cmd1", "-x", "8" }, false)] 15 | [InlineData(new string[] { "cmd2", "-x", "8" }, false)] 16 | [InlineData(new string[] { }, false)] 17 | public void NonRequiredCommandShouldNotSetResultInErrorStateWhenRequiredOptionsAreMissing(string[] args, bool _) 18 | { 19 | Services.AddCommandLineParser(); 20 | 21 | var parser = ResolveParser(); 22 | 23 | parser.AddCommand() 24 | .Name("cmd1") 25 | .Required(false) 26 | .Description("cmd1"); 27 | 28 | parser.AddCommand() 29 | .Name("cmd2") 30 | .Required(false) 31 | .Description("cmd2"); 32 | 33 | var result = parser.Parse(args); 34 | 35 | result.AssertNoErrors(); 36 | } 37 | 38 | private class MultipleCommandTestsOptions 39 | { 40 | [Required, Name("x", "bla"), Description("some description")] 41 | public int Option { get; set; } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandConfigurationBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions.Command 2 | { 3 | /// 4 | /// Command builder 5 | /// 6 | public interface ICommandConfigurationBuilder 7 | { 8 | /// 9 | /// Configures if the command is required 10 | /// 11 | /// True or false 12 | /// 13 | ICommandConfigurationBuilder Required(bool required = true); 14 | 15 | /// 16 | /// Configures the description text for the command 17 | /// 18 | /// Description 19 | /// 20 | ICommandConfigurationBuilder Description(string description); 21 | 22 | /// 23 | /// Configures the command name 24 | /// 25 | /// Command name 26 | /// 27 | ICommandConfigurationBuilder Name(string name); 28 | 29 | /// 30 | /// Configures if the command should auto execute 31 | /// 32 | /// True for automated execution, false for manual 33 | /// 34 | ICommandConfigurationBuilder AutoExecute(bool autoExecute); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/ICommandLineOption.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions 2 | { 3 | /// 4 | /// Option configuration options 5 | /// 6 | public interface ICommandLineOption : IArgument 7 | { 8 | /// 9 | /// Short name of the option 10 | /// 11 | string ShortName { get; } 12 | 13 | /// 14 | /// Long name of the option 15 | /// 16 | string LongName { get; } 17 | 18 | /// 19 | /// Description of the option 20 | /// 21 | string Description { get; } 22 | 23 | /// 24 | /// Indicates if the option is required 25 | /// 26 | bool IsRequired { get; } 27 | 28 | /// 29 | /// Indicates if a short option name has been specified 30 | /// 31 | bool HasShortName { get; } 32 | 33 | /// 34 | /// Indicates if a long option name has been specified 35 | /// 36 | bool HasLongName { get; } 37 | 38 | /// 39 | /// Inidicates if a default value has been specified for this option 40 | /// 41 | bool HasDefault { get; } 42 | 43 | /// 44 | /// Option order 45 | /// 46 | int? Order { get; } 47 | 48 | /// 49 | /// Can have multiple values? 50 | /// 51 | bool AllowMultipleValues { get; } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/FluentValidationsExtensions.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | MatthiWare.CommandLineParser.Extensions.FluentValidations 5 | 0.5.0 6 | CommandLineParser.Core.Extensions.FluentValidations 7 | Matthias Beerens 8 | Matthiee 9 | https://github.com/MatthiWare/CommandLineParser.Core/blob/master/LICENSE 10 | https://github.com/MatthiWare/CommandLineParser.Core 11 | false 12 | 13 | Fluent Validations extension for MatthiWare.CommandLineParser.Core 14 | 15 | Fluent Validations extension for CommandLineParser.Core 16 | 17 | - Option clustering/grouping 18 | - Suggestions when mistyping command/options 19 | - Positional parameters 20 | - Support for all basic datatypes 21 | 22 | Copyright Matthias Beerens 2019 23 | commandline parser commandline-parser extension fluent-validation validations 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/OptionNotFoundException.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | 3 | namespace MatthiWare.CommandLine.Core.Exceptions 4 | { 5 | /// 6 | /// Indiciates the configured required option is not found 7 | /// 8 | public class OptionNotFoundException : BaseParserException 9 | { 10 | /// 11 | /// Option that was not found 12 | /// 13 | public ICommandLineOption Option => (ICommandLineOption)Argument; 14 | 15 | /// 16 | /// Creates a new for a given 17 | /// 18 | /// The option that was not found 19 | public OptionNotFoundException(ICommandLineOption option) 20 | : base(option, CreateMessage(option)) 21 | { } 22 | 23 | private static string CreateMessage(ICommandLineOption option) 24 | { 25 | var hasShort = option.HasShortName; 26 | var hasLong = option.HasLongName; 27 | var hasBoth = hasShort && hasLong; 28 | 29 | var hasBothSeperator = hasBoth ? "|" : string.Empty; 30 | var shortName = hasShort ? $"{option.ShortName}" : string.Empty; 31 | var longName = hasLong ? $"{option.LongName}" : string.Empty; 32 | var optionString = hasShort || hasLong ? string.Empty : option.ToString(); 33 | 34 | return $"Required option '{shortName}{hasBothSeperator}{longName}{optionString}' not found!"; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Command/CommandLineCommandBase.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Command; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace MatthiWare.CommandLine.Core.Command 10 | { 11 | [DebuggerDisplay("Command [{Name}] Options: {m_options.Count} Commands: {m_commands.Count} Required: {IsRequired} Execute: {AutoExecute}")] 12 | internal abstract class CommandLineCommandBase : 13 | ICommandLineCommandParser, 14 | ICommandLineCommandContainer, 15 | ICommandLineCommand 16 | { 17 | protected readonly Dictionary m_options = new Dictionary(); 18 | protected readonly List m_commands = new List(); 19 | 20 | public IReadOnlyList Options => new ReadOnlyCollectionWrapper(m_options.Values); 21 | 22 | public IReadOnlyList Commands => m_commands.AsReadOnly(); 23 | 24 | public string Name { get; protected set; } 25 | public string Description { get; protected set; } 26 | public bool IsRequired { get; protected set; } 27 | public bool AutoExecute { get; protected set; } = true; 28 | 29 | public abstract Task ExecuteAsync(CancellationToken cancellationToken); 30 | 31 | public abstract Task ParseAsync(CancellationToken cancellationToken); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/EnumResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 8 | { 9 | public class EnumResolverTests 10 | : BaseResolverTests 11 | { 12 | public EnumResolverTests(ITestOutputHelper outputHelper) : base(outputHelper) 13 | { 14 | } 15 | 16 | [Theory] 17 | [InlineData(true, "-m", "Error")] 18 | [InlineData(false, "-m", "xd")] 19 | [InlineData(false, "-m", "")] 20 | public void TestCanResolve(bool expected, string key, string value) 21 | { 22 | var resolver = ServiceProvider.GetRequiredService>(); 23 | var model = new ArgumentModel(key, value); 24 | 25 | Assert.Equal(expected, resolver.CanResolve(model)); 26 | } 27 | 28 | [Theory] 29 | [InlineData(TestEnum.Error, "-m", "Error")] 30 | [InlineData(TestEnum.Error, "-m", "error")] 31 | [InlineData(TestEnum.Verbose, "-m", "Verbose")] 32 | [InlineData(TestEnum.Verbose, "-m", "verbose")] 33 | public void TestResolve(TestEnum expected, string key, string value) 34 | { 35 | var resolver = ServiceProvider.GetRequiredService>(); 36 | var model = new ArgumentModel(key, value); 37 | 38 | Assert.Equal(expected, resolver.Resolve(model)); 39 | } 40 | 41 | public enum TestEnum 42 | { 43 | Info, 44 | Error, 45 | Verbose 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/TestBase.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using System; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests 8 | { 9 | public abstract class TestBase 10 | { 11 | private readonly ITestOutputHelper testOutputHelper; 12 | private IServiceProvider serviceProvider = null; 13 | 14 | public ILogger Logger { get; set; } 15 | 16 | public IServiceCollection Services { get; set; } 17 | 18 | public IServiceProvider ServiceProvider 19 | { 20 | get 21 | { 22 | if (serviceProvider is null) 23 | { 24 | serviceProvider = Services.BuildServiceProvider(); 25 | } 26 | 27 | return serviceProvider; 28 | } 29 | } 30 | 31 | public ICommandLineParser ResolveParser() 32 | where TOption : class, new() 33 | { 34 | return ServiceProvider.GetRequiredService>(); 35 | } 36 | 37 | public ICommandLineParser ResolveParser() 38 | { 39 | return ServiceProvider.GetRequiredService(); 40 | } 41 | 42 | public TestBase(ITestOutputHelper testOutputHelper) 43 | { 44 | this.testOutputHelper = testOutputHelper ?? throw new ArgumentNullException(nameof(testOutputHelper)); 45 | Logger = this.testOutputHelper.BuildLoggerFor(); 46 | 47 | Services = new ServiceCollection(); 48 | Services.AddSingleton(Logger); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/DoubleResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 8 | { 9 | public class DoubleResolverTests 10 | : BaseResolverTests 11 | { 12 | public DoubleResolverTests(ITestOutputHelper outputHelper) : base(outputHelper) 13 | { 14 | } 15 | 16 | [Theory] 17 | [InlineData(true, "6E-14")] 18 | [InlineData(false, "false")] 19 | public void TestCanResolve(bool expected, string value) 20 | { 21 | var resolver = ServiceProvider.GetRequiredService>(); 22 | var model = new ArgumentModel("key", value); 23 | 24 | Assert.Equal(expected, resolver.CanResolve(model)); 25 | } 26 | 27 | [Theory] 28 | [InlineData(5.2, "5.2")] 29 | [InlineData(6E-14, "6E-14")] 30 | [InlineData(0.84551240822557006, "0.84551240822557006")] 31 | [InlineData(0.84551240822557, "0.84551240822557")] 32 | [InlineData(4.2, "4.2000000000000002")] 33 | [InlineData(4.2, "4.2")] 34 | [InlineData(double.NaN, "NaN")] 35 | [InlineData(double.NegativeInfinity, "-Infinity")] 36 | [InlineData(double.PositiveInfinity, "Infinity")] 37 | public void TestResolve(double expected, string value) 38 | { 39 | var resolver = ServiceProvider.GetRequiredService>(); 40 | var model = new ArgumentModel("key", value); 41 | 42 | Assert.Equal(expected, resolver.Resolve(model)); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /SampleApp/Commands/StartServerCommand.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using static SampleApp.Program; 6 | 7 | namespace SampleApp.Commands 8 | { 9 | public class StartServerCommand : Command 10 | { 11 | public override void OnConfigure(ICommandConfigurationBuilder builder) 12 | { 13 | base.OnConfigure(builder); 14 | 15 | builder 16 | .Name("start") 17 | .Description("Start the server command.") 18 | .Required(); 19 | 20 | builder.Configure(opt => opt.Verbose) 21 | .Description("Verbose output [true/false]") 22 | .Default(false) 23 | .Name("v", "verbose"); 24 | } 25 | 26 | public override async Task OnExecuteAsync(Options options, CommandOptions commandOptions, CancellationToken cancellationToken) 27 | { 28 | await WriteTextWithDots("Starting server"); 29 | 30 | await Task.Delay(1250); 31 | 32 | await WriteTextWithDots("Beep boop initializing socket connections"); 33 | 34 | await Task.Delay(1250); 35 | 36 | Console.WriteLine($"Server started using verbose option: {commandOptions.Verbose}"); 37 | } 38 | 39 | private async Task WriteTextWithDots(string text, int delayPerDot = 750, int amountOfDots = 3) 40 | { 41 | Console.Write(text); 42 | 43 | for (int i = 0; i < amountOfDots; i++) 44 | { 45 | await Task.Delay(delayPerDot); 46 | Console.Write("."); 47 | } 48 | 49 | Console.WriteLine(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/Command`TOptions`TCmdOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Command 5 | { 6 | /// 7 | /// Defines a command 8 | /// 9 | /// Base options of the command 10 | /// Command options 11 | public abstract class Command : 12 | Command 13 | where TOptions : class, new() 14 | where TCommandOptions : class, new() 15 | { 16 | /// 17 | /// Configures the command 18 | /// for more info. 19 | /// 20 | /// 21 | public virtual void OnConfigure(ICommandConfigurationBuilder builder) 22 | { 23 | OnConfigure((ICommandConfigurationBuilder)builder); 24 | } 25 | 26 | /// 27 | /// Executes the command 28 | /// 29 | /// 30 | /// 31 | public virtual void OnExecute(TOptions options, TCommandOptions commandOptions) 32 | { 33 | OnExecute(options); 34 | } 35 | 36 | /// 37 | /// Executes the command 38 | /// 39 | /// 40 | /// 41 | public virtual Task OnExecuteAsync(TOptions options, TCommandOptions commandOptions, CancellationToken cancellationToken) 42 | { 43 | return OnExecuteAsync(options, cancellationToken); 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/Core/FluentTypeValidatorCollection`1.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MatthiWare.CommandLine.Abstractions.Validations; 3 | using MatthiWare.CommandLine.Core; 4 | using System; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace MatthiWare.CommandLine.Extensions.FluentValidations.Core 10 | { 11 | internal class FluentTypeValidatorCollection : Abstractions.Validations.IValidator 12 | { 13 | private readonly TypedInstanceCache validators; 14 | 15 | public FluentTypeValidatorCollection(IServiceProvider serviceProvider) 16 | { 17 | validators = new TypedInstanceCache(serviceProvider); 18 | } 19 | 20 | public void AddValidator(FluentValidation.IValidator validator) 21 | { 22 | validators.Add(validator); 23 | } 24 | 25 | public void AddValidator(Type t) => validators.Add(t); 26 | 27 | public void AddValidator() where K : FluentValidation.IValidator 28 | => AddValidator(typeof(K)); 29 | 30 | public async Task ValidateAsync(object @object, CancellationToken cancellationToken = default) 31 | { 32 | var errors = (await Task.WhenAll(validators.Get() 33 | .Select(async v => await v.ValidateAsync(new ValidationContext(@object), cancellationToken)))) 34 | .SelectMany(r => r.Errors) 35 | .ToList(); 36 | 37 | if (errors.Any()) 38 | { 39 | return FluentValidationsResult.Failure(errors); 40 | } 41 | else 42 | { 43 | return FluentValidationsResult.Succes(); 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/IOptionBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace MatthiWare.CommandLine.Abstractions 2 | { 3 | /// 4 | /// API for configuring options 5 | /// 6 | public interface IOptionBuilder 7 | { 8 | /// 9 | /// Sets if the option is required 10 | /// 11 | /// Required or not 12 | /// 13 | IOptionBuilder Required(bool required = true); 14 | 15 | /// 16 | /// Help text to be displayed for this option 17 | /// 18 | /// The description of the option 19 | /// 20 | IOptionBuilder Description(string description); 21 | 22 | /// 23 | /// Specify the default value for this option 24 | /// 25 | /// 26 | /// 27 | IOptionBuilder Default(object defaultValue); 28 | 29 | /// 30 | /// Configures the name for the option 31 | /// 32 | /// short name 33 | /// 34 | IOptionBuilder Name(string shortName); 35 | 36 | /// 37 | /// Configures the name for the option 38 | /// 39 | /// Short name 40 | /// Long name 41 | /// 42 | IOptionBuilder Name(string shortName, string longName); 43 | 44 | /// 45 | /// Order in which the option will be parsed 46 | /// 47 | /// 48 | /// 49 | IOptionBuilder Order(int order); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Core/TypedInstanceCacheTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Moq; 4 | using System; 5 | using System.Linq; 6 | using Xunit; 7 | using Xunit.Abstractions; 8 | 9 | namespace MatthiWare.CommandLine.Tests.Core 10 | { 11 | public class TypedInstanceCacheTests : TestBase 12 | { 13 | public TypedInstanceCacheTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 14 | { 15 | } 16 | 17 | [Theory] 18 | [InlineData(true)] 19 | [InlineData(false)] 20 | public void AddingItemsDoesNotTriggerResolve(bool doubleAdd) 21 | { 22 | var containerMock = new Mock(); 23 | 24 | var cache = new TypedInstanceCache(containerMock.Object); 25 | 26 | var type1 = new MyType(); 27 | var type2 = new MyType(); 28 | 29 | cache.Add(type1); 30 | 31 | var result = cache.Get(); 32 | 33 | Assert.Equal(type1, result.First()); 34 | 35 | if (doubleAdd) 36 | { 37 | cache.Add(type2); 38 | 39 | result = cache.Get(); 40 | 41 | Assert.Equal(type2, result.First()); 42 | } 43 | 44 | Assert.True(result.Count == 1); 45 | 46 | containerMock.Verify(c => c.GetService(It.Is(t => t == typeof(MyType))), Times.Never()); 47 | } 48 | 49 | [Fact] 50 | public void AddingItemTypeDoesTriggerResolve() 51 | { 52 | var type1 = new MyType(); 53 | 54 | Services.AddSingleton(type1); 55 | 56 | var cache = new TypedInstanceCache(Services.BuildServiceProvider()); 57 | 58 | cache.Add(typeof(MyType)); 59 | 60 | var result = cache.Get(); 61 | 62 | Assert.Equal(type1, result.First()); 63 | 64 | Assert.True(result.Count == 1); 65 | } 66 | 67 | private class MyType { } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandBuilder'.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Command 5 | { 6 | /// 7 | /// Configures commands using a fluent interface 8 | /// 9 | /// Command options class 10 | /// Base option 11 | public interface ICommandBuilder : ICommandConfigurationBuilder, ICommandExecutor 12 | where TOption : class 13 | where TSource : class, new() 14 | { 15 | /// 16 | /// Configures an option in the model 17 | /// 18 | /// Type of the property 19 | /// Model property to configure 20 | /// 21 | IOptionBuilder Configure(Expression> selector); 22 | 23 | /// 24 | /// Configures if the command is required 25 | /// 26 | /// True or false 27 | /// 28 | new ICommandBuilder Required(bool required = true); 29 | 30 | /// 31 | /// Describes the command, used in the usage output. 32 | /// 33 | /// description of the command 34 | /// 35 | new ICommandBuilder Description(string desc); 36 | 37 | /// 38 | /// Configures the command name 39 | /// 40 | /// name 41 | /// 42 | new ICommandBuilder Name(string name); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/ParserResultTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 5 | using MatthiWare.CommandLine.Core.Parsing; 6 | 7 | using Moq; 8 | 9 | using Xunit; 10 | 11 | namespace MatthiWare.CommandLine.Tests.Parsing 12 | { 13 | public class ParserResultTest 14 | { 15 | [Fact] 16 | public void TestMergeResultOfErrorsWorks() 17 | { 18 | var result = new ParseResult(); 19 | 20 | Assert.Empty(result.Errors); 21 | 22 | var exception1 = new Exception("test"); 23 | 24 | result.MergeResult(new[] { exception1 }); 25 | 26 | Assert.True(result.HasErrors); 27 | Assert.Same(exception1, result.Errors.First()); 28 | 29 | result.MergeResult(new[] { new Exception("2") }); 30 | 31 | Assert.True(result.HasErrors); 32 | Assert.NotSame(exception1, result.Errors.Skip(1).First()); 33 | } 34 | 35 | [Fact] 36 | public void TestMergeResultOfCommandResultWorks() 37 | { 38 | var result = new ParseResult(); 39 | 40 | var mockCmdResult = new Mock(); 41 | 42 | mockCmdResult.SetupGet(x => x.HasErrors).Returns(false); 43 | mockCmdResult.SetupGet(x => x.Errors).Returns(new List()); 44 | 45 | result.MergeResult(mockCmdResult.Object); 46 | 47 | mockCmdResult.VerifyGet(x => x.HasErrors); 48 | 49 | result.AssertNoErrors(); 50 | } 51 | 52 | [Fact] 53 | public void TestMergeResultOfResultWorks() 54 | { 55 | var result = new ParseResult(); 56 | 57 | var obj = new object(); 58 | 59 | result.MergeResult(obj); 60 | 61 | result.AssertNoErrors(); 62 | 63 | Assert.Empty(result.Errors); 64 | 65 | Assert.Same(obj, result.Result); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Command/CommandDiscoverer.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Core.Utils; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reflection; 7 | 8 | namespace MatthiWare.CommandLine.Core.Command 9 | { 10 | /// 11 | public class CommandDiscoverer : ICommandDiscoverer 12 | { 13 | /// 14 | public IReadOnlyList DiscoverCommandTypes(Type optionType, Assembly[] assemblies) 15 | { 16 | var foundCommands = new List(); 17 | 18 | foreach (var assembly in assemblies) 19 | { 20 | FindCommandsInAssembly(assembly, foundCommands, optionType); 21 | } 22 | 23 | return foundCommands.AsReadOnly(); 24 | } 25 | 26 | private void FindCommandsInAssembly(Assembly assembly, List list, Type optionType) 27 | => list.AddRange(assembly.ExportedTypes.Where(t => IsValidCommandType(t, optionType))); 28 | 29 | private static bool IsValidCommandType(Type type, Type optionType) 30 | { 31 | if (type.IsAbstract || !type.IsClass) 32 | { 33 | return false; 34 | } 35 | 36 | bool isAssignableToGenericCommand = type.IsAssignableToGenericType(typeof(Command<>)); 37 | bool isAssignableToCommand = typeof(Abstractions.Command.Command).IsAssignableFrom(type); 38 | 39 | if (!isAssignableToCommand && !isAssignableToGenericCommand) 40 | { 41 | return false; 42 | } 43 | 44 | if (isAssignableToGenericCommand) 45 | { 46 | var firstGenericArgument = type.BaseType.GenericTypeArguments.First(); 47 | 48 | if (optionType != firstGenericArgument) 49 | { 50 | return false; 51 | } 52 | } 53 | 54 | return true; 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/BoolResolver.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | 4 | using MatthiWare.CommandLine.Abstractions.Models; 5 | using MatthiWare.CommandLine.Abstractions.Parsing; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers 9 | { 10 | internal class BoolResolver : BaseArgumentResolver 11 | { 12 | private static readonly string[] recognisedFalseArgs = new[] { "off", "0", "false", "no" }; 13 | private static readonly string[] recognisedTrueArgs = new[] { "on", "1", "true", "yes", string.Empty, null }; 14 | private readonly ILogger logger; 15 | 16 | public BoolResolver(ILogger logger) 17 | { 18 | this.logger = logger; 19 | } 20 | 21 | public override bool CanResolve(ArgumentModel model) => CanResolve(model.Values.FirstOrDefault()); 22 | 23 | public override bool CanResolve(string value) => TryParse(value, out _); 24 | 25 | public override bool Resolve(ArgumentModel model) => Resolve(model.Values.FirstOrDefault()); 26 | 27 | public override bool Resolve(string value) 28 | { 29 | TryParse(value, out bool result); 30 | 31 | return result; 32 | } 33 | 34 | private bool TryParse(string value, out bool result) 35 | { 36 | if (recognisedFalseArgs.Contains(value, StringComparer.InvariantCultureIgnoreCase)) 37 | { 38 | logger.LogDebug("BoolResolver does not recognize {input} as false", value); 39 | result = false; 40 | return true; 41 | } 42 | 43 | if (recognisedTrueArgs.Contains(value, StringComparer.OrdinalIgnoreCase)) 44 | { 45 | logger.LogDebug("BoolResolver does not recognize {input} as true", value); 46 | result = true; 47 | return true; 48 | } 49 | 50 | return bool.TryParse(value, out result); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/OptionParseException.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Models; 3 | 4 | namespace MatthiWare.CommandLine.Core.Exceptions 5 | { 6 | /// 7 | /// Indicates that an option was unable to be parsed 8 | /// This could be caused by an missing . 9 | /// 10 | public class OptionParseException : BaseParserException 11 | { 12 | /// 13 | /// The option that failed 14 | /// 15 | public ICommandLineOption Option => (ICommandLineOption)Argument; 16 | 17 | /// 18 | /// Provided argument model 19 | /// 20 | public ArgumentModel ArgumentModel { get; } 21 | 22 | /// 23 | /// Creates a new 24 | /// 25 | /// The failed option 26 | /// The specified argument 27 | public OptionParseException(ICommandLineOption option, ArgumentModel argModel) 28 | : base(option, CreateMessage(option, argModel)) 29 | { 30 | ArgumentModel = argModel; 31 | } 32 | 33 | private static string CreateMessage(ICommandLineOption option, ArgumentModel argModel) 34 | { 35 | bool hasShort = option.HasShortName; 36 | bool hasLong = option.HasLongName; 37 | bool hasBoth = hasShort && hasLong; 38 | 39 | string optionName = !hasShort && !hasLong ? option.ToString() : string.Empty; 40 | string hasBothSeperator = hasBoth ? "|" : string.Empty; 41 | string shortName = hasShort ? option.ShortName : string.Empty; 42 | string longName = hasLong ? option.LongName : string.Empty; 43 | 44 | return $"Unable to parse option '{shortName}{hasBothSeperator}{longName}{optionName}' value '{string.Join(", ", argModel.Values)}' is invalid!"; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/OptionBuilderTest.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using MatthiWare.CommandLine.Core; 4 | using MatthiWare.CommandLine.Core.Parsing.Resolvers; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using Moq; 7 | using System; 8 | using Xunit; 9 | 10 | namespace MatthiWare.CommandLine.Tests 11 | { 12 | public class OptionBuilderTest 13 | { 14 | [Fact] 15 | public void OptionBuilderConfiguresOptionCorrectly() 16 | { 17 | var resolverMock = new Mock>(); 18 | var serviceProviderMock = new Mock(); 19 | serviceProviderMock.Setup(_ => _.GetService(It.IsAny())).Returns(resolverMock.Object); 20 | 21 | var cmdOption = new CommandLineOption( 22 | new CommandLineParserOptions { PrefixLongOption = string.Empty, PrefixShortOption = string.Empty }, 23 | new object(), 24 | XUnitExtensions.CreateLambda(o => o.ToString()), 25 | new DefaultResolver(NullLogger.Instance, serviceProviderMock.Object), NullLogger.Instance); 26 | 27 | var builder = cmdOption as IOptionBuilder; 28 | var option = cmdOption as CommandLineOptionBase; 29 | 30 | string sDefault = "default"; 31 | string sHelp = "help"; 32 | string sLong = "long"; 33 | string sShort = "short"; 34 | 35 | builder 36 | .Default(sDefault) 37 | .Description(sHelp) 38 | .Name(sShort, sLong) 39 | .Required(); 40 | 41 | Assert.True(option.HasDefault); 42 | Assert.Equal(sDefault, option.DefaultValue); 43 | 44 | Assert.True(option.HasLongName); 45 | Assert.Equal(sLong, option.LongName); 46 | 47 | Assert.Equal(sHelp, option.Description); 48 | 49 | Assert.True(option.HasShortName); 50 | Assert.Equal(sShort, option.ShortName); 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /CommandLineParser.Tests/Utils/ApiTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Runtime.CompilerServices; 6 | using Xunit; 7 | 8 | namespace MatthiWare.CommandLine.Tests.Utils 9 | { 10 | public class ApiTests 11 | { 12 | [Fact] 13 | public void AllObsoleteMembersAreEditorBrowsableNever() 14 | { 15 | foreach (var type in typeof(CommandLineParser).Assembly.GetExportedTypes() 16 | .Where(t => t.IsPublic)) 17 | { 18 | if (type.GetCustomAttribute() != null) 19 | { 20 | var editorBrowsable = type.GetCustomAttribute(); 21 | Assert.True(editorBrowsable != null, $"Type: {type.FullName} should have [EditorBrowsable]"); 22 | Assert.True(editorBrowsable.State == EditorBrowsableState.Never, 23 | $"Type: {type.FullName} should have EditorBrowsable.State == Never"); 24 | } 25 | 26 | foreach (var member in type.GetMembers( 27 | BindingFlags.Public | BindingFlags.Static | BindingFlags.Instance)) 28 | { 29 | if (member.GetCustomAttribute() != null 30 | || member.DeclaringType != type) 31 | { 32 | continue; 33 | } 34 | 35 | if (member.GetCustomAttribute() == null) 36 | { 37 | continue; 38 | } 39 | 40 | var editorBrowsable = member.GetCustomAttribute(); 41 | 42 | Assert.True(editorBrowsable != null, 43 | $"{type.FullName}.{member.Name} should have [EditorBrowsable]"); 44 | 45 | Assert.True(editorBrowsable.State == EditorBrowsableState.Never, 46 | $"{type.FullName}.{member.Name} should have EditorBrowsable.State == Never"); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/ICommandLineArgumentResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | 3 | namespace MatthiWare.CommandLine.Abstractions.Parsing 4 | { 5 | /// 6 | /// Argument resolver 7 | /// 8 | public interface ICommandLineArgumentResolver 9 | { 10 | /// 11 | /// Checks if the resolver can resolve the argument 12 | /// 13 | /// argument 14 | /// True if it can resolve it correctly 15 | bool CanResolve(ArgumentModel model); 16 | 17 | /// 18 | /// Checks if the resolver can resolve the argument 19 | /// 20 | /// Argument 21 | /// True if it can resolve it correctly 22 | bool CanResolve(string value); 23 | 24 | /// 25 | /// Resolves the argument from the model 26 | /// 27 | /// Argument model 28 | /// The resolved type 29 | object Resolve(ArgumentModel model); 30 | 31 | /// 32 | /// Resolves the argument from the model 33 | /// 34 | /// Argument 35 | /// The resolved type 36 | object Resolve(string value); 37 | } 38 | 39 | /// 40 | /// Generic argument resolver 41 | /// 42 | /// Argument type 43 | public interface ICommandLineArgumentResolver : ICommandLineArgumentResolver 44 | { 45 | /// 46 | /// Resolves the argument from the model 47 | /// 48 | /// Argument model 49 | /// The resolved type 50 | new T Resolve(ArgumentModel model); 51 | 52 | /// 53 | /// Resolves the argument from the model 54 | /// 55 | /// Argument 56 | /// The resolved type 57 | new T Resolve(string value); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/CommandLineModelTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Core.Attributes; 2 | 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | 6 | namespace MatthiWare.CommandLine.Tests 7 | { 8 | public class CommandLineModelTests : TestBase 9 | { 10 | public CommandLineModelTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 11 | { 12 | } 13 | 14 | [Fact] 15 | public void TestBasicModel() 16 | { 17 | Services.AddCommandLineParser(); 18 | var parser = ResolveParser(); 19 | 20 | Assert.Equal(1, parser.Options.Count); 21 | 22 | var message = parser.Options[0]; 23 | 24 | Assert.NotNull(message); 25 | 26 | Assert.True(message.HasLongName && message.HasShortName); 27 | 28 | Assert.Equal("-m", message.ShortName); 29 | Assert.Equal("--message", message.LongName); 30 | 31 | Assert.True(message.HasDefault); 32 | Assert.True(message.IsRequired); 33 | 34 | Assert.Equal("Help", message.Description); 35 | } 36 | 37 | [Fact] 38 | public void TestBasicModelWithOverwritingUsingFluentApi() 39 | { 40 | Services.AddCommandLineParser(); 41 | var parser = ResolveParser(); 42 | 43 | parser.Configure(_ => _.Message) 44 | .Required(false) 45 | .Description("Different"); 46 | 47 | Assert.Equal(1, parser.Options.Count); 48 | 49 | var message = parser.Options[0]; 50 | 51 | Assert.NotNull(message); 52 | 53 | Assert.True(message.HasLongName && message.HasShortName); 54 | 55 | Assert.Equal("-m", message.ShortName); 56 | Assert.Equal("--message", message.LongName); 57 | 58 | Assert.True(message.HasDefault); 59 | Assert.False(message.IsRequired); 60 | 61 | Assert.Equal("Different", message.Description); 62 | } 63 | 64 | private class Model 65 | { 66 | [Required, Name("m", "message"), DefaultValue("not found"), Description("Help")] 67 | public string Message { get; set; } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CommandLineParser/DependencyInjectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using MatthiWare.CommandLine.Core; 3 | using System; 4 | using MatthiWare.CommandLine.Abstractions; 5 | 6 | namespace MatthiWare.CommandLine 7 | { 8 | /// 9 | /// Extension methods to allow DI services to be registered. 10 | /// 11 | public static class DependencyInjectionExtensions 12 | { 13 | /// 14 | /// Adds to the services 15 | /// This won't overwrite existing services. 16 | /// 17 | /// Base option type 18 | /// Current service collection 19 | /// Current options reference 20 | public static void AddCommandLineParser(this IServiceCollection services, CommandLineParserOptions options = null) 21 | where TOption : class, new() 22 | { 23 | Func> factory = (IServiceProvider provider) 24 | => new CommandLineParser(provider.GetService(), provider); 25 | 26 | services 27 | .AddInternalCommandLineParserServices2(options) 28 | .AddCommandLineParserFactoryGeneric(factory); 29 | } 30 | 31 | /// 32 | /// Adds to the services 33 | /// This won't overwrite existing services. 34 | /// 35 | /// Current service collection 36 | /// Current options reference 37 | public static void AddCommandLineParser(this IServiceCollection services, CommandLineParserOptions options = null) 38 | { 39 | Func factory = (IServiceProvider provider) 40 | => new CommandLineParser(provider.GetService(), provider); 41 | 42 | services 43 | .AddInternalCommandLineParserServices2(options) 44 | .AddCommandLineParserFactory(factory); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/Collections/SetResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Collections; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers.Collections 9 | { 10 | /// 11 | internal sealed class SetResolver : ISetResolver 12 | { 13 | private readonly ILogger logger; 14 | private readonly IArgumentResolver resolver; 15 | 16 | /// 17 | public SetResolver(ILogger logger, IArgumentResolver resolver) 18 | { 19 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | this.resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); 21 | } 22 | 23 | /// 24 | public bool CanResolve(ArgumentModel model) 25 | { 26 | if (model is null) 27 | { 28 | return false; 29 | } 30 | 31 | foreach (var value in model.Values) 32 | { 33 | if (!resolver.CanResolve(value)) 34 | { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | /// 43 | public bool CanResolve(string value) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | 48 | /// 49 | public HashSet Resolve(ArgumentModel model) 50 | { 51 | var list = new HashSet(); 52 | 53 | foreach (var value in model.Values) 54 | { 55 | list.Add(resolver.Resolve(value)); 56 | } 57 | 58 | return list; 59 | } 60 | 61 | /// 62 | public object Resolve(string value) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | 67 | object ICommandLineArgumentResolver.Resolve(ArgumentModel model) => Resolve(model); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandConfigurationBuilder`.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Command 5 | { 6 | /// 7 | /// Builder for a generic command 8 | /// 9 | /// 10 | public interface ICommandConfigurationBuilder 11 | : ICommandConfigurationBuilder 12 | where TSource : class 13 | { 14 | /// 15 | /// Configures an option in the model 16 | /// 17 | /// Type of the property 18 | /// Model property to configure 19 | /// 20 | IOptionBuilder Configure(Expression> selector); 21 | 22 | /// 23 | /// Configures if the command is required 24 | /// 25 | /// True or false 26 | /// 27 | new ICommandConfigurationBuilder Required(bool required = true); 28 | 29 | /// 30 | /// Configures the description text for the command 31 | /// 32 | /// The description 33 | /// 34 | new ICommandConfigurationBuilder Description(string description); 35 | 36 | /// 37 | /// Configures the command name 38 | /// 39 | /// Command name 40 | /// 41 | new ICommandConfigurationBuilder Name(string name); 42 | 43 | /// 44 | /// Configures if the command should auto execute 45 | /// 46 | /// True for automated execution, false for manual 47 | /// 48 | new ICommandConfigurationBuilder AutoExecute(bool autoExecute); 49 | } 50 | } -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/Collections/ListResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Collections; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | using System.Collections.Generic; 7 | 8 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers.Collections 9 | { 10 | /// 11 | internal sealed class ListResolver : IListResolver 12 | { 13 | private readonly ILogger logger; 14 | private readonly IArgumentResolver resolver; 15 | 16 | /// 17 | public ListResolver(ILogger logger, IArgumentResolver resolver) 18 | { 19 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 20 | this.resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); 21 | } 22 | 23 | /// 24 | public bool CanResolve(ArgumentModel model) 25 | { 26 | if (model is null) 27 | { 28 | return false; 29 | } 30 | 31 | foreach (var value in model.Values) 32 | { 33 | if (!resolver.CanResolve(value)) 34 | { 35 | return false; 36 | } 37 | } 38 | 39 | return true; 40 | } 41 | 42 | /// 43 | public bool CanResolve(string value) 44 | { 45 | throw new NotImplementedException(); 46 | } 47 | 48 | /// 49 | public List Resolve(ArgumentModel model) 50 | { 51 | var list = new List(model.Values.Count); 52 | 53 | foreach (var value in model.Values) 54 | { 55 | list.Add(resolver.Resolve(value)); 56 | } 57 | 58 | return list; 59 | } 60 | 61 | /// 62 | public object Resolve(string value) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | 67 | object ICommandLineArgumentResolver.Resolve(ArgumentModel model) => Resolve(model); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/IParserResult.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using MatthiWare.CommandLine.Abstractions.Command; 6 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 7 | 8 | namespace MatthiWare.CommandLine.Abstractions.Parsing 9 | { 10 | /// 11 | /// Parser result 12 | /// 13 | /// 14 | public interface IParserResult 15 | { 16 | /// 17 | /// Returns true if the user specified a help option 18 | /// 19 | bool HelpRequested { get; } 20 | 21 | /// 22 | /// Help was requested for this or 23 | /// 24 | IArgument HelpRequestedFor { get; } 25 | 26 | /// 27 | /// Parsed result 28 | /// 29 | /// 30 | /// Result contains exceptions. For more info see and properties. 31 | /// 32 | TResult Result { get; } 33 | 34 | /// 35 | /// Returns true if any exceptions occured during parsing. 36 | /// 37 | bool HasErrors { get; } 38 | 39 | /// 40 | /// Contains the thrown exception during parsing. 41 | /// 42 | IReadOnlyCollection Errors { get; } 43 | 44 | /// 45 | /// Executes the commands 46 | /// 47 | /// cancellation token 48 | /// /// 49 | /// Result contains exceptions. For more info see and properties. 50 | /// 51 | Task ExecuteCommandsAsync(CancellationToken cancellationToken); 52 | 53 | /// 54 | /// Read-only collection that contains the parsed commands' results. 55 | /// 56 | /// 57 | IReadOnlyList CommandResults { get; } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /CommandLineParser/CommandLineParser.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | MatthiWare.CommandLine 6 | MatthiWare.CommandLineParser 7 | 0.7.0 8 | Matthias Beerens 9 | MatthiWare 10 | Command Line Parser 11 | Command Line Parser for .NET Core written in .NET Standard 12 | https://github.com/MatthiWare/CommandLineParser.Core 13 | 14 | https://github.com/MatthiWare/CommandLineParser.Core 15 | Commandline parser commandline-parser cli 16 | 7.3 17 | 0.7.0.0 18 | 0.7.0.0 19 | LICENSE 20 | - Change license to MIT 21 | Copyright Matthias Beerens 2018 22 | git 23 | True 24 | README.md 25 | 26 | 27 | 28 | full 29 | true 30 | .\CommandLineParser.xml 31 | 32 | 33 | 34 | .\CommandLineParser.xml 35 | 36 | 37 | 38 | 39 | True 40 | 41 | 42 | 43 | True 44 | \ 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/Command/ICommandParserResult.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace MatthiWare.CommandLine.Abstractions.Parsing.Command 8 | { 9 | /// 10 | /// Results fo the command that has been parsed 11 | /// 12 | public interface ICommandParserResult 13 | { 14 | /// 15 | /// Indicates if the command' method has been executed. 16 | /// 17 | bool Executed { get; } 18 | 19 | /// 20 | /// Indicates if the command has been found. 21 | /// 22 | bool Found { get; } 23 | 24 | /// 25 | /// Specifies the command/option that the help display has been requested for 26 | /// 27 | IArgument HelpRequestedFor { get; } 28 | 29 | /// 30 | /// Returns true if the user specified a help option 31 | /// 32 | bool HelpRequested { get; } 33 | 34 | /// 35 | /// Subcommands of the current command 36 | /// 37 | IReadOnlyCollection SubCommands { get; } 38 | 39 | /// 40 | /// The associated command 41 | /// 42 | ICommandLineCommand Command { get; } 43 | 44 | /// 45 | /// Returns true if any exceptions occured during parsing. 46 | /// 47 | bool HasErrors { get; } 48 | 49 | /// 50 | /// Contains the thrown exception(s) during parsing. 51 | /// 52 | IReadOnlyCollection Errors { get; } 53 | 54 | /// 55 | /// Executes the command async 56 | /// 57 | /// 58 | /// Result contains exceptions. For more info see and properties. 59 | /// 60 | /// A task 61 | Task ExecuteCommandAsync(CancellationToken cancellationToken); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Parsing/IArgumentManager.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Abstractions.Models; 3 | using MatthiWare.CommandLine.Abstractions.Usage; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace MatthiWare.CommandLine.Abstractions.Parsing 8 | { 9 | /// 10 | /// Managers the arguments 11 | /// 12 | public interface IArgumentManager 13 | { 14 | /// 15 | /// Returns a read-only list of arguments that never got processed because they appeared after the flag. 16 | /// 17 | IReadOnlyList UnprocessedArguments { get; } 18 | 19 | /// 20 | /// Returns a read-only list of unused arguments. 21 | /// In most cases this will be mistyped arguments that are not mapped to the actual option/command names. 22 | /// You can pass these arguments inside the to get a suggestion of what could be the correct argument. 23 | /// 24 | IReadOnlyList UnusedArguments { get; } 25 | 26 | /// 27 | /// Returns if the flag was found. 28 | /// 29 | bool StopParsingFlagSpecified { get; } 30 | 31 | /// 32 | /// Tries to get the arguments associated to the current option 33 | /// 34 | /// the argument 35 | /// The result arguments 36 | /// True if arguments are found, false if not 37 | bool TryGetValue(IArgument argument, out ArgumentModel model); 38 | 39 | /// 40 | /// Processes the argument list 41 | /// 42 | /// Input arguments 43 | /// List of processesing errors 44 | /// Container for the commands and options 45 | void Process(IReadOnlyList arguments, IList errors, ICommandLineCommandContainer commandContainer); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Exceptions/CommandParseException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using MatthiWare.CommandLine.Abstractions.Command; 6 | 7 | namespace MatthiWare.CommandLine.Core.Exceptions 8 | { 9 | /// 10 | /// Unable to parse the command 11 | /// 12 | public class CommandParseException : BaseParserException 13 | { 14 | /// 15 | /// Command that caused the parsing error 16 | /// 17 | public ICommandLineCommand Command => (ICommandLineCommand)Argument; 18 | 19 | /// 20 | /// Creates a new command parse exception 21 | /// 22 | /// the failed command 23 | /// collection of inner exception 24 | public CommandParseException(ICommandLineCommand command, IReadOnlyCollection innerExceptions) 25 | : base(command, CreateMessage(command, innerExceptions), new AggregateException(innerExceptions)) 26 | { } 27 | 28 | private static string CreateMessage(ICommandLineCommand command, IReadOnlyCollection exceptions) 29 | { 30 | if (exceptions.Count > 1) 31 | { 32 | return CreateMultipleExceptionsMessage(command, exceptions); 33 | } 34 | else 35 | { 36 | return CreateSingleExceptionMessage(command, exceptions.First()); 37 | } 38 | } 39 | 40 | private static string CreateSingleExceptionMessage(ICommandLineCommand command, Exception exception) 41 | => $"Unable to parse command '{command.Name}' reason: {exception.Message}"; 42 | 43 | private static string CreateMultipleExceptionsMessage(ICommandLineCommand command, IReadOnlyCollection exceptions) 44 | { 45 | var message = new StringBuilder(); 46 | message.AppendLine($"Unable to parse command '{command.Name}' because {exceptions.Count} errors occured"); 47 | 48 | foreach (var exception in exceptions) 49 | { 50 | message.AppendLine($" - {exception.Message}"); 51 | } 52 | 53 | return message.ToString(); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/FluentValidationsExtensions.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | MatthiWare.CommandLine.Extensions.FluentValidations 6 | CommandLineParser.FluentValidations 7 | 0.7.0 8 | Matthias Beerens 9 | MatthiWare 10 | FluentValidations Extension For MatthiWare.CommandLineParser 11 | FluentValidations extension for MatthiWare.CommandLineParser 12 | https://github.com/MatthiWare/CommandLineParser.Core 13 | 14 | https://github.com/MatthiWare/CommandLineParser.Core 15 | Commandline parser commandline-parser cli fluent-validations extension 16 | 7.3 17 | 0.7.0.0 18 | 0.7.0.0 19 | LICENSE 20 | - Update license to MIT 21 | Copyright Matthias Beerens 2019 22 | MatthiWare.CommandLineParser.Extensions.FluentValidations 23 | git 24 | True 25 | 26 | 27 | 28 | .\CommandLineParser.FluentValidations.xml 29 | 30 | 31 | 32 | .\CommandLineParser.FluentValidations.xml 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | True 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Command/CommandParserResult.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Command; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 4 | using MatthiWare.CommandLine.Core.Command; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | 11 | namespace MatthiWare.CommandLine.Core.Parsing.Command 12 | { 13 | internal class CommandParserResult : ICommandParserResult 14 | { 15 | private readonly CommandLineCommandBase m_cmd; 16 | private readonly List commandParserResults = new List(); 17 | private readonly List exceptions = new List(); 18 | 19 | public bool HasErrors { get; private set; } = false; 20 | 21 | public ICommandLineCommand Command => m_cmd; 22 | 23 | public IReadOnlyCollection SubCommands => commandParserResults; 24 | 25 | public bool HelpRequested => HelpRequestedFor != null; 26 | 27 | public IArgument HelpRequestedFor { get; set; } = null; 28 | 29 | public IReadOnlyCollection Errors => exceptions; 30 | 31 | public bool Found => true; 32 | 33 | public bool Executed { get; private set; } = false; 34 | 35 | public CommandParserResult(CommandLineCommandBase command) 36 | { 37 | m_cmd = command; 38 | } 39 | 40 | public void MergeResult(ICollection errors) 41 | { 42 | HasErrors = errors.Any(); 43 | 44 | if (!HasErrors) 45 | { 46 | return; 47 | } 48 | 49 | exceptions.AddRange(errors); 50 | } 51 | 52 | public void MergeResult(ICommandParserResult result) 53 | { 54 | HasErrors |= result.HasErrors; 55 | 56 | if (result.HelpRequested) 57 | { 58 | HelpRequestedFor = result.HelpRequestedFor; 59 | } 60 | 61 | commandParserResults.Add(result); 62 | } 63 | 64 | public async Task ExecuteCommandAsync(CancellationToken cancellationToken) 65 | { 66 | await m_cmd.ExecuteAsync(cancellationToken); 67 | Executed = true; 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/Resolvers/Collections/ArrayResolver.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using MatthiWare.CommandLine.Abstractions.Parsing.Collections; 4 | using Microsoft.Extensions.Logging; 5 | using System; 6 | 7 | namespace MatthiWare.CommandLine.Core.Parsing.Resolvers.Collections 8 | { 9 | /// 10 | internal class ArrayResolver : IArrayResolver 11 | { 12 | private readonly ILogger logger; 13 | private readonly IArgumentResolver resolver; 14 | 15 | /// 16 | public ArrayResolver(ILogger logger, IArgumentResolver resolver) 17 | { 18 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 19 | this.resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); 20 | } 21 | 22 | /// 23 | public bool CanResolve(ArgumentModel model) 24 | { 25 | if (model is null) 26 | { 27 | return false; 28 | } 29 | 30 | foreach (var input in model.Values) 31 | { 32 | if (!resolver.CanResolve(input)) 33 | { 34 | return false; 35 | } 36 | } 37 | 38 | return true; 39 | } 40 | 41 | /// 42 | public bool CanResolve(string value) 43 | { 44 | throw new NotImplementedException(); 45 | } 46 | 47 | /// 48 | public TModel[] Resolve(ArgumentModel model) 49 | { 50 | var array = Array.CreateInstance(typeof(TModel), model.Values.Count); 51 | 52 | for (int i = 0; i < model.Values.Count; i++) 53 | { 54 | var value = resolver.Resolve(model.Values[i]); 55 | array.SetValue(value, i); 56 | } 57 | 58 | return (TModel[])array; 59 | } 60 | 61 | /// 62 | public object Resolve(string value) 63 | { 64 | throw new NotImplementedException(); 65 | } 66 | 67 | object ICommandLineArgumentResolver.Resolve(ArgumentModel model) 68 | => Resolve(model); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Validations/IValidatorsContainer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions.Validations 5 | { 6 | /// 7 | /// Contains all the validators registered 8 | /// 9 | public interface IValidatorsContainer 10 | { 11 | /// 12 | /// Adds a validator 13 | /// 14 | /// 15 | /// 16 | void AddValidator(IValidator validator); 17 | 18 | /// 19 | /// Adds a validator 20 | /// 21 | /// 22 | /// 23 | void AddValidator(Type key, IValidator validator); 24 | 25 | /// 26 | /// Adds a validator 27 | /// 28 | /// 29 | /// 30 | void AddValidator() where V : IValidator; 31 | 32 | /// 33 | /// Adds a validator 34 | /// 35 | /// 36 | /// 37 | void AddValidator(Type key, Type validator); 38 | 39 | /// 40 | /// Checks if a validator exists for a given type 41 | /// 42 | /// 43 | /// 44 | bool HasValidatorFor(); 45 | 46 | /// 47 | /// Checks if a validator exists for a given type 48 | /// 49 | /// 50 | /// 51 | bool HasValidatorFor(Type type); 52 | 53 | /// 54 | /// Returns a read-only list of validators for a given type 55 | /// 56 | /// 57 | /// 58 | IReadOnlyCollection GetValidators(); 59 | 60 | /// 61 | /// Returns a read-only list of validators for a given type 62 | /// 63 | /// 64 | /// 65 | IReadOnlyCollection GetValidators(Type key); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Command 6 | { 7 | /// 8 | /// Generic command builder 9 | /// 10 | /// 11 | public interface ICommandBuilder 12 | { 13 | /// 14 | /// Configures how the command should be invoked. 15 | /// Default behavior is to auto invoke the command. 16 | /// 17 | /// True if the command executor will be invoked (default), false if you want to invoke manually. 18 | /// 19 | ICommandBuilder InvokeCommand(bool invoke); 20 | 21 | /// 22 | /// Configures if the command is required 23 | /// 24 | /// True or false 25 | /// 26 | ICommandBuilder Required(bool required = true); 27 | 28 | /// 29 | /// Describes the command, used in the usage output. 30 | /// 31 | /// description of the command 32 | /// 33 | ICommandBuilder Description(string description); 34 | 35 | /// 36 | /// Configures the command name 37 | /// 38 | /// name 39 | /// 40 | ICommandBuilder Name(string name); 41 | 42 | /// 43 | /// Configures the execution of the command 44 | /// 45 | /// The execution action 46 | /// 47 | ICommandBuilder OnExecuting(Action action); 48 | 49 | /// 50 | /// Configures the execution of the command async 51 | /// 52 | /// The execution action 53 | /// 54 | ICommandBuilder OnExecutingAsync(Func action); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/BasicDITests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Moq; 4 | using Xunit; 5 | using Xunit.Abstractions; 6 | 7 | namespace MatthiWare.CommandLine.Tests 8 | { 9 | public class BasicDITests : TestBase 10 | { 11 | public BasicDITests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 12 | { 13 | } 14 | 15 | [Fact] 16 | public void CommandLineParserUsesInjectedServiceCorrectly() 17 | { 18 | var mockedService = new Mock(); 19 | 20 | mockedService 21 | .Setup(_ => _.Call()) 22 | .Verifiable(); 23 | 24 | Services.AddSingleton(mockedService.Object); 25 | 26 | Services.AddCommandLineParser(); 27 | 28 | var parser = ResolveParser(); 29 | 30 | parser.RegisterCommand(); 31 | 32 | var result = parser.Parse(new[] { "cmd" }); 33 | 34 | result.AssertNoErrors(); 35 | 36 | mockedService.Verify(_ => _.Call(), Times.Once()); 37 | } 38 | 39 | [Fact] 40 | public void CommandLineParserServiceResolvesCorrectly() 41 | { 42 | var mockedService = Mock.Of(); 43 | 44 | Services.AddSingleton(mockedService); 45 | 46 | Services.AddCommandLineParser(); 47 | 48 | var parser = ResolveParser(); 49 | 50 | var resolved = parser.Services.GetRequiredService(); 51 | 52 | Assert.Equal(mockedService, resolved); 53 | } 54 | 55 | public class MyCommandThatUsesService : Command 56 | { 57 | private readonly MySerice serice; 58 | 59 | public MyCommandThatUsesService(MySerice serice) 60 | { 61 | this.serice = serice ?? throw new System.ArgumentNullException(nameof(serice)); 62 | } 63 | 64 | public override void OnConfigure(ICommandConfigurationBuilder builder) 65 | { 66 | builder 67 | .Name("cmd") 68 | .AutoExecute(true) 69 | .Required(true); 70 | } 71 | 72 | public override void OnExecute() 73 | { 74 | base.OnExecute(); 75 | 76 | serice.Call(); 77 | } 78 | } 79 | 80 | public interface MySerice 81 | { 82 | void Call(); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/IOptionBuilder`.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | 4 | namespace MatthiWare.CommandLine.Abstractions 5 | { 6 | /// 7 | /// API for configuring options 8 | /// 9 | public interface IOptionBuilder : IOptionBuilder 10 | { 11 | /// 12 | /// Sets if the option is required 13 | /// 14 | /// Required or not 15 | /// 16 | new IOptionBuilder Required(bool required = true); 17 | 18 | /// 19 | /// Help text to be displayed for this option 20 | /// 21 | /// The description of the option 22 | /// 23 | new IOptionBuilder Description(string description); 24 | 25 | /// 26 | /// Specify the default value for this option 27 | /// 28 | /// 29 | /// 30 | IOptionBuilder Default(TOption defaultValue); 31 | 32 | /// 33 | /// Configures the name for the option 34 | /// 35 | /// short name 36 | /// 37 | new IOptionBuilder Name(string shortName); 38 | 39 | /// 40 | /// Configures the name for the option 41 | /// 42 | /// Short name 43 | /// Long name 44 | /// 45 | new IOptionBuilder Name(string shortName, string longName); 46 | 47 | /// 48 | /// Order in which the option will be parsed 49 | /// 50 | /// 51 | /// 52 | new IOptionBuilder Order(int order); 53 | 54 | /// 55 | /// Transforms the parsed value using the transform function 56 | /// 57 | /// Transformation function 58 | /// 59 | IOptionBuilder Transform(Expression> transformation); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '24 8 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'csharp' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v2 39 | 40 | # Initializes the CodeQL tools for scanning. 41 | - name: Initialize CodeQL 42 | uses: github/codeql-action/init@v1 43 | with: 44 | languages: ${{ matrix.language }} 45 | # If you wish to specify custom queries, you can do so here or in a config file. 46 | # By default, queries listed here will override any specified in a config file. 47 | # Prefix the list here with "+" to use these queries and those in the config file. 48 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 49 | 50 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 51 | # If this step fails, then you should remove it and run the build manually (see below) 52 | - name: Autobuild 53 | uses: github/codeql-action/autobuild@v1 54 | 55 | # ℹ️ Command-line programs to run using the OS shell. 56 | # 📚 https://git.io/JvXDl 57 | 58 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 59 | # and modify them (or add more) to build your code if your project 60 | # uses a compiled language 61 | 62 | #- run: | 63 | # make bootstrap 64 | # make release 65 | 66 | - name: Perform CodeQL Analysis 67 | uses: github/codeql-action/analyze@v1 68 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Usage/IUsageBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using MatthiWare.CommandLine.Abstractions.Command; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Usage 6 | { 7 | /// 8 | /// Output builder 9 | /// 10 | public interface IUsageBuilder 11 | { 12 | /// 13 | /// Generates the output 14 | /// 15 | /// Output string 16 | string Build(); 17 | 18 | /// 19 | /// Add usage 20 | /// 21 | /// Name of the applpication 22 | /// Indicates if the output contains options 23 | /// Indicates if the output contains commands 24 | void AddUsage(string name, bool hasOptions, bool hasCommands); 25 | 26 | /// 27 | /// Add all options 28 | /// 29 | /// 30 | void AddOptions(IEnumerable options); 31 | 32 | /// 33 | /// Add a specific option 34 | /// 35 | /// 36 | void AddOption(ICommandLineOption option); 37 | 38 | /// 39 | /// Adds all command descriptions 40 | /// 41 | /// 42 | void AddCommandDescriptions(IEnumerable commands); 43 | 44 | /// 45 | /// Adds a specific command description 46 | /// 47 | /// 48 | void AddCommandDescription(ICommandLineCommand command); 49 | 50 | /// 51 | /// Adds a command to the output builder 52 | /// 53 | /// 54 | /// 55 | void AddCommand(string name, ICommandLineCommand command); 56 | 57 | /// 58 | /// Adds a command to the output builder 59 | /// 60 | /// 61 | /// 62 | void AddCommand(string name, ICommandLineCommandContainer container); 63 | 64 | /// 65 | /// Adds the errors to the output builder 66 | /// 67 | /// 68 | void AddErrors(IReadOnlyCollection errors); 69 | 70 | void AddSuggestionHeader(string inputKey); 71 | void AddSuggestion(string suggestion); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Usage/IUsagePrinter.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.ComponentModel; 6 | 7 | namespace MatthiWare.CommandLine.Abstractions.Usage 8 | { 9 | /// 10 | /// CLI Usage Output Printer 11 | /// 12 | public interface IUsagePrinter 13 | { 14 | /// 15 | /// Gets the usage builder 16 | /// 17 | IUsageBuilder Builder { get; } 18 | 19 | /// 20 | /// Print global usage 21 | /// 22 | void PrintUsage(); 23 | 24 | /// 25 | /// Print an argument 26 | /// 27 | /// The given argument 28 | [Obsolete("Use PrintCommandUsage or PrintOptionUsage instead")] 29 | [EditorBrowsable(EditorBrowsableState.Never)] 30 | void PrintUsage(IArgument argument); 31 | 32 | /// 33 | /// Print command usage 34 | /// 35 | /// The given command 36 | [Obsolete("Use PrintCommandUsage instead")] 37 | [EditorBrowsable(EditorBrowsableState.Never)] 38 | void PrintUsage(ICommandLineCommand command); 39 | 40 | /// 41 | /// Print command usage 42 | /// 43 | /// The given command 44 | void PrintCommandUsage(ICommandLineCommand command); 45 | 46 | /// 47 | /// Print option usage 48 | /// 49 | /// The given option 50 | [EditorBrowsable(EditorBrowsableState.Never)] 51 | [Obsolete("Use PrintCommandUsage instead")] 52 | void PrintUsage(ICommandLineOption option); 53 | 54 | /// 55 | /// Print option usage 56 | /// 57 | /// The given option 58 | void PrintOptionUsage(ICommandLineOption option); 59 | 60 | /// 61 | /// Print errors 62 | /// 63 | /// list of errors 64 | void PrintErrors(IReadOnlyCollection errors); 65 | 66 | /// 67 | /// Prints suggestions based on the input arguments 68 | /// 69 | /// Input model 70 | /// True if a suggestion was found and printed, otherwise false 71 | bool PrintSuggestion(UnusedArgumentModel model); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /CommandLineParser/CommandLineParserOptions.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace MatthiWare.CommandLine 4 | { 5 | /// 6 | /// Configuration options for 7 | /// 8 | public class CommandLineParserOptions 9 | { 10 | /// 11 | /// Prefix for the short option 12 | /// 13 | public string PrefixShortOption { get; set; } = "-"; 14 | 15 | /// 16 | /// Prefix for the long option 17 | /// 18 | public string PrefixLongOption { get; set; } = "--"; 19 | 20 | /// 21 | /// Postfix for the long option 22 | /// 23 | public string PostfixOption { get; set; } = "="; 24 | 25 | /// 26 | /// Stops parsing of remaining arguments after this has been found 27 | /// 28 | public string StopParsingAfter { get; set; } 29 | 30 | /// 31 | /// Help option name. 32 | /// Accepts both formatted and unformatted help name. 33 | /// If the name is a single string it will use the 34 | /// If the name is split for example h|help it will use the following format |]]> 35 | /// 36 | public string HelpOptionName { get; set; } = "h|help"; 37 | 38 | /// 39 | /// Enable or disable the help option 40 | /// 41 | /// 42 | public bool EnableHelpOption { get; set; } = true; 43 | 44 | /// 45 | /// Enables or disables the automatic usage and error printing 46 | /// 47 | public bool AutoPrintUsageAndErrors { get; set; } = true; 48 | 49 | /// 50 | /// Sets the application name. Will use the by default if none is specified. 51 | /// 52 | public string AppName { get; set; } 53 | 54 | internal (string shortOption, string longOption) GetConfiguredHelpOption() 55 | { 56 | var tokens = HelpOptionName.Split('|'); 57 | 58 | string shortResult; 59 | string longResult = null; 60 | 61 | if (tokens.Length > 1) 62 | { 63 | shortResult = $"{PrefixShortOption}{tokens[0]}"; 64 | longResult = $"{PrefixLongOption}{tokens[1]}"; 65 | } 66 | else 67 | { 68 | shortResult = $"{PrefixLongOption}{tokens[0]}"; 69 | } 70 | 71 | return (shortResult, longResult); 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Usage/NoColorOutputTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Usage; 2 | using MatthiWare.CommandLine.Core.Attributes; 3 | using MatthiWare.CommandLine.Core.Usage; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Moq; 6 | using System; 7 | using System.Collections.Generic; 8 | using Xunit; 9 | using Xunit.Abstractions; 10 | 11 | namespace MatthiWare.CommandLine.Tests.Usage 12 | { 13 | [Collection("Non-Parallel Collection")] 14 | public class NoColorOutputTests : TestBase 15 | { 16 | private readonly CommandLineParser parser; 17 | private readonly IEnvironmentVariablesService variablesService; 18 | private Action consoleColorGetter; 19 | private bool variableServiceResult; 20 | 21 | public NoColorOutputTests(ITestOutputHelper output) : base(output) 22 | { 23 | var envMock = new Mock(); 24 | envMock.SetupGet(env => env.NoColorRequested).Returns(() => variableServiceResult); 25 | 26 | var consoleMock = new Mock(); 27 | 28 | variablesService = envMock.Object; 29 | 30 | var usageBuilderMock = new Mock(); 31 | usageBuilderMock.Setup(m => m.AddErrors(It.IsAny>())).Callback(() => 32 | { 33 | consoleColorGetter(consoleMock.Object.ForegroundColor); 34 | }); 35 | 36 | Services.AddSingleton(envMock.Object); 37 | Services.AddSingleton(consoleMock.Object); 38 | Services.AddSingleton(usageBuilderMock.Object); 39 | 40 | parser = new CommandLineParser(Services); 41 | } 42 | 43 | [Fact] 44 | public void CheckUsageOutputRespectsNoColor() 45 | { 46 | ParseAndCheckNoColor(false); 47 | ParseAndCheckNoColor(true); 48 | } 49 | 50 | private void ParseAndCheckNoColor(bool noColorOuput) 51 | { 52 | consoleColorGetter = noColorOuput ? (Action)AssertNoColor : AssertColor; 53 | 54 | variableServiceResult = noColorOuput; 55 | 56 | parser.Parse(new string[] { "alpha" }); 57 | } 58 | 59 | private void AssertNoColor(ConsoleColor color) 60 | { 61 | Assert.True(variablesService.NoColorRequested); 62 | Assert.NotEqual(ConsoleColor.Red, color); 63 | } 64 | 65 | private void AssertColor(ConsoleColor color) 66 | { 67 | Assert.False(variablesService.NoColorRequested); 68 | Assert.Equal(ConsoleColor.Red, color); 69 | } 70 | 71 | private class Options 72 | { 73 | [Required, Name("b")] 74 | public bool MyBool { get; set; } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Models/ModelInitializer.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using MatthiWare.CommandLine.Abstractions.Models; 3 | using MatthiWare.CommandLine.Core.Attributes; 4 | using MatthiWare.CommandLine.Core.Utils; 5 | using System; 6 | using System.Linq.Expressions; 7 | using System.Reflection; 8 | 9 | namespace MatthiWare.CommandLine.Core.Models 10 | { 11 | /// 12 | public class ModelInitializer : IModelInitializer 13 | { 14 | /// 15 | public void InitializeModel(Type optionType, object caller, string configureMethodName, string registerMethodName) 16 | { 17 | var properties = optionType.GetProperties(); 18 | 19 | foreach (var propInfo in properties) 20 | { 21 | var attributes = propInfo.GetCustomAttributes(true); 22 | 23 | var lambda = propInfo.GetLambdaExpression(out string key); 24 | 25 | var cfg = caller.GetType().GetMethod(configureMethodName, BindingFlags.NonPublic | BindingFlags.Instance); 26 | 27 | foreach (var attribute in attributes) 28 | { 29 | switch (attribute) 30 | { 31 | case RequiredAttribute required: 32 | GetOption(cfg, propInfo, lambda, key).Required(required.Required); 33 | break; 34 | case DefaultValueAttribute defaultValue: 35 | GetOption(cfg, propInfo, lambda, key).Default(defaultValue.DefaultValue); 36 | break; 37 | case DescriptionAttribute helpText: 38 | GetOption(cfg, propInfo, lambda, key).Description(helpText.Description); 39 | break; 40 | case NameAttribute name: 41 | GetOption(cfg, propInfo, lambda, key).Name(name.ShortName, name.LongName); 42 | break; 43 | case OptionOrderAttribute order: 44 | GetOption(cfg, propInfo, lambda, key).Order(order.Order); 45 | break; 46 | } 47 | } 48 | 49 | var commandType = propInfo.PropertyType; 50 | 51 | bool isAssignableToCommand = typeof(Abstractions.Command.Command).IsAssignableFrom(commandType); 52 | 53 | if (isAssignableToCommand) 54 | { 55 | caller.ExecuteGenericRegisterCommand(registerMethodName, commandType); 56 | } 57 | } 58 | 59 | IOptionBuilder GetOption(MethodInfo method, PropertyInfo prop, LambdaExpression lambda, string key) 60 | { 61 | return method.InvokeGenericMethod(prop, caller, lambda, key) as IOptionBuilder; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /CommandLineParser/CommandLineParser.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.ComponentModel; 5 | 6 | namespace MatthiWare.CommandLine 7 | { 8 | /// 9 | /// Command line parser 10 | /// 11 | public class CommandLineParser : CommandLineParser, ICommandLineParser 12 | { 13 | /// 14 | /// Creates a new instance of the commandline parser 15 | /// 16 | public CommandLineParser() 17 | : base() 18 | { } 19 | 20 | /// 21 | /// Creates a new instance of the commandline parser 22 | /// 23 | /// The parser options 24 | public CommandLineParser(CommandLineParserOptions parserOptions) 25 | : base(parserOptions) 26 | { } 27 | 28 | /// 29 | /// Creates a new instance of the commandline parser 30 | /// 31 | /// Services collection to use, if null will create an internal one 32 | [EditorBrowsable(EditorBrowsableState.Never)] 33 | [Obsolete("Use extension method 'AddCommandLineParser' to register the parser with DI")] 34 | public CommandLineParser(IServiceCollection serviceCollection) 35 | : this(new CommandLineParserOptions(), serviceCollection) 36 | { } 37 | 38 | /// 39 | /// Creates a new instance of the commandline parser 40 | /// 41 | /// options that the parser will use 42 | /// Services collection to use, if null will create an internal one 43 | [EditorBrowsable(EditorBrowsableState.Never)] 44 | [Obsolete("Use extension method 'AddCommandLineParser' to register the parser with DI")] 45 | public CommandLineParser(CommandLineParserOptions parserOptions, IServiceCollection serviceCollection) 46 | : base(parserOptions, serviceCollection) 47 | { } 48 | 49 | /// 50 | /// Creates a new instance of the commandline parser 51 | /// 52 | /// Services provider to use 53 | public CommandLineParser(IServiceProvider serviceProvider) 54 | : this(new CommandLineParserOptions(), serviceProvider) 55 | { } 56 | 57 | /// 58 | /// Creates a new instance of the commandline parser 59 | /// 60 | /// options that the parser will use 61 | /// Services Provider to use 62 | public CommandLineParser(CommandLineParserOptions parserOptions, IServiceProvider serviceProvider) 63 | : base(parserOptions, serviceProvider) 64 | { } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Parsing/ParseResult'.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | using MatthiWare.CommandLine.Abstractions; 7 | using MatthiWare.CommandLine.Abstractions.Parsing; 8 | using MatthiWare.CommandLine.Abstractions.Parsing.Command; 9 | 10 | namespace MatthiWare.CommandLine.Core.Parsing 11 | { 12 | internal class ParseResult : IParserResult 13 | { 14 | private readonly List commandParserResults = new List(); 15 | private readonly List exceptions = new List(); 16 | 17 | #region Properties 18 | 19 | public TResult Result { get; private set; } 20 | 21 | public bool HasErrors { get; private set; } = false; 22 | 23 | public IReadOnlyList CommandResults => commandParserResults.AsReadOnly(); 24 | 25 | public bool HelpRequested => HelpRequestedFor != null; 26 | 27 | public IArgument HelpRequestedFor { get; set; } = null; 28 | 29 | public IReadOnlyCollection Errors => exceptions; 30 | 31 | #endregion 32 | 33 | public void MergeResult(ICommandParserResult result) 34 | { 35 | HasErrors |= result.HasErrors; 36 | 37 | if (result.HelpRequested) 38 | { 39 | HelpRequestedFor = result.HelpRequestedFor; 40 | } 41 | 42 | commandParserResults.Add(result); 43 | } 44 | 45 | public void MergeResult(ICollection errors) 46 | { 47 | if (!errors.Any()) 48 | { 49 | return; 50 | } 51 | 52 | HasErrors = true; 53 | 54 | exceptions.AddRange(errors); 55 | } 56 | 57 | public void MergeResult(TResult result) 58 | { 59 | this.Result = result; 60 | } 61 | 62 | public async Task ExecuteCommandsAsync(CancellationToken cancellationToken) 63 | { 64 | if (HasErrors) 65 | { 66 | throw new InvalidOperationException("Parsing failed, commands might be corrupted."); 67 | } 68 | 69 | await ExecuteCommandsInternal(CommandResults, cancellationToken); 70 | } 71 | 72 | private async Task ExecuteCommandsInternal(IReadOnlyCollection commandParserResults, CancellationToken cancellationToken) 73 | { 74 | // execute parent commands first 75 | foreach (var cmdResult in commandParserResults) 76 | { 77 | await cmdResult.ExecuteCommandAsync(cancellationToken); 78 | } 79 | 80 | // execute child commands 81 | foreach (var cmdResult in commandParserResults) 82 | { 83 | await ExecuteCommandsInternal(cmdResult.SubCommands, cancellationToken); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/CustomerReportedTests.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using MatthiWare.CommandLine.Core.Attributes; 3 | using Xunit; 4 | 5 | namespace MatthiWare.CommandLine.Tests 6 | { 7 | public class CustomerReportedTests 8 | { 9 | #region Issue_12 10 | /// 11 | /// Running with *no* parameters at all crashes the command line parser #12 12 | /// https://github.com/MatthiWare/CommandLineParser.Core/issues/12 13 | /// 14 | [Theory] 15 | [InlineData(true, false, false)] 16 | [InlineData(false, false, false)] 17 | [InlineData(true, false, true)] 18 | [InlineData(false, false, true)] 19 | public void NoCommandLineArgumentsCrashesParser_Issue_12(bool required, bool outcome, bool empty) 20 | { 21 | var parser = new CommandLineParser(); 22 | 23 | parser.Configure(opt => opt.Test) 24 | .Name("1") 25 | .Default(1) 26 | .Required(required); 27 | 28 | var parsed = parser.Parse(empty ? new string[] { } : new[] { "app.exe" }); 29 | 30 | Assert.NotNull(parsed); 31 | 32 | Assert.Equal(outcome, parsed.HasErrors); 33 | } 34 | 35 | private class OptionsModelIssue_12 36 | { 37 | public int Test { get; set; } 38 | } 39 | #endregion 40 | 41 | #region Issue_30_AutoPrintUsageAndErrors 42 | /// 43 | /// AutoPrintUsageAndErrors always prints even when everything is *fine* #30 44 | /// https://github.com/MatthiWare/CommandLineParser.Core/issues/30 45 | /// 46 | [Theory] 47 | [InlineData(null, null)] 48 | [InlineData("true", null)] 49 | [InlineData("false", null)] 50 | [InlineData(null, "bla")] 51 | public void AutoPrintUsageAndErrorsShouldNotPrintWhenEverythingIsFIne(string verbose, string path) 52 | { 53 | var parser = new CommandLineParser(); 54 | 55 | var items = new List(); 56 | AddItemToArray(verbose); 57 | AddItemToArray(path); 58 | 59 | var parsed = parser.Parse(items.ToArray()); 60 | 61 | parsed.AssertNoErrors(); 62 | 63 | void AddItemToArray(string item) 64 | { 65 | if (item != null) 66 | { 67 | items.Add(item); 68 | } 69 | } 70 | } 71 | 72 | private class OptionsModelIssue_30 73 | { 74 | [Required] 75 | [Name("v", "verb")] 76 | [DefaultValue(true)] 77 | [Description("Verbose description")] 78 | public bool Verbose { get; set; } 79 | 80 | [Required] 81 | [Name("p", "path")] 82 | [DefaultValue(@"C:\Some\Path")] 83 | [Description("Path description")] 84 | public string Path { get; set; } 85 | } 86 | #endregion 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /CommandLineParser/Abstractions/Command/ICommandExecutor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace MatthiWare.CommandLine.Abstractions.Command 6 | { 7 | /// 8 | /// API for configurion command executions 9 | /// 10 | /// Base option 11 | /// Command option 12 | public interface ICommandExecutor 13 | where TOption : class 14 | where TSource : class, new() 15 | { 16 | /// 17 | /// Configures how the command should be invoked. 18 | /// Default behavior is to auto invoke the command. 19 | /// 20 | /// True if the command executor will be invoked (default), false if you want to invoke manually. 21 | /// 22 | ICommandBuilder InvokeCommand(bool invoke); 23 | 24 | /// 25 | /// Sets the command execute action 26 | /// 27 | /// Action to execute 28 | /// 29 | ICommandBuilder OnExecuting(Action action); 30 | 31 | /// 32 | /// Sets the command execute action 33 | /// 34 | /// Action to execute 35 | /// 36 | ICommandBuilder OnExecuting(Action action); 37 | 38 | /// 39 | /// Sets the command execute action 40 | /// 41 | /// Action to execute 42 | /// 43 | ICommandBuilder OnExecuting(Action action); 44 | 45 | /// 46 | /// Sets the async command execute action 47 | /// 48 | /// Action to execute 49 | /// A task of 50 | ICommandBuilder OnExecutingAsync(Func action); 51 | 52 | /// 53 | /// Sets the async command execute action 54 | /// 55 | /// Action to execute 56 | /// A task of 57 | ICommandBuilder OnExecutingAsync(Func action); 58 | 59 | /// 60 | /// Sets the async command execute action 61 | /// 62 | /// Action to execute 63 | /// A task of 64 | ICommandBuilder OnExecutingAsync(Func action); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /CommandLineParser/Core/Validations/ValidatorsContainer.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Validations; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace MatthiWare.CommandLine.Core.Validations 8 | { 9 | internal class ValidatorsContainer : IValidatorsContainer 10 | { 11 | private readonly Dictionary> m_types = new Dictionary>(); 12 | private readonly Dictionary> m_cache = new Dictionary>(); 13 | 14 | private readonly IServiceProvider serviceProvider; 15 | 16 | public ValidatorsContainer(IServiceProvider serviceProvider) 17 | { 18 | this.serviceProvider = serviceProvider; 19 | } 20 | 21 | public void AddValidator(Type key, IValidator validator) 22 | { 23 | GetOrCreateTypeListFor(key).Add(validator.GetType()); 24 | GetOrCreateCacheListFor(key).Add((key, validator)); 25 | } 26 | 27 | public void AddValidator(IValidator validator) => AddValidator(typeof(TKey), validator); 28 | 29 | public void AddValidator() where V : IValidator => AddValidator(typeof(TKey), typeof(V)); 30 | 31 | public void AddValidator(Type key, Type validator) => GetOrCreateTypeListFor(key).Add(validator); 32 | 33 | public IReadOnlyCollection GetValidators() 34 | { 35 | var key = typeof(T); 36 | 37 | return GetValidators(key); 38 | } 39 | 40 | public IReadOnlyCollection GetValidators(Type key) 41 | { 42 | var types = GetOrCreateTypeListFor(key); 43 | var instances = GetOrCreateCacheListFor(key); 44 | 45 | var typesNotInList = types.Except(instances.Select(kvp => kvp.validator.GetType())).ToArray(); 46 | 47 | foreach (var type in typesNotInList) 48 | { 49 | var instance = (IValidator) ActivatorUtilities.GetServiceOrCreateInstance(serviceProvider, type); 50 | 51 | instances.Add((type, instance)); 52 | } 53 | 54 | return instances.Select(kvp => kvp.validator).ToArray(); 55 | } 56 | 57 | public bool HasValidatorFor() => HasValidatorFor(typeof(T)); 58 | 59 | public bool HasValidatorFor(Type type) => m_types.ContainsKey(type); 60 | 61 | private List<(Type key, IValidator validator)> GetOrCreateCacheListFor(Type key) 62 | { 63 | if (!m_cache.TryGetValue(key, out List<(Type, IValidator)> instances)) 64 | { 65 | instances = new List<(Type, IValidator)>(); 66 | m_cache.Add(key, instances); 67 | } 68 | 69 | return instances; 70 | } 71 | 72 | private List GetOrCreateTypeListFor(Type key) 73 | { 74 | if (!m_types.TryGetValue(key, out List types)) 75 | { 76 | types = new List(); 77 | m_types.Add(key, types); 78 | } 79 | 80 | return types; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Resolvers/DefaultResolverTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Models; 2 | using MatthiWare.CommandLine.Abstractions.Parsing; 3 | using MatthiWare.CommandLine.Core.Parsing.Resolvers; 4 | using Microsoft.Extensions.DependencyInjection; 5 | using Microsoft.Extensions.Logging.Abstractions; 6 | using System; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace MatthiWare.CommandLine.Tests.Parsing.Resolvers 11 | { 12 | public class DefaultResolverTests 13 | : BaseResolverTests 14 | { 15 | public DefaultResolverTests(ITestOutputHelper outputHelper) : base(outputHelper) 16 | { 17 | } 18 | 19 | [Fact] 20 | public void ThrowsExceptionInCorrectPlaces() 21 | { 22 | Assert.Throws(() => new DefaultResolver(null, null)); 23 | Assert.Throws(() => new DefaultResolver(NullLogger.Instance, null)); 24 | } 25 | 26 | [Fact] 27 | public void WorksCorrectlyWithNullValues() 28 | { 29 | var resolver = new DefaultResolver(NullLogger.Instance, ServiceProvider); 30 | 31 | Assert.False(resolver.CanResolve((ArgumentModel)null)); 32 | Assert.False(resolver.CanResolve((string)null)); 33 | Assert.Null(resolver.Resolve((ArgumentModel)null)); 34 | Assert.Null(resolver.Resolve((string)null)); 35 | } 36 | 37 | [Theory] 38 | [InlineData(true, "-m", "test")] 39 | [InlineData(true, "-m", "my string")] 40 | public void TestCanResolve(bool expected, string key, string value) 41 | { 42 | var resolver = ServiceProvider.GetRequiredService>(); 43 | var model = new ArgumentModel(key, value); 44 | 45 | Assert.Equal(expected, resolver.CanResolve(model)); 46 | Assert.Equal(expected, resolver.CanResolve(value)); 47 | } 48 | 49 | [Theory] 50 | [InlineData(false, "-m", "test")] 51 | [InlineData(false, "-m", "my string")] 52 | public void TestCanResolveWithWrongCtor(bool expected, string key, string value) 53 | { 54 | var resolver = ServiceProvider.GetRequiredService>(); 55 | var model = new ArgumentModel(key, value); 56 | 57 | Assert.Equal(expected, resolver.CanResolve(model)); 58 | Assert.Equal(expected, resolver.CanResolve(value)); 59 | } 60 | 61 | [Theory] 62 | [InlineData("test", "-m", "test")] 63 | [InlineData("my string", "-m", "my string")] 64 | public void TestResolve(string expected, string key, string value) 65 | { 66 | var resolver = ServiceProvider.GetRequiredService>(); 67 | var model = new ArgumentModel(key, value); 68 | 69 | Assert.Equal(expected, resolver.Resolve(model).Result); 70 | Assert.Equal(expected, resolver.Resolve(value).Result); 71 | } 72 | 73 | public class MyTestType 74 | { 75 | public MyTestType(string ctor) 76 | { 77 | Result = ctor; 78 | } 79 | 80 | public string Result { get; } 81 | } 82 | 83 | public class MyTestType2 84 | { 85 | public MyTestType2(int someInt) 86 | { 87 | } 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/CommandLineParser.FluentValidations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CommandLineParser.FluentValidations 5 | 6 | 7 | 8 | 9 | Configuration for fluent validations 10 | 11 | 12 | 13 | 14 | Creates a new fluent validation configuration 15 | 16 | 17 | 18 | 19 | 20 | 21 | Adds a validator 22 | 23 | type to validate 24 | Validator type 25 | Self 26 | 27 | 28 | 29 | Adds a validator 30 | 31 | Type to validate 32 | Validator type 33 | Self 34 | 35 | 36 | 37 | Adds an instantiated validator 38 | 39 | Type to validate 40 | Validator type 41 | Instance 42 | Self 43 | 44 | 45 | 46 | Add an instantiated validator 47 | 48 | Type to validate 49 | Validator instance 50 | Self 51 | 52 | 53 | 54 | FluentValidations Extensions for CommandLineParser 55 | 56 | 57 | 58 | 59 | Extensions to configure FluentValidations for the Parser 60 | 61 | 62 | 63 | Configuration action 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![.NET Core](https://github.com/MatthiWare/CommandLineParser.Core/actions/workflows/dotnet-core.yml/badge.svg)](https://github.com/MatthiWare/CommandLineParser.Core/actions/workflows/dotnet-core.yml) 2 | [![Issues](https://img.shields.io/github/issues/MatthiWare/CommandLineParser.Core.svg)](https://github.com/MatthiWare/CommandLineParser.Core/issues) 3 | [![CodeCov](https://codecov.io/gh/MatthiWare/CommandLineParser.Core/branch/master/graph/badge.svg)](https://codecov.io/gh/MatthiWare/CommandLineParser.Core) 4 | [![CodeFactor](https://www.codefactor.io/repository/github/matthiware/commandlineparser.core/badge)](https://www.codefactor.io/repository/github/matthiware/commandlineparser.core) 5 | [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://tldrlegal.com/license/mit-license) 6 | [![Nuget](https://buildstats.info/nuget/MatthiWare.CommandLineParser)](https://www.nuget.org/packages/MatthiWare.CommandLineParser) 7 | 8 | # CommandLineParser 9 | 10 | A simple, light-weight and strongly typed commandline parser made in .NET Standard! 11 | 12 | ## Installation 13 | ```powershell 14 | PM> Install-Package MatthiWare.CommandLineParser 15 | ``` 16 | 17 | # Quick Start 18 | 19 | Example of configuring the port option using the Fluent API. 20 | 21 | ``` csharp 22 | static void Main(string[] args) 23 | { 24 | // create the parser 25 | var parser = new CommandLineParser(); 26 | 27 | // configure the options using the Fluent API 28 | parser.Configure(options => options.Port) 29 | .Name("p", "port") 30 | .Description("The port of the server") 31 | .Required(); 32 | 33 | // parse 34 | var parserResult = parser.Parse(args); 35 | 36 | // check for parsing errors 37 | if (parserResult.HasErrors) 38 | { 39 | Console.ReadKey(); 40 | 41 | return -1; 42 | } 43 | 44 | var serverOptions = parserResult.Result; 45 | 46 | Console.WriteLine($"Parsed port is {serverOptions.Port}"); 47 | } 48 | 49 | private class ServerOptions 50 | { 51 | // options 52 | public int Port { get; set; } 53 | } 54 | 55 | ``` 56 | 57 | Run command line 58 | 59 | ```shell 60 | dotnet myapp --port 2551 61 | ``` 62 | 63 | #### For more advanced configuration options see [the wiki](https://github.com/MatthiWare/CommandLineParser.Core/wiki). 64 | 65 | 66 | # Contributors 67 | 68 | [![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/0)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/0)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/1)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/1)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/2)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/2)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/3)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/3)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/4)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/4)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/5)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/5)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/6)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/6)[![](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/images/7)](https://sourcerer.io/fame/Matthiee/MatthiWare/CommandLineParser.Core/links/7) 69 | -------------------------------------------------------------------------------- /CommandLineParser/Core/CommandLineOption`.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq.Expressions; 3 | using MatthiWare.CommandLine.Abstractions; 4 | using MatthiWare.CommandLine.Abstractions.Parsing; 5 | using MatthiWare.CommandLine.Core.Utils; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace MatthiWare.CommandLine.Core 9 | { 10 | internal class CommandLineOption : CommandLineOptionBase, IOptionBuilder 11 | { 12 | public CommandLineOption(CommandLineParserOptions parserOptions, object source, LambdaExpression selector, IArgumentResolver argumentResolver, ILogger logger) 13 | : base(parserOptions, source, selector, argumentResolver, logger) 14 | { 15 | this.AllowMultipleValues = typeof(TOption).CanHaveMultipleValues(); 16 | } 17 | 18 | public IOptionBuilder Required(bool required = true) 19 | { 20 | ((IOptionBuilder)this).Required(required); 21 | 22 | return this; 23 | } 24 | 25 | public new IOptionBuilder Description(string description) 26 | { 27 | ((IOptionBuilder)this).Description(description); 28 | 29 | return this; 30 | } 31 | 32 | public IOptionBuilder Default(TOption defaultValue) 33 | { 34 | ((IOptionBuilder)this).Default(defaultValue); 35 | 36 | return this; 37 | } 38 | 39 | public IOptionBuilder Name(string shortName) 40 | { 41 | ((IOptionBuilder)this).Name(shortName); 42 | 43 | return this; 44 | } 45 | 46 | public IOptionBuilder Name(string shortName, string longName) 47 | { 48 | ((IOptionBuilder)this).Name(shortName, longName); 49 | 50 | return this; 51 | } 52 | 53 | public IOptionBuilder Transform(Expression> transformation) 54 | { 55 | SetTranslator(transformation.Compile()); 56 | 57 | return this; 58 | } 59 | 60 | IOptionBuilder IOptionBuilder.Required(bool required) 61 | { 62 | IsRequired = required; 63 | 64 | return this; 65 | } 66 | 67 | IOptionBuilder IOptionBuilder.Description(string description) 68 | { 69 | base.Description = description; 70 | 71 | return this; 72 | } 73 | 74 | IOptionBuilder IOptionBuilder.Default(object defaultValue) 75 | { 76 | DefaultValue = defaultValue; 77 | 78 | return this; 79 | } 80 | 81 | IOptionBuilder IOptionBuilder.Name(string shortName) 82 | { 83 | LongName = $"{m_parserOptions.PrefixLongOption}{shortName}"; 84 | ShortName = $"{m_parserOptions.PrefixShortOption}{shortName}"; 85 | 86 | return this; 87 | } 88 | 89 | IOptionBuilder IOptionBuilder.Name(string shortName, string longName) 90 | { 91 | LongName = $"{m_parserOptions.PrefixLongOption}{longName}"; 92 | ShortName = $"{m_parserOptions.PrefixShortOption}{shortName}"; 93 | 94 | return this; 95 | } 96 | 97 | public IOptionBuilder Order(int order) 98 | { 99 | ((IOptionBuilder)this).Order(order); 100 | 101 | return this; 102 | } 103 | 104 | IOptionBuilder IOptionBuilder.Order(int order) 105 | { 106 | base.Order = order; 107 | 108 | return this; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CommandLineParser/Core/TypedInstanceCache.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace MatthiWare.CommandLine.Core 7 | { 8 | /// 9 | /// A strongly-typed instance cache 10 | /// 11 | /// 12 | public class TypedInstanceCache 13 | { 14 | private readonly IServiceProvider serviceProvider; 15 | private readonly Dictionary> instances = new Dictionary>(); 16 | 17 | /// 18 | /// Creates an instance of the strongly-typed instance cache 19 | /// 20 | /// 21 | public TypedInstanceCache(IServiceProvider serviceProvider) 22 | { 23 | this.serviceProvider = serviceProvider; 24 | } 25 | 26 | /// 27 | /// Adds a new instance to the cache 28 | /// 29 | /// 30 | public void Add(TValue value) 31 | { 32 | var key = typeof(TValue); 33 | 34 | if (!instances.ContainsKey(key)) 35 | { 36 | instances.Add(typeof(TValue), new InstanceMetadata(key, value)); 37 | } 38 | else 39 | { 40 | instances[key].SetInstance(value); 41 | } 42 | } 43 | 44 | /// 45 | /// Adds a type to the cache that will be resolved later 46 | /// 47 | /// 48 | public void Add(Type type) 49 | { 50 | if (!instances.ContainsKey(type)) 51 | { 52 | instances.Add(typeof(TValue), new InstanceMetadata(type)); 53 | } 54 | else 55 | { 56 | instances[type].Clear(); 57 | } 58 | } 59 | 60 | /// 61 | /// Gets the values from the cache. This will instantiate unresolved items. 62 | /// 63 | /// 64 | public IReadOnlyList Get() 65 | { 66 | var toResolve = instances.Values.Where(meta => !meta.Created).ToArray(); 67 | 68 | foreach (var meta in toResolve) 69 | { 70 | var instance = ActivatorUtilities.GetServiceOrCreateInstance(this.serviceProvider, meta.Type); 71 | 72 | meta.SetInstance(instance); 73 | } 74 | 75 | return instances.Values.Select(meta => meta.Instance).ToList(); 76 | } 77 | 78 | private class InstanceMetadata 79 | { 80 | public bool Created { get; private set; } 81 | public T Instance { get; private set; } 82 | 83 | public readonly Type Type; 84 | 85 | public InstanceMetadata(Type type, T instance) 86 | { 87 | Type = type; 88 | 89 | SetInstance(instance); 90 | } 91 | 92 | public InstanceMetadata(Type type) 93 | { 94 | Type = type; 95 | 96 | Clear(); 97 | } 98 | 99 | public void SetInstance(object instance) 100 | { 101 | Instance = (T)instance; 102 | Created = true; 103 | } 104 | 105 | public void Clear() 106 | { 107 | Instance = default; 108 | Created = false; 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Command/CommandInModelTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Core.Attributes; 3 | using Xunit; 4 | using Xunit.Abstractions; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace MatthiWare.CommandLine.Tests.Command 8 | { 9 | public class CommandInModelTests : TestBase 10 | { 11 | public CommandInModelTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 12 | { 13 | } 14 | 15 | #region FindCommandsInModel 16 | 17 | [Fact(Timeout = 1000)] 18 | public void FindCommandsInModel() 19 | { 20 | Services.AddLogging(); 21 | Services.AddCommandLineParser(); 22 | 23 | var parser = ResolveParser(); 24 | 25 | Assert.Equal(3, parser.Commands.Count); 26 | } 27 | 28 | public class ModelWithCommands 29 | { 30 | public NonGenericCommand NonGenericCommand { get; set; } 31 | public GenericCommandWithParentOptions GenericCommandWithParentOptions { get; set; } 32 | public GenericCommandWithOwnOptions GenericCommandWithOwnOptions { get; set; } 33 | } 34 | 35 | public class ModelWithOptions 36 | { 37 | [Name("i", "input")] 38 | public string MyOption { get; set; } 39 | } 40 | 41 | public class NonGenericCommand : Abstractions.Command.Command 42 | { 43 | public override void OnConfigure(ICommandConfigurationBuilder builder) 44 | { 45 | builder.Name(nameof(NonGenericCommand)); 46 | } 47 | } 48 | 49 | public class GenericCommandWithParentOptions : Command 50 | { 51 | public override void OnConfigure(ICommandConfigurationBuilder builder) 52 | { 53 | builder.Name(nameof(GenericCommandWithParentOptions)); 54 | } 55 | } 56 | 57 | public class GenericCommandWithOwnOptions : Command 58 | { 59 | public override void OnConfigure(ICommandConfigurationBuilder builder) 60 | { 61 | builder.Name(nameof(GenericCommandWithOwnOptions)); 62 | } 63 | } 64 | 65 | #endregion 66 | 67 | #region SubCommandFindCommandsInModel 68 | 69 | [Fact] 70 | public void FindCommandsInCommandModel() 71 | { 72 | Services.AddCommandLineParser(); 73 | 74 | var parser = ResolveParser(); 75 | 76 | parser.RegisterCommand(); 77 | 78 | Assert.Equal(3, ((ICommandLineCommandContainer)parser.Commands[0]).Commands.Count); 79 | } 80 | 81 | public class SubCommandModelWithCommands 82 | { 83 | public NonGenericCommand NonGenericCommand { get; set; } 84 | public SubCommandWithModelOptions SubCommandWithModelOptions { get; set; } 85 | public SimpleGenericCommand SimpleGenericCommand { get; set; } 86 | } 87 | 88 | public class GenericSubCommandWithOwnOptions : Command 89 | { 90 | public override void OnConfigure(ICommandConfigurationBuilder builder) 91 | { 92 | builder.Name(nameof(GenericSubCommandWithOwnOptions)); 93 | } 94 | } 95 | 96 | public class SubCommandWithModelOptions : Command 97 | { 98 | public override void OnConfigure(ICommandConfigurationBuilder builder) 99 | { 100 | builder.Name(nameof(SubCommandWithModelOptions)); 101 | } 102 | } 103 | 104 | public class SimpleGenericCommand : Command 105 | { 106 | public override void OnConfigure(ICommandConfigurationBuilder builder) 107 | { 108 | builder.Name(nameof(SimpleGenericCommand)); 109 | } 110 | } 111 | 112 | #endregion 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /CommandLineParser/Core/CommandLineOptionBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq.Expressions; 4 | using System.Reflection; 5 | using MatthiWare.CommandLine.Abstractions; 6 | using MatthiWare.CommandLine.Abstractions.Models; 7 | using MatthiWare.CommandLine.Abstractions.Parsing; 8 | using Microsoft.Extensions.Logging; 9 | 10 | namespace MatthiWare.CommandLine.Core 11 | { 12 | [DebuggerDisplay("Cmd Option {ShortName ?? LongName ?? m_selector.ToString()}, Req: {IsRequired}, HasDefault: {HasDefault}")] 13 | internal abstract class CommandLineOptionBase : IParser, ICommandLineOption 14 | { 15 | private readonly object m_source; 16 | private readonly LambdaExpression m_selector; 17 | private readonly ILogger logger; 18 | private object m_defaultValue = null; 19 | protected readonly CommandLineParserOptions m_parserOptions; 20 | private Delegate m_translator = null; 21 | 22 | public CommandLineOptionBase(CommandLineParserOptions parserOptions, object source, LambdaExpression selector, ICommandLineArgumentResolver resolver, ILogger logger) 23 | { 24 | m_parserOptions = parserOptions ?? throw new ArgumentNullException(nameof(source)); 25 | m_source = source ?? throw new ArgumentNullException(nameof(source)); 26 | m_selector = selector ?? throw new ArgumentNullException(nameof(selector)); 27 | Resolver = resolver ?? throw new ArgumentNullException(nameof(resolver)); 28 | this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); 29 | } 30 | 31 | public object DefaultValue 32 | { 33 | get => m_defaultValue; 34 | set 35 | { 36 | HasDefault = true; 37 | m_defaultValue = value; 38 | } 39 | } 40 | 41 | public ICommandLineArgumentResolver Resolver { get; } 42 | 43 | public string ShortName { get; protected set; } 44 | public string LongName { get; protected set; } 45 | public string Description { get; protected set; } 46 | public bool IsRequired { get; protected set; } 47 | public bool HasDefault { get; protected set; } 48 | public bool HasShortName => !string.IsNullOrWhiteSpace(ShortName) && !ShortName.Equals(m_parserOptions.PrefixShortOption); 49 | public bool HasLongName => !string.IsNullOrWhiteSpace(LongName) && !LongName.Equals(m_parserOptions.PrefixLongOption); 50 | 51 | public bool AutoExecute { get; protected set; } 52 | 53 | public int? Order { get; protected set; } 54 | 55 | public bool AllowMultipleValues { get; protected set; } 56 | 57 | public void UseDefault() => AssignValue(DefaultValue); 58 | 59 | public bool CanParse(ArgumentModel model) => Resolver.CanResolve(model); 60 | 61 | public void Parse(ArgumentModel model) => AssignValue(Resolver.Resolve(model)); 62 | 63 | private void AssignValue(object value) 64 | { 65 | var property = (PropertyInfo)((MemberExpression)m_selector.Body).Member; 66 | var actualValue = m_translator?.DynamicInvoke(value) ?? value; 67 | 68 | var key = $"{property.DeclaringType.FullName}.{property.Name}"; 69 | logger.LogDebug("Option '{OptionName}' ({key}) value assigned '{value}'", ShortName, key, actualValue); 70 | 71 | property.SetValue(m_source, actualValue, null); 72 | } 73 | 74 | protected void SetTranslator(Delegate @delegate) => m_translator = @delegate; 75 | 76 | public bool CheckOptionNotFound() => IsRequired && !HasDefault; 77 | 78 | public bool ShouldUseDefault(bool found, ArgumentModel model) 79 | => (found && ShouldUseDefaultWhenParsingFails(model)) || (!found && ShouldUseDefaultWhenNoValueProvidedButDefaultValueIsSpecified(model)); 80 | 81 | private bool ShouldUseDefaultWhenParsingFails(ArgumentModel model) 82 | => !CanParse(model) && HasDefault; 83 | 84 | private bool ShouldUseDefaultWhenNoValueProvidedButDefaultValueIsSpecified(ArgumentModel model) 85 | => (model is null || !model.HasValue) && HasDefault; 86 | 87 | public override string ToString() => m_selector.ToString(); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /Extensions/FluentValidationsExtensions/Core/FluentValidationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using FluentValidation; 2 | using MatthiWare.CommandLine.Abstractions.Validations; 3 | using MatthiWare.CommandLine.Core.Utils; 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace MatthiWare.CommandLine.Extensions.FluentValidations.Core 8 | { 9 | /// 10 | /// Configuration for fluent validations 11 | /// 12 | public sealed class FluentValidationConfiguration : ValidationConfigurationBase 13 | { 14 | private readonly IServiceProvider serviceProvider; 15 | private readonly Dictionary validators = new Dictionary(); 16 | 17 | /// 18 | /// Creates a new fluent validation configuration 19 | /// 20 | /// 21 | /// 22 | public FluentValidationConfiguration(IValidatorsContainer container, IServiceProvider serviceProvider) 23 | : base(container) 24 | { 25 | this.serviceProvider = serviceProvider; 26 | } 27 | 28 | /// 29 | /// Adds a validator 30 | /// 31 | /// type to validate 32 | /// Validator type 33 | /// Self 34 | public FluentValidationConfiguration AddValidator(Type key, Type validator) 35 | { 36 | if (!validator.IsAssignableToGenericType(typeof(FluentValidation.IValidator<>))) 37 | { 38 | throw new InvalidCastException($"{validator} is not assignable to {typeof(FluentValidation.IValidator<>)}"); 39 | } 40 | 41 | GetValidatorCollection(key).AddValidator(validator); 42 | 43 | return this; 44 | } 45 | 46 | /// 47 | /// Adds a validator 48 | /// 49 | /// Type to validate 50 | /// Validator type 51 | /// Self 52 | public FluentValidationConfiguration AddValidator() 53 | where V : AbstractValidator 54 | { 55 | GetValidatorCollection(typeof(K)).AddValidator(); 56 | 57 | return this; 58 | } 59 | 60 | /// 61 | /// Adds an instantiated validator 62 | /// 63 | /// Type to validate 64 | /// Validator type 65 | /// Instance 66 | /// Self 67 | public FluentValidationConfiguration AddValidatorInstance(V instance) 68 | where V : AbstractValidator 69 | { 70 | if (instance is null) 71 | { 72 | throw new ArgumentNullException(nameof(instance)); 73 | } 74 | 75 | GetValidatorCollection(typeof(K)).AddValidator(instance); 76 | 77 | return this; 78 | } 79 | 80 | /// 81 | /// Add an instantiated validator 82 | /// 83 | /// Type to validate 84 | /// Validator instance 85 | /// Self 86 | public FluentValidationConfiguration AddValidatorInstance(Type key, FluentValidation.IValidator instance) 87 | { 88 | if (instance is null) 89 | { 90 | throw new ArgumentNullException(nameof(instance)); 91 | } 92 | 93 | GetValidatorCollection(key).AddValidator(instance); 94 | 95 | return this; 96 | } 97 | 98 | private FluentTypeValidatorCollection GetValidatorCollection(Type key) 99 | { 100 | if (!validators.TryGetValue(key, out FluentTypeValidatorCollection validator)) 101 | { 102 | validator = new FluentTypeValidatorCollection(serviceProvider); 103 | 104 | validators.Add(key, validator); 105 | Validators.AddValidator(key, validator); 106 | } 107 | 108 | return validator; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /CommandLineParser.Tests/Parsing/Validation/ValidationAbstractionTests.cs: -------------------------------------------------------------------------------- 1 | using MatthiWare.CommandLine.Abstractions.Command; 2 | using MatthiWare.CommandLine.Abstractions.Validations; 3 | using MatthiWare.CommandLine.Core.Attributes; 4 | using Moq; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | using Xunit; 8 | using Xunit.Abstractions; 9 | 10 | namespace MatthiWare.CommandLine.Tests.Parsing.Validation 11 | { 12 | public class ValidationAbstractionTests : TestBase 13 | { 14 | public ValidationAbstractionTests(ITestOutputHelper testOutputHelper) : base(testOutputHelper) 15 | { 16 | } 17 | 18 | [Fact] 19 | public void ParsingCallsValidation() 20 | { 21 | Services.AddCommandLineParser(); 22 | var parser = ResolveParser(); 23 | 24 | var validValidationResultMock = new Mock(); 25 | validValidationResultMock.SetupGet(v => v.IsValid).Returns(true); 26 | 27 | var optionWithCommandMockValidator = new Mock>(); 28 | optionWithCommandMockValidator 29 | .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) 30 | .ReturnsAsync(validValidationResultMock.Object) 31 | .Verifiable(); 32 | 33 | var optionMockValidator = new Mock>(); 34 | optionMockValidator 35 | .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) 36 | .ReturnsAsync(validValidationResultMock.Object) 37 | .Verifiable(); 38 | 39 | parser.Validators.AddValidator(optionWithCommandMockValidator.Object); 40 | parser.Validators.AddValidator(optionMockValidator.Object); 41 | 42 | var result = parser.Parse(new[] { "-x", "true", "cmd", "-y", "true" }); 43 | 44 | result.AssertNoErrors(); 45 | 46 | optionMockValidator.Verify(); 47 | optionWithCommandMockValidator.Verify(); 48 | } 49 | 50 | [Fact] 51 | public async Task ParsingCallsValidationAsync() 52 | { 53 | Services.AddCommandLineParser(); 54 | var parser = ResolveParser(); 55 | 56 | var validValidationResultMock = new Mock(); 57 | validValidationResultMock.SetupGet(v => v.IsValid).Returns(true); 58 | 59 | var optionWithCommandMockValidator = new Mock>(); 60 | optionWithCommandMockValidator 61 | .Setup(v => v.ValidateAsync(It.IsAny(), It.IsAny())) 62 | .ReturnsAsync(validValidationResultMock.Object) 63 | .Verifiable(); 64 | 65 | var optionMockValidator = new Mock>(); 66 | optionMockValidator 67 | .Setup(v => v.ValidateAsync(It.IsAny