├── .trunk ├── .gitignore ├── config │ ├── .hadolint.yaml │ └── .markdownlint.yaml └── trunk.yaml ├── .github └── dependabot.yml ├── src ├── Samples │ ├── MultilevelCommandApp │ │ ├── CommandBase.cs │ │ ├── StatusCommand.cs │ │ ├── MultilevelCommandApp.csproj │ │ ├── GetConfigurationCommand.cs │ │ ├── SetConfigurationCommand.cs │ │ ├── ProgramArguments.cs │ │ ├── ConfigurationCommand.cs │ │ └── Program.cs │ ├── HelpApp │ │ ├── Actions │ │ │ ├── Amazing.cs │ │ │ └── Mediocre.cs │ │ ├── HelpApp.csproj │ │ └── Program.cs │ └── MergedCommandApp │ │ ├── ExtraCommandsType.cs │ │ ├── CommandType.cs │ │ ├── ExtraCommand.cs │ │ ├── SimpleCommand.cs │ │ ├── ProgramArguments.cs │ │ ├── EnumProvider.cs │ │ ├── Program.cs │ │ └── MergedCommandApp.csproj ├── NClap │ ├── Utilities │ │ ├── ColoredStringBuilder.cs │ │ ├── None.cs │ │ ├── Some.cs │ │ ├── IDeepCloneable`1.cs │ │ ├── CircularEnumerator.cs │ │ ├── MaybeUtilities.cs │ │ ├── TokenizerOptions.cs │ │ ├── Windows │ │ │ ├── NativeMethods.cs │ │ │ └── InputUtilities.cs │ │ ├── GenericCollectionFactory.cs │ │ ├── IMutableMemberInfo.cs │ │ └── FluentBuilder`1.cs │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Help │ │ ├── ArgumentSortOrder.cs │ │ ├── ArgumentGroupingMode.cs │ │ ├── ArgumentHelpLayout.cs │ │ ├── ArgumentShortNameHelpMode.cs │ │ ├── ArgumentDefaultValueHelpMode.cs │ │ ├── OneColumnArgumentHelpLayout.cs │ │ ├── ArgumentEnumValueHelpFlags.cs │ │ ├── ArgumentSetHelpSectionType.cs │ │ ├── ArgumentEnumValueHelpOptions.cs │ │ ├── ArgumentSyntaxHelpOptions.cs │ │ ├── ArgumentSyntaxFlags.cs │ │ ├── TwoColumnArgumentHelpLayout.cs │ │ └── ArgumentMetadataHelpOptions.cs │ ├── Metadata │ │ ├── ArgumentGroupAttribute.cs │ │ ├── IArgumentSetWithHelp.cs │ │ ├── ExitCommand.cs │ │ ├── ICommand.cs │ │ ├── UnimplementedCommand.cs │ │ ├── HelpArgumentsBase.cs │ │ ├── ArgumentTypeAttribute.cs │ │ ├── Command.cs │ │ ├── ArgumentValueFlags.cs │ │ ├── HelpCommandAttribute.cs │ │ ├── ArgumentValidationContext.cs │ │ ├── CommandResult.cs │ │ ├── IntegerComparisonValidationAttribute.cs │ │ ├── ArgumentSetStyle.cs │ │ ├── IArgumentProvider.cs │ │ ├── ArgumentNameGenerationFlags.cs │ │ ├── ICommandGroup.cs │ │ ├── SynchronousCommand.cs │ │ ├── ExtensibleEnumAttribute.cs │ │ ├── HelpCommand.cs │ │ ├── CommandGroupOptions.cs │ │ ├── NumberOptions.cs │ │ ├── FileSystemPathValidationAttribute.cs │ │ ├── IntegerValidationAttribute.cs │ │ ├── MustNotBeEmptyAttribute.cs │ │ ├── HelpCommandArgumentCompleter.cs │ │ ├── NamedArgumentAttribute.cs │ │ ├── ArgumentValidationAttribute.cs │ │ ├── CommandAttribute.cs │ │ ├── PositionalArgumentAttribute.cs │ │ ├── MustBeLessThanAttribute.cs │ │ ├── MustBeGreaterThanAttribute.cs │ │ ├── MustBeLessThanOrEqualToAttribute.cs │ │ ├── MustNotExistAttribute.cs │ │ ├── MustBeGreaterThanOrEqualToAttribute.cs │ │ ├── StringValidationAttribute.cs │ │ ├── ArgumentValueAttribute.cs │ │ └── MustNotBeAttribute.cs │ ├── Parser │ │ ├── ArgumentNameType.cs │ │ ├── ArgumentSetParseResultType.cs │ │ ├── CommandGroupDefinition.cs │ │ └── CommandDefinition.cs │ ├── Types │ │ ├── IEnumArgumentTypeProvider.cs │ │ ├── IObjectFormatter.cs │ │ ├── IStringParser.cs │ │ ├── IStringCompleter.cs │ │ ├── IEnumArgumentType.cs │ │ ├── ArgumentCompletionContext.cs │ │ ├── IArgumentType.cs │ │ ├── ICollectionArgumentType.cs │ │ ├── StringArgumentType.cs │ │ ├── IArgumentValue.cs │ │ └── MergedEnumArgumentType.cs │ ├── ConsoleInput │ │ ├── ConsoleUtilities.cs │ │ ├── IReadOnlyConsoleKeyBindingSet.cs │ │ ├── ConsoleInputOperationResult.cs │ │ ├── IConsoleInput.cs │ │ ├── ITokenCompleter.cs │ │ ├── IConsoleReader.cs │ │ └── IConsoleHistory.cs │ ├── stylecop.json │ ├── Expressions │ │ ├── Operator.cs │ │ ├── ExpressionEnvironment.cs │ │ ├── Expression.cs │ │ ├── ParenthesisExpression.cs │ │ ├── StringLiteral.cs │ │ ├── ConcatenationExpression.cs │ │ └── OperatorExpression.cs │ ├── Exceptions │ │ └── InternalInvariantBrokenException.cs │ ├── Repl │ │ ├── LoopInputOutputParameters.cs │ │ ├── ILoopClient.cs │ │ └── LoopOptions.cs │ └── IFileSystemReader.cs ├── Tests │ ├── UnitTests │ │ ├── Exceptions │ │ │ ├── InternalInvariantBrokenExceptionTests.cs │ │ │ ├── InvalidCommandExceptionTests.cs │ │ │ └── InvalidArgumentSetExceptionTests.cs │ │ ├── Help │ │ │ ├── ArgumentSetHelpOptionsTests.cs │ │ │ ├── ArgumentSetUsageInfoTests.cs │ │ │ └── ArgumentSetHelpOptionsExtensionsTests.cs │ │ ├── Types │ │ │ ├── ArgumentParseContextTests.cs │ │ │ ├── ArgumentTypeExtensionTests.cs │ │ │ ├── KeyValuePairArgumentTypeTests.cs │ │ │ ├── TupleArgumentTypeTests.cs │ │ │ └── EnumArgumentValueTests.cs │ │ ├── GlobalSuppressions.cs │ │ ├── Metadata │ │ │ ├── CommandTests.cs │ │ │ ├── UnimplementedCommandTests.cs │ │ │ ├── ArgumentTypeAttributeTests.cs │ │ │ ├── ArgumentValueAttributeTests.cs │ │ │ ├── StringValidationAttributeTests.cs │ │ │ └── ArgumentAttributeTests.cs │ │ ├── ConsoleInput │ │ │ ├── TestTokenCompleter.cs │ │ │ └── SimulatedConsoleInput.cs │ │ ├── StringsTests.cs │ │ ├── Utilities │ │ │ ├── ColoredStringExtensionMethodsTests.cs │ │ │ ├── DeepCloneTests.cs │ │ │ ├── EnumerableUtilitiesTests.cs │ │ │ ├── MutableFieldInfoTests.cs │ │ │ ├── FluentBuilderTests.cs │ │ │ ├── MutablePropertyInfoTests.cs │ │ │ ├── MaybeTests.cs │ │ │ ├── TypeUtilitiesTests.cs │ │ │ └── StringWrapperTests.cs │ │ ├── CommandLineParserOptionsTests.cs │ │ ├── NClap.Tests.csproj │ │ ├── Parser │ │ │ └── ArgumentSetParserTests.cs │ │ ├── Expressions │ │ │ ├── ExpressionTests.cs │ │ │ ├── TestEnvironment.cs │ │ │ ├── StringLiteralTests.cs │ │ │ ├── ParenthesisExpressionTests.cs │ │ │ └── StringExpanderTests.cs │ │ └── AssemblyTests.cs │ └── TestApp │ │ ├── CliHelp.cs │ │ ├── SubCommandType.cs │ │ ├── LogoCommand.cs │ │ ├── NClap.TestApp.csproj │ │ ├── CompleteCommand.cs │ │ ├── ReadLineCommand.cs │ │ ├── Program.cs │ │ ├── ProgramArguments.cs │ │ ├── MainCommandType.cs │ │ └── ReplCommand.cs └── Tools │ └── Inspector │ ├── Program.cs │ ├── NClap.Inspector.csproj │ ├── CompleteTokensCommand.cs │ └── CompleteLineCommand.cs ├── .devcontainer ├── hashes.txt ├── devcontainer.json └── Dockerfile ├── GitVersion.yml ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── CODE_OF_CONDUCT.md ├── .editorconfig ├── LICENSE.txt └── README.md /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | *logs 3 | *actions 4 | *notifications 5 | plugins 6 | user_trunk.yaml 7 | user.yaml 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: nuget 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | open-pull-requests-limit: 10 8 | -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/CommandBase.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace MultilevelCommandApp 4 | { 5 | internal abstract class CommandBase : SynchronousCommand 6 | { 7 | } 8 | } -------------------------------------------------------------------------------- /.devcontainer/hashes.txt: -------------------------------------------------------------------------------- 1 | 095fa945b775be944f660875cfe80218898c210615b6584faea31a4060ba3463 ./bat-musl_0.22.1_amd64.deb 2 | a65a87bd545e969979ae9388f6333167f041a1f09fa9d60b32fd3072348ff6ce ./exa-linux-x86_64-v0.10.1.zip 3 | -------------------------------------------------------------------------------- /.trunk/config/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | ignored: 2 | # Following source doesn't work in most setups 3 | - SC1090 4 | - SC1091 5 | 6 | # DL3004: Don't use sudo 7 | - DL3004 8 | # DL3008: Pin apt package versions 9 | - DL3008 10 | -------------------------------------------------------------------------------- /src/Samples/HelpApp/Actions/Amazing.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace HelpApp.Actions 4 | { 5 | internal class Amazing : SynchronousCommand 6 | { 7 | public override CommandResult Execute() => CommandResult.Success; 8 | } 9 | } -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | next-version: 3.1.0 2 | branches: 3 | main: 4 | regex: ^main$ 5 | mode: ContinuousDelivery 6 | tag: "" 7 | increment: Patch 8 | prevent-increment-of-merged-branch-version: true 9 | track-merge-target: false 10 | -------------------------------------------------------------------------------- /src/NClap/Utilities/ColoredStringBuilder.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Utilities 2 | { 3 | /// 4 | /// String builder for constructing colored strings. 5 | /// 6 | public class ColoredStringBuilder 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[csharp]": { 4 | "editor.defaultFormatter": "ms-dotnettools.csharp" 5 | }, 6 | "dotnet-test-explorer.testProjectPath": "**/*.sln", 7 | "dotnet-test-explorer.enableTelemetry": false 8 | } 9 | -------------------------------------------------------------------------------- /src/NClap/Utilities/None.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Utilities 2 | { 3 | /// 4 | /// Utility for constructing objects with no contained 5 | /// values. 6 | /// 7 | internal sealed class None 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/ExtraCommandsType.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace MergedCommandApp 4 | { 5 | internal enum ExtraCommandsType 6 | { 7 | [Command(typeof(ExtraCommand), Description = "Do something a bit extra")] 8 | Extra 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.trunk/config/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Autoformatter friendly markdownlint config (all formatting rules disabled) 2 | default: true 3 | blank_lines: false 4 | bullet: false 5 | html: false 6 | indentation: false 7 | line_length: false 8 | spaces: false 9 | url: false 10 | whitespace: false 11 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/CommandType.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace MergedCommandApp 4 | { 5 | [ExtensibleEnum(typeof(EnumProvider))] 6 | internal enum CommandType 7 | { 8 | [Command(typeof(SimpleCommand), Description = "Keep it simple")] 9 | Foo 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/NClap/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: InternalsVisibleTo("NClap.Tests")] 6 | 7 | [assembly: CLSCompliant(true)] 8 | [assembly: ComVisible(false)] 9 | [assembly: Guid("ce1a820d-79f3-410d-b869-884dea01fbe6")] 10 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentSortOrder.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Sort order type for arguments. 5 | /// 6 | public enum ArgumentSortOrder 7 | { 8 | /// 9 | /// Sort lexicographically. 10 | /// 11 | Lexicographic 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Exceptions/InternalInvariantBrokenExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NClap.Exceptions; 3 | 4 | namespace NClap.Tests.Exceptions 5 | { 6 | [TestClass] 7 | public class InternalInvariantBrokenExceptionTests : ExceptionTests 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Exceptions/InvalidCommandExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Exceptions; 4 | 5 | namespace NClap.Tests.Exceptions 6 | { 7 | [TestClass] 8 | public class InvalidCommandExceptionTests : ExceptionTests 9 | { 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact 4 | [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentGroupingMode.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Grouping mode for arguments. 5 | /// 6 | public enum ArgumentGroupingMode 7 | { 8 | /// 9 | /// Group required arguments vs. optional arguments. 10 | /// 11 | RequiredVersusOptional 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/ExtraCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Metadata; 3 | 4 | namespace MergedCommandApp 5 | { 6 | internal class ExtraCommand : SynchronousCommand 7 | { 8 | public override CommandResult Execute() 9 | { 10 | Console.WriteLine("Extra!"); 11 | return CommandResult.Success; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/SimpleCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Metadata; 3 | 4 | namespace MergedCommandApp 5 | { 6 | internal class SimpleCommand : SynchronousCommand 7 | { 8 | public override CommandResult Execute() 9 | { 10 | Console.WriteLine("Simple."); 11 | return CommandResult.Success; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/ProgramArguments.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace MergedCommandApp 4 | { 5 | internal class ProgramArguments : IArgumentSetWithHelp 6 | { 7 | [PositionalArgument(ArgumentFlags.Required)] 8 | public CommandGroup Command { get; set; } 9 | 10 | [NamedArgument] 11 | public bool Help { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/EnumProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using NClap.Types; 3 | 4 | namespace MergedCommandApp 5 | { 6 | internal class EnumProvider : IEnumArgumentTypeProvider 7 | { 8 | public IEnumerable GetTypes() 9 | { 10 | return new[] { (IEnumArgumentType)ArgumentType.GetType(typeof(ExtraCommandsType)) }; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Tests/TestApp/CliHelp.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace NClap.TestApp 4 | { 5 | class CliHelp : SynchronousCommand 6 | { 7 | public override CommandResult Execute() 8 | { 9 | var info = CommandLineParser.GetUsageInfo(typeof(ProgramArguments)); 10 | CommandLineParser.DefaultReporter(info); 11 | return CommandResult.Success; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentGroupAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Unused. 7 | /// 8 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 9 | [Obsolete("This attribute is not supported and will be removed from a future release.", true)] 10 | public sealed class ArgumentGroupAttribute : Attribute 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/NClap/Parser/ArgumentNameType.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Parser 2 | { 3 | /// 4 | /// Type of a name for a named argument. 5 | /// 6 | public enum ArgumentNameType 7 | { 8 | /// 9 | /// A short name. 10 | /// 11 | ShortName, 12 | 13 | /// 14 | /// A long name. 15 | /// 16 | LongName 17 | } 18 | } -------------------------------------------------------------------------------- /src/Tests/TestApp/SubCommandType.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace NClap.TestApp 4 | { 5 | [ArgumentType(DisplayName = "Sub-command")] 6 | enum SubCommandType 7 | { 8 | [Command(typeof(UnimplementedCommand))] 9 | Foo, 10 | 11 | [Command(typeof(UnimplementedCommand))] 12 | Bar, 13 | 14 | [Command(typeof(CommandGroup))] 15 | Main 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "NClap dev container", 3 | "dockerFile": "Dockerfile", 4 | "extensions": [ 5 | "formulahendry.dotnet-test-explorer", 6 | "ms-dotnettools.csharp", 7 | "eamodio.gitlens" 8 | ], 9 | "features": { 10 | "ghcr.io/devcontainers/features/github-cli:1": { 11 | "version": "latest" 12 | } 13 | }, 14 | "remoteUser": "vscode" 15 | } 16 | -------------------------------------------------------------------------------- /src/NClap/Metadata/IArgumentSetWithHelp.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Interface to be implemented on argument set types that expose help options. 5 | /// 6 | public interface IArgumentSetWithHelp 7 | { 8 | /// 9 | /// True if the user wants to receive usage help information; false 10 | /// otherwise. 11 | /// 12 | bool Help { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/NClap/Types/IEnumArgumentTypeProvider.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Enum argument type provider. 7 | /// 8 | public interface IEnumArgumentTypeProvider 9 | { 10 | /// 11 | /// Retrieves types being provided. 12 | /// 13 | /// Enumeration of types. 14 | IEnumerable GetTypes(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/ConsoleUtilities.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.ConsoleInput 2 | { 3 | /// 4 | /// Assorted console utilities exported for use outside this assembly. 5 | /// 6 | public static class ConsoleUtilities 7 | { 8 | /// 9 | /// Reads a line of input from the console. 10 | /// 11 | /// The read string. 12 | public static string ReadLine() => new ConsoleReader().ReadLine(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ExitCommand.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Simple command implementation that only exits. 5 | /// 6 | public class ExitCommand : SynchronousCommand 7 | { 8 | /// 9 | /// Does nothing, but indicates to the caller that termination is desired. 10 | /// 11 | /// CommandResult.Terminate. 12 | public override CommandResult Execute() => CommandResult.Terminate; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap; 3 | 4 | namespace MergedCommandApp 5 | { 6 | class Program 7 | { 8 | static int Main(string[] args) 9 | { 10 | if (!CommandLineParser.TryParse(args, out ProgramArguments progArgs)) 11 | { 12 | return 1; 13 | } 14 | 15 | var result = progArgs.Command.Execute(); 16 | return result == NClap.Metadata.CommandResult.Success ? 0 : 1; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Samples/HelpApp/HelpApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/NClap/Types/IObjectFormatter.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Types 2 | { 3 | /// 4 | /// Interface implemented by objects that can convert objects into strings. 5 | /// 6 | public interface IObjectFormatter 7 | { 8 | /// 9 | /// Converts a value into a readable string form. 10 | /// 11 | /// The value to format into a string. 12 | /// The formatted string. 13 | string Format(object value); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Tools/Inspector/Program.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace NClap.Inspector 4 | { 5 | class Program 6 | { 7 | public static int Main(string[] args) 8 | { 9 | if (!CommandLineParser.TryParse(args, out ProgramArguments parsedArgs)) 10 | { 11 | return 1; 12 | } 13 | 14 | if (parsedArgs.Execute() != CommandResult.Success) 15 | { 16 | return 1; 17 | } 18 | 19 | return 0; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Samples/MergedCommandApp/MergedCommandApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Tests/TestApp/LogoCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Metadata; 3 | 4 | namespace NClap.TestApp 5 | { 6 | class LogoCommand : SynchronousCommand 7 | { 8 | public override CommandResult Execute() 9 | { 10 | Console.WriteLine("Logo:"); 11 | Console.WriteLine("---------------------------"); 12 | Console.Write(CommandLineParser.GetLogo()); 13 | Console.WriteLine("---------------------------"); 14 | 15 | return CommandResult.Success; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ICommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Represents a command (a.k.a. verb). 8 | /// 9 | public interface ICommand 10 | { 11 | /// 12 | /// Executes the command. 13 | /// 14 | /// Cancellation token. 15 | /// Result of execution. 16 | Task ExecuteAsync(CancellationToken cancel); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/NClap/Metadata/UnimplementedCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Simple stub command implementation that is not implemented. 7 | /// 8 | public class UnimplementedCommand : SynchronousCommand 9 | { 10 | /// 11 | /// Throws an exception. 12 | /// 13 | /// Does not return. 14 | public override CommandResult Execute() => throw new NotImplementedException("Executed unimplemented command."); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/NClap/Metadata/HelpArgumentsBase.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Simple implementation of . 5 | /// 6 | public class HelpArgumentsBase : IArgumentSetWithHelp 7 | { 8 | /// 9 | /// True if the user wants to receive usage help information; false 10 | /// otherwise. 11 | /// 12 | [NamedArgument(ArgumentFlags.AtMostOnce, Description = "Display help information")] 13 | public bool Help { get; set; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/NClap/stylecop.json: -------------------------------------------------------------------------------- 1 | { 2 | "settings": { 3 | "documentationRules": { 4 | "xmlHeader": false, 5 | "documentInternalElements": true, 6 | "fileNamingConvention": "metadata" 7 | }, 8 | "maintainabilityRules": { 9 | "topLevelTypes": ["class", "interface"] 10 | }, 11 | "orderingRules": { 12 | "systemUsingDirectivesFirst": true, 13 | "usingDirectivesPlacement": "outsideNamespace", 14 | "blankLinesBetweenUsingGroups": "omit" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/StatusCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NClap.Metadata; 3 | 4 | namespace MultilevelCommandApp 5 | { 6 | internal class StatusCommand : CommandBase 7 | { 8 | private readonly ILogger logger; 9 | 10 | public StatusCommand(ILogger logger) 11 | { 12 | this.logger = logger; 13 | } 14 | 15 | public override CommandResult Execute() 16 | { 17 | logger.LogWarning($"Status."); 18 | return CommandResult.Success; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Tests/UnitTests/Help/ArgumentSetHelpOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using NClap.Help; 3 | using NClap.Tests.Utilities; 4 | 5 | namespace NClap.Tests.Help 6 | { 7 | [TestClass] 8 | public class ArgumentSetHelpOptionsTests 9 | { 10 | [TestMethod] 11 | public void TestThatDeepCloningDefaultOptionsWorksAsExpected() 12 | { 13 | var options = new ArgumentSetHelpOptions(); 14 | DeepCloneTests.CloneShouldYieldADistinctButEquivalentObject(options); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/IReadOnlyConsoleKeyBindingSet.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace NClap.ConsoleInput 6 | { 7 | /// 8 | /// Read-only abstract interface for querying a console key binding set. 9 | /// 10 | [SuppressMessage("Microsoft.Naming", "CA1710:IdentifiersShouldHaveCorrectSuffix", Justification = "[Legacy]")] 11 | public interface IReadOnlyConsoleKeyBindingSet : IReadOnlyDictionary 12 | { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/NClap/Expressions/Operator.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Expressions 2 | { 3 | /// 4 | /// Operator type. 5 | /// 6 | internal enum Operator 7 | { 8 | /// 9 | /// Sentinel invalid value. 10 | /// 11 | Unspecified = 0, 12 | 13 | /// 14 | /// Converts to lower-case. 15 | /// 16 | ConvertToLowerCase = 1, 17 | 18 | /// 19 | /// Converts to lower-case. 20 | /// 21 | ConvertToUpperCase = 2, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/NClap/Utilities/Some.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Utilities 2 | { 3 | /// 4 | /// Convenience class for constructing values. 5 | /// 6 | internal static class Some 7 | { 8 | /// 9 | /// Constructs an object with a value present. 10 | /// 11 | /// Type of the value. 12 | /// The value. 13 | /// The constructed object. 14 | public static Maybe Of(T value) => new Maybe(value); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Tests/TestApp/NClap.TestApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | win10-x64;linux-x64 7 | false 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Attribute for annotating types that can be used as arguments. 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Enum)] 9 | public sealed class ArgumentTypeAttribute : Attribute 10 | { 11 | /// 12 | /// Optionally indicates how this type should be displayed in 13 | /// help/usage information. 14 | /// 15 | public string DisplayName { get; set; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/NClap/Utilities/IDeepCloneable`1.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Utilities 2 | { 3 | /// 4 | /// An interface representing an item that is deeply cloneable. 5 | /// 6 | /// The type of the clone. 7 | public interface IDeepCloneable 8 | { 9 | /// 10 | /// Creates a deep clone of the item, where no data references are shared. 11 | /// Changes made to the clone do not affect the original, and vice versa. 12 | /// 13 | /// The clone. 14 | T DeepClone(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Samples/HelpApp/Actions/Mediocre.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace HelpApp.Actions 4 | { 5 | internal enum MediocrityLevel 6 | { 7 | Mediocre, 8 | TrulyMediocre 9 | } 10 | 11 | internal class Mediocre : SynchronousCommand 12 | { 13 | [NamedArgument( 14 | ArgumentFlags.Required, 15 | LongName = "med-level", 16 | Description = "Level of mediocrity desired to be attained. Or barely passed.")] 17 | MediocrityLevel Level { get; set; } 18 | 19 | public override CommandResult Execute() => CommandResult.Success; 20 | } 21 | } -------------------------------------------------------------------------------- /src/Tests/UnitTests/Types/ArgumentParseContextTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Types; 6 | 7 | namespace NClap.Tests.Types 8 | { 9 | [TestClass] 10 | public class ArgumentParseContextTests 11 | { 12 | [TestMethod] 13 | public void DefaultedReader() 14 | { 15 | var context = new ArgumentParseContext(); 16 | 17 | Action setNull = () => context.FileSystemReader = null; 18 | setNull.Should().NotThrow(); 19 | 20 | context.FileSystemReader.Should().NotBeNull(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentHelpLayout.cs: -------------------------------------------------------------------------------- 1 | using NClap.Utilities; 2 | 3 | namespace NClap.Help 4 | { 5 | /// 6 | /// Abstract base class for an argument help layout. 7 | /// 8 | public abstract class ArgumentHelpLayout : IDeepCloneable 9 | { 10 | /// 11 | /// Default constructor. 12 | /// 13 | protected ArgumentHelpLayout() 14 | { 15 | } 16 | 17 | /// 18 | /// Create a separate clone of this object. 19 | /// 20 | /// Clone. 21 | public abstract ArgumentHelpLayout DeepClone(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // This file is used by Code Analysis to maintain SuppressMessage 3 | // attributes that are applied to this project. 4 | // Project-level suppressions either have no target or are given 5 | // a specific target and scoped to a namespace, type, member, etc. 6 | // 7 | 8 | using System.Diagnostics.CodeAnalysis; 9 | using System.Runtime.CompilerServices; 10 | 11 | [assembly: SuppressMessage("Globalization", "CA1305:Specify IFormatProvider", Justification = "[Legacy]")] 12 | [assembly: SuppressMessage("Globalization", "CA1307:Specify StringComparison", Justification = "[Legacy]")] 13 | 14 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/MultilevelCommandApp.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/NClap/Metadata/Command.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Base class for implementing commands. 8 | /// 9 | [ArgumentType(DisplayName = "command")] 10 | public abstract class Command : ICommand 11 | { 12 | /// 13 | /// Executes the command. 14 | /// 15 | /// Cancellation token. 16 | /// Result of execution. 17 | public virtual Task ExecuteAsync(CancellationToken cancel) => 18 | Task.FromResult(CommandResult.UsageError); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentShortNameHelpMode.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Mode for including short name info in help. 5 | /// 6 | public enum ArgumentShortNameHelpMode 7 | { 8 | /// 9 | /// Do not include short names. 10 | /// 11 | Omit, 12 | 13 | /// 14 | /// Include short names together with long names at the beginning of the 15 | /// description. 16 | /// 17 | IncludeWithLongName, 18 | 19 | /// 20 | /// Append short names to the end of the description. 21 | /// 22 | AppendToDescription 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/CommandTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Metadata; 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace NClap.Tests.Metadata 9 | { 10 | [TestClass] 11 | public class CommandTests 12 | { 13 | private class TestCommand : Command 14 | { 15 | } 16 | 17 | [TestMethod] 18 | public void TestThatBaseCommandImplementationReturnsUsageError() 19 | { 20 | var command = new TestCommand(); 21 | command.ExecuteAsync(CancellationToken.None).Result.Should().Be(CommandResult.UsageError); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tools/Inspector/NClap.Inspector.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | net6.0 6 | win10-x64;linux-x64 7 | false 8 | 9 | 10 | 11 | true 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentValueFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Flags controlling the use of argument values. 7 | /// 8 | [Flags] 9 | public enum ArgumentValueFlags 10 | { 11 | /// 12 | /// Indicates default behavior is desired. 13 | /// 14 | None, 15 | 16 | /// 17 | /// Indicates that the related value should not be allowed. 18 | /// 19 | Disallowed, 20 | 21 | /// 22 | /// Indicates that the related value should not be displayed in help 23 | /// text. 24 | /// 25 | Hidden 26 | } 27 | } -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentDefaultValueHelpMode.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Mode for including info about argument default values. 5 | /// 6 | public enum ArgumentDefaultValueHelpMode 7 | { 8 | /// 9 | /// Do not include default values. 10 | /// 11 | Omit, 12 | 13 | /// 14 | /// Prepend to the start of the argument's description (but after the 15 | /// argument's syntax). 16 | /// 17 | PrependToDescription, 18 | 19 | /// 20 | /// Append to the end of the argument's description. 21 | /// 22 | AppendToDescription 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/NClap/Expressions/ExpressionEnvironment.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Expressions 2 | { 3 | /// 4 | /// An expression environment. 5 | /// 6 | internal abstract class ExpressionEnvironment 7 | { 8 | /// 9 | /// Tries to retrieve the value associated with the given 10 | /// variable. 11 | /// 12 | /// Name of the variable. 13 | /// On success, receives the value associated 14 | /// with the variable. 15 | /// true if the variable was found; false otherwise. 16 | public abstract bool TryGetVariable(string variableName, out string value); 17 | } 18 | } -------------------------------------------------------------------------------- /src/Tests/TestApp/CompleteCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Metadata; 3 | using NClap.Types; 4 | 5 | namespace NClap.TestApp 6 | { 7 | class CompleteCommand : SynchronousCommand 8 | { 9 | [NamedArgument] 10 | public FileSystemPath Path { get; set; } 11 | 12 | [NamedArgument] 13 | public int Integer { get; set; } 14 | 15 | [NamedArgument] 16 | public string String { get; set; } 17 | 18 | [NamedArgument] 19 | public Guid Guid { get; set; } 20 | 21 | [NamedArgument] 22 | public bool Boolean { get; set; } 23 | 24 | public override CommandResult Execute() 25 | { 26 | return CommandResult.Success; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/UnimplementedCommandTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Metadata; 4 | using System; 5 | using System.Threading; 6 | 7 | namespace NClap.Tests.Metadata 8 | { 9 | [TestClass] 10 | public class UnimplementedCommandTests 11 | { 12 | [TestMethod] 13 | public void TestThatUnimplementedCommandAlwaysReturnsCorrectCode() 14 | { 15 | var command = new UnimplementedCommand(); 16 | command.Awaiting(c => c.ExecuteAsync(CancellationToken.None)).Should().ThrowAsync(); 17 | command.Invoking(c => c.Execute()).Should().Throw(); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/ConsoleInputOperationResult.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.ConsoleInput 2 | { 3 | /// 4 | /// The result of processing a console input operation. 5 | /// 6 | internal enum ConsoleInputOperationResult 7 | { 8 | /// 9 | /// The operation was handled, but more input is available. 10 | /// 11 | Normal, 12 | 13 | /// 14 | /// The event was handled, and the end of the input line was reached. 15 | /// 16 | EndOfInputLine, 17 | 18 | /// 19 | /// The event was handled, and the end of the input stream was reached. 20 | /// 21 | EndOfInputStream 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/NClap/Expressions/Expression.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Expressions 2 | { 3 | /// 4 | /// Base class for all expressions. 5 | /// 6 | internal abstract class Expression 7 | { 8 | /// 9 | /// Tries to evaluate the given expression in the context of 10 | /// the given environment. 11 | /// 12 | /// Environment in which to evaluate the 13 | /// expression. 14 | /// On success, receives the evaluation 15 | /// result. 16 | /// true on success; false otherwise. 17 | public abstract bool TryEvaluate(ExpressionEnvironment env, out string value); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/NClap/Metadata/HelpCommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Attribute for annotating help commands. 7 | /// 8 | [AttributeUsage(AttributeTargets.Field)] 9 | public sealed class HelpCommandAttribute : CommandAttribute 10 | { 11 | /// 12 | /// Gets the type that "implements" this command. 13 | /// 14 | /// The type of the command associated with this 15 | /// attribute. 16 | /// The type. 17 | public override Type GetImplementingType(Type commandType) => 18 | typeof(HelpCommand<>).MakeGenericType(commandType); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Tests/TestApp/ReadLineCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.ConsoleInput; 3 | using NClap.Metadata; 4 | 5 | namespace NClap.TestApp 6 | { 7 | class ReadLineCommand : SynchronousCommand 8 | { 9 | [NamedArgument(ArgumentFlags.Optional, DefaultValue = true, Description = "Echo input back to screen.")] 10 | bool Echo { get; set; } 11 | 12 | public override CommandResult Execute() 13 | { 14 | Console.WriteLine("Reading input line..."); 15 | 16 | var line = ConsoleUtilities.ReadLine(); 17 | 18 | if (Echo) 19 | { 20 | Console.WriteLine($"Read: [{line}]"); 21 | } 22 | 23 | return CommandResult.Success; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NClap/Help/OneColumnArgumentHelpLayout.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Describes a single-column argument help layout: name(s) followed 5 | /// by description (if applicable) in the second. 6 | /// 7 | public class OneColumnArgumentHelpLayout : ArgumentHelpLayout 8 | { 9 | /// 10 | /// Default constructor. 11 | /// 12 | public OneColumnArgumentHelpLayout() 13 | { 14 | } 15 | 16 | /// 17 | /// Create a separate clone of this object. 18 | /// 19 | /// Clone. 20 | public override ArgumentHelpLayout DeepClone() => new OneColumnArgumentHelpLayout(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentValidationContext.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Context for argument validation. 5 | /// 6 | public class ArgumentValidationContext 7 | { 8 | /// 9 | /// Primary constructor. 10 | /// 11 | /// File system reader for context. 12 | /// 13 | public ArgumentValidationContext(IFileSystemReader fileSystemReader) 14 | { 15 | FileSystemReader = fileSystemReader; 16 | } 17 | 18 | /// 19 | /// The file-system reader to use in this context. 20 | /// 21 | public IFileSystemReader FileSystemReader { get; } 22 | } 23 | } -------------------------------------------------------------------------------- /src/NClap/Metadata/CommandResult.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Result from a command's execution. 5 | /// 6 | public enum CommandResult 7 | { 8 | /// 9 | /// The command completed successfully. 10 | /// 11 | Success, 12 | 13 | /// 14 | /// The command requested the termination of the caller. 15 | /// 16 | Terminate, 17 | 18 | /// 19 | /// The command detected a usage or syntax error. 20 | /// 21 | UsageError, 22 | 23 | /// 24 | /// The command experienced a runtime failure. 25 | /// 26 | RuntimeFailure 27 | } 28 | } -------------------------------------------------------------------------------- /src/NClap/Utilities/CircularEnumerator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NClap.Utilities 5 | { 6 | /// 7 | /// Utilities for interacting with circular enumerators. 8 | /// 9 | internal static class CircularEnumerator 10 | { 11 | /// 12 | /// Creates a new circular enumerator. 13 | /// 14 | /// Type of the item in the enumerated list. 15 | /// 16 | /// List to be enumerated. 17 | /// The enumerator. 18 | public static CircularEnumerator Create(IReadOnlyList values) => 19 | new CircularEnumerator(values); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/GetConfigurationCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NClap.Metadata; 3 | 4 | namespace MultilevelCommandApp 5 | { 6 | internal class GetConfigurationCommand : CommandBase 7 | { 8 | private readonly ILogger logger; 9 | private readonly ConfigurationCommand configCommand; 10 | 11 | public GetConfigurationCommand(ILogger logger, ConfigurationCommand configCommand) 12 | { 13 | this.logger = logger; 14 | this.configCommand = configCommand; 15 | } 16 | 17 | public override CommandResult Execute() 18 | { 19 | logger.LogInformation($"Getting {configCommand.Policy} configuration"); 20 | return CommandResult.Success; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/SetConfigurationCommand.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NClap.Metadata; 3 | 4 | namespace MultilevelCommandApp 5 | { 6 | internal class SetConfigurationCommand : CommandBase 7 | { 8 | private readonly ILogger logger; 9 | private readonly ConfigurationCommand configCommand; 10 | 11 | public SetConfigurationCommand(ILogger logger, ConfigurationCommand configCommand) 12 | { 13 | this.logger = logger; 14 | this.configCommand = configCommand; 15 | } 16 | 17 | public override CommandResult Execute() 18 | { 19 | logger.LogInformation($"Setting {configCommand.Policy} configuration"); 20 | return CommandResult.Success; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Tests/TestApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Help; 3 | 4 | namespace NClap.TestApp 5 | { 6 | class Program 7 | { 8 | private static int Main(string[] args) 9 | { 10 | var options = new CommandLineParserOptions 11 | { 12 | HelpOptions = new ArgumentSetHelpOptions() 13 | .With() 14 | .TwoColumnLayout() 15 | }; 16 | 17 | if (!CommandLineParser.TryParse(args, options, out ProgramArguments programArgs)) 18 | { 19 | return -1; 20 | } 21 | 22 | var result = programArgs.Command.Execute(); 23 | Console.WriteLine($"Result: {result}"); 24 | 25 | return 0; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.json] 2 | indent_style = space 3 | indent_size = 4 4 | 5 | [*.{cs,vb}] 6 | # CA1711: Identifiers should not have incorrect suffix 7 | dotnet_diagnostic.CA1711.severity = none 8 | # CA1008: Enums should have zero value 9 | dotnet_diagnostic.CA1008.severity = none 10 | # CA1069: Enums should not have duplicate values 11 | dotnet_diagnostic.CA1069.severity = none 12 | # TODO: CA5392: Use DefaultDllImportSearchPaths attribute for P/Invokes 13 | dotnet_diagnostic.CA5392.severity = none 14 | # TODO: CA2237: Mark ISerializable types with SerializableAttribute 15 | dotnet_diagnostic.CA2237.severity = none 16 | # TODO: CA2201: Do not raise reserved exception types 17 | dotnet_diagnostic.CA2201.severity = none 18 | # TODO: CA1309: Use ordinal StringComparison 19 | dotnet_diagnostic.CA1309.severity = none -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/ProgramArguments.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace MultilevelCommandApp 4 | { 5 | class ProgramArguments 6 | { 7 | public enum ToplevelCommandType 8 | { 9 | [ArgumentValue(Flags = ArgumentValueFlags.Disallowed)] Invalid, 10 | 11 | [Command(typeof(ConfigurationCommand), LongName = "Config")] Configuration, 12 | [Command(typeof(StatusCommand))] Status, 13 | 14 | [Command(typeof(ExitCommand))] Exit, 15 | [HelpCommand] Help 16 | } 17 | 18 | [NamedArgument] 19 | public bool Verbose { get; set; } 20 | 21 | [PositionalArgument(ArgumentFlags.Optional)] 22 | public CommandGroup Command { get; set; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/ConsoleInput/TestTokenCompleter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NClap.ConsoleInput; 4 | 5 | namespace NClap.Tests.ConsoleInput 6 | { 7 | internal class TestTokenCompleter : ITokenCompleter 8 | { 9 | Func, int, IEnumerable> _func; 10 | 11 | public TestTokenCompleter(Func, int, IEnumerable> func) 12 | { 13 | _func = func; 14 | } 15 | 16 | public TestTokenCompleter(IEnumerable results) 17 | { 18 | _func = (tokens, index) => results; 19 | } 20 | 21 | public IEnumerable GetCompletions(IEnumerable tokens, int tokenIndex) => 22 | _func(tokens, tokenIndex); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tests/TestApp/ProgramArguments.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace NClap.TestApp 4 | { 5 | enum LogLevel 6 | { 7 | Default, 8 | Heightened, 9 | Awesome, 10 | Subdued 11 | } 12 | 13 | [ArgumentSet( 14 | Logo = Logo, 15 | Style = ArgumentSetStyle.PowerShell, 16 | Description = "Some tool that is useful only for testing.")] 17 | class ProgramArguments : HelpArgumentsBase 18 | { 19 | public const string Logo = @"My Test Tool 20 | Version 1.0"; 21 | 22 | [PositionalArgument(ArgumentFlags.Required, Position = 0)] 23 | public CommandGroup Command { get; set; } 24 | 25 | [NamedArgument(ArgumentFlags.Optional)] 26 | public LogLevel LogLevel { get; set; } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/ArgumentTypeAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Metadata; 4 | using NClap.Types; 5 | 6 | namespace NClap.Tests.Metadata 7 | { 8 | [TestClass] 9 | public class ArgumentTypeAttributeTests 10 | { 11 | private const string CustomDisplayName = "My custom display name"; 12 | 13 | [ArgumentType(DisplayName = CustomDisplayName)] 14 | private enum MyCustomType 15 | { 16 | SomeValue 17 | } 18 | 19 | [TestMethod] 20 | public void TestThatCustomDisplayNameIsObserved() 21 | { 22 | var argType = ArgumentType.GetType(typeof(MyCustomType)); 23 | argType.DisplayName.Should().Be(CustomDisplayName); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NClap/Metadata/IntegerComparisonValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Abstract base class for integer validation attributes that compare 5 | /// against a known value. 6 | /// 7 | public abstract class IntegerComparisonValidationAttribute : IntegerValidationAttribute 8 | { 9 | /// 10 | /// Constructor for derived classes to use. 11 | /// 12 | /// Value to compare against. 13 | protected IntegerComparisonValidationAttribute(object target) 14 | { 15 | Target = target; 16 | } 17 | 18 | /// 19 | /// Fixed comparison value for validation. 20 | /// 21 | public object Target { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentSetStyle.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Overall argument parsing style. 5 | /// 6 | public enum ArgumentSetStyle 7 | { 8 | /// 9 | /// No specific style is desired or specified. 10 | /// 11 | Unspecified, 12 | 13 | /// 14 | /// The style of simple Windows command-line tools. 15 | /// 16 | WindowsCommandLine, 17 | 18 | /// 19 | /// The style of PowerShell cmdlets. 20 | /// 21 | PowerShell, 22 | 23 | /// 24 | /// The style of apps and scripts implemented using getopt and its 25 | /// default formatting. 26 | /// 27 | GetOpt 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/NClap/Metadata/IArgumentProvider.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Interface for an object to expose additional arguments that it does 7 | /// not directly contain. 8 | /// 9 | internal interface IArgumentProvider 10 | { 11 | /// 12 | /// Retrieve info for the object type that defines the arguments to be 13 | /// parsed. 14 | /// 15 | /// The defining type. 16 | Type GetTypeDefiningArguments(); 17 | 18 | /// 19 | /// Retrieve a reference to the object into which parsed arguments 20 | /// should be stored. 21 | /// 22 | /// The object in question. 23 | object GetDestinationObject(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentNameGenerationFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Style flags for names generated automatically from code symbols. 7 | /// 8 | [Flags] 9 | public enum ArgumentNameGenerationFlags 10 | { 11 | /// 12 | /// Use the code symbol verbatim. 13 | /// 14 | UseOriginalCodeSymbol = 0x0, 15 | 16 | /// 17 | /// Make a best effort attempt to convert code symbols to hyphenated, 18 | /// lower-case symbols when generating long names. 19 | /// 20 | GenerateHyphenatedLowerCaseLongNames = 0x1, 21 | 22 | /// 23 | /// Prefer lower case short names. 24 | /// 25 | PreferLowerCaseForShortNames = 0x2 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Help/ArgumentSetUsageInfoTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Help; 4 | using NClap.Metadata; 5 | using NClap.Parser; 6 | 7 | namespace NClap.Tests.Help 8 | { 9 | [TestClass] 10 | public class ArgumentSetUsageInfoTests 11 | { 12 | [TestMethod] 13 | public void TestThatGetLogoYieldsEmptyStringLogoCannotBeExpanded() 14 | { 15 | var attrib = new ArgumentSetAttribute 16 | { 17 | Logo = "{", 18 | ExpandLogo = true 19 | }; 20 | 21 | var argSet = new ArgumentSetDefinition(attrib); 22 | var usageInfo = new ArgumentSetUsageInfo(argSet, null); 23 | 24 | var logo = usageInfo.Logo; 25 | logo.Should().BeEmpty(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/IConsoleInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.ConsoleInput 4 | { 5 | /// 6 | /// Abstract interface for interacting with an input console. 7 | /// 8 | public interface IConsoleInput 9 | { 10 | /// 11 | /// True if Control-C is treated as a normal input character; false if 12 | /// it's specially handled. 13 | /// 14 | bool TreatControlCAsInput { get; set; } 15 | 16 | /// 17 | /// Reads a key press from the console. 18 | /// 19 | /// True to suppress auto-echoing the key's 20 | /// character; false to echo it as normal. 21 | /// Info about the press. 22 | ConsoleKeyInfo ReadKey(bool suppressEcho); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/StringsTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.VisualStudio.TestTools.UnitTesting; 2 | using FluentAssertions; 3 | 4 | namespace NClap.Tests 5 | { 6 | [TestClass] 7 | public class StringsTests 8 | { 9 | [TestMethod] 10 | public void Instantiate() 11 | { 12 | var s = new Strings(); 13 | s.Should().NotBeNull(); 14 | } 15 | 16 | [TestMethod] 17 | public void Culture() 18 | { 19 | // The culture is initially null. 20 | Strings.Culture.Should().BeNull(); 21 | 22 | // Should be okay to set to null too. 23 | Strings.Culture = null; 24 | } 25 | 26 | [TestMethod] 27 | public void DefaultPrompt() 28 | { 29 | Strings.DefaultPrompt.Should().NotBeNullOrWhiteSpace(); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/ITokenCompleter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.ConsoleInput 4 | { 5 | /// 6 | /// Abstract interface for an object that can generate completions for a console 7 | /// input token. 8 | /// 9 | public interface ITokenCompleter 10 | { 11 | /// 12 | /// Retrieves the completions for the given token, in the context of the given 13 | /// set of tokens. 14 | /// 15 | /// The current set of tokens. 16 | /// The 0-based index of the token to get completions 17 | /// for. 18 | /// The enumeration of completions for the token. 19 | IEnumerable GetCompletions(IEnumerable tokens, int tokenIndex); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NClap/Types/IStringParser.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics.CodeAnalysis; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Interface implemented by objects that can parse strings. 7 | /// 8 | public interface IStringParser 9 | { 10 | /// 11 | /// Tries to parse the provided string, extracting a value of the type 12 | /// described by this interface. 13 | /// 14 | /// Context for parsing. 15 | /// The string to parse. 16 | /// On success, receives the parsed value; null 17 | /// otherwise. 18 | /// True on success; false otherwise. 19 | bool TryParse(ArgumentParseContext context, string stringToParse, out object value); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ICommandGroup.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Metadata 2 | { 3 | /// 4 | /// Interface for interacting with any command group. 5 | /// 6 | public interface ICommandGroup : ICommand 7 | { 8 | /// 9 | /// True if the group has a selection, false if no selection was yet 10 | /// made. 11 | /// 12 | bool HasSelection { get; } 13 | 14 | /// 15 | /// The enum value corresponding with the selected command, or null if no 16 | /// selection has yet been made. 17 | /// 18 | object Selection { get; } 19 | 20 | /// 21 | /// The command presently selected from this group, or null if no 22 | /// selection has yet been made. 23 | /// 24 | ICommand InstantiatedCommand { get; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/NClap/Metadata/SynchronousCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Base class for implementing synchronously executing commands. 8 | /// 9 | public abstract class SynchronousCommand : Command 10 | { 11 | /// 12 | /// Executes the command. 13 | /// 14 | /// Cancellation token. 15 | /// Result of execution. 16 | public override Task ExecuteAsync(CancellationToken cancel) => 17 | Task.Run(() => Execute(), cancel); 18 | 19 | /// 20 | /// Executes the command. 21 | /// 22 | /// Result of execution. 23 | public abstract CommandResult Execute(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | version: 0.1 2 | actions: 3 | disabled: 4 | - trunk-announce 5 | - trunk-check-pre-push 6 | - trunk-fmt-pre-commit 7 | enabled: 8 | - trunk-cache-prune 9 | - trunk-upgrade-available 10 | runtimes: 11 | enabled: 12 | - go@1.18.3 13 | - node@16.14.2 14 | cli: 15 | version: 0.18.0-beta 16 | sha256: 17 | darwin_arm64: ed797167515f28c22d5f7bd553f67fd94ce84d5f709963bfc2af1c2ecba10d6a 18 | darwin_x86_64: d40927a6b7a84d00103044c342ed240baab52eeb9e0f6d40e5d2adff299889ec 19 | linux_x86_64: 4da43299049fb1836960b72de4f6830f5e672ca876656836b85588d7a5723eab 20 | plugins: 21 | sources: 22 | - id: trunk 23 | ref: v0.0.4 24 | uri: https://github.com/trunk-io/plugins 25 | lint: 26 | enabled: 27 | - actionlint@1.6.19 28 | - git-diff-check@SYSTEM 29 | - gitleaks@8.13.0 30 | - hadolint@2.10.0 31 | - markdownlint@0.32.2 32 | - prettier@2.7.1 33 | -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/ConfigurationCommand.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | using NClap.Metadata; 4 | 5 | namespace MultilevelCommandApp 6 | { 7 | enum ConfigurationPolicy 8 | { 9 | Permanent, 10 | Ephemeral 11 | } 12 | 13 | internal class ConfigurationCommand : ICommand 14 | { 15 | internal enum Ty 16 | { 17 | [Command(typeof(GetConfigurationCommand))] Get, 18 | [Command(typeof(SetConfigurationCommand))] Set 19 | } 20 | 21 | [NamedArgument(ArgumentFlags.Optional)] 22 | public ConfigurationPolicy Policy { get; set; } 23 | 24 | [PositionalArgument(ArgumentFlags.Required, LongName = "ConfigAction")] 25 | public CommandGroup Command { get; set; } 26 | 27 | public Task ExecuteAsync(CancellationToken cancel) => Command.ExecuteAsync(cancel); 28 | } 29 | } -------------------------------------------------------------------------------- /src/NClap/Utilities/MaybeUtilities.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace NClap.Utilities 5 | { 6 | /// 7 | /// Extension methods for objects. 8 | /// 9 | internal static class MaybeUtilities 10 | { 11 | /// 12 | /// From the given enumeration of values, yield an enumeration 13 | /// with only the values present (and unwrap them from their 14 | /// objects). 15 | /// 16 | /// Value type. 17 | /// Input enumeration. 18 | /// The resulting enumeration. 19 | public static IEnumerable WhereHasValue(this IEnumerable> values) => 20 | values.Where(v => v.HasValue).Select(v => v.Value); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentEnumValueHelpFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Help 4 | { 5 | /// 6 | /// Mode for generating help for enum values. 7 | /// 8 | [Flags] 9 | public enum ArgumentEnumValueHelpFlags 10 | { 11 | /// 12 | /// Each enum is documented in the default format, at each use site. 13 | /// This summary will be duplicated if the enum type is used in multiple 14 | /// arguments. 15 | /// 16 | None = 0, 17 | 18 | /// 19 | /// Enum types with multiple references will be promoted to their own 20 | /// sections. 21 | /// 22 | SingleSummaryOfEnumsWithMultipleUses = 0x1, 23 | 24 | /// 25 | /// Command enums will be promoted to their own sections. 26 | /// 27 | SingleSummaryOfAllCommandEnums = 0x2 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/ArgumentValueAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Exceptions; 4 | using NClap.Metadata; 5 | 6 | namespace NClap.Tests.Metadata 7 | { 8 | [TestClass] 9 | public class ArgumentValueAttributeTests 10 | { 11 | [TestMethod] 12 | public void TestThatLongNameAcceptsNull() 13 | { 14 | var attrib = new ArgumentValueAttribute(); 15 | 16 | attrib.Invoking(a => a.LongName = null) 17 | .Should().NotThrow(); 18 | } 19 | 20 | [TestMethod] 21 | public void TestThatLongNameThrowsOnEmptyString() 22 | { 23 | var attrib = new ArgumentValueAttribute(); 24 | 25 | attrib.Invoking(a => a.LongName = string.Empty) 26 | .Should().Throw(); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ExtensibleEnumAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Types; 3 | 4 | namespace NClap.Metadata 5 | { 6 | // CA1019: Define accessors for attribute arguments 7 | #pragma warning disable CA1019 8 | 9 | /// 10 | /// Attribute that indicates the associated enum type is extensible. 11 | /// 12 | [AttributeUsage(AttributeTargets.Enum, AllowMultiple = true)] 13 | public sealed class ExtensibleEnumAttribute : Attribute 14 | { 15 | /// 16 | /// Constructor. 17 | /// 18 | /// Provider. 19 | public ExtensibleEnumAttribute(Type provider) 20 | { 21 | Provider = provider; 22 | } 23 | 24 | /// 25 | /// Implementation of . 26 | /// 27 | public Type Provider { get; set; } 28 | } 29 | 30 | #pragma warning restore CA1019 31 | } 32 | -------------------------------------------------------------------------------- /src/NClap/Types/IStringCompleter.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Interface implemented by objects that can generate completions for a 7 | /// given string. 8 | /// 9 | public interface IStringCompleter 10 | { 11 | /// 12 | /// Generates a set of valid strings--parseable to this type--that 13 | /// contain the provided string as a strict prefix. 14 | /// 15 | /// Context for parsing. 16 | /// The string to complete. 17 | /// An enumeration of a set of completion strings; if no such 18 | /// strings could be generated, or if the type doesn't support 19 | /// completion, then an empty enumeration is returned. 20 | IEnumerable GetCompletions(ArgumentCompletionContext context, string valueToComplete); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Exceptions/InvalidArgumentSetExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Exceptions; 4 | using NClap.Metadata; 5 | using NClap.Tests.Metadata; 6 | 7 | namespace NClap.Tests.Exceptions 8 | { 9 | [TestClass] 10 | public class InvalidArgumentSetExceptionTests : ExceptionTests 11 | { 12 | public class SampleArguments 13 | { 14 | [NamedArgument] public int Value; 15 | } 16 | 17 | [TestMethod] 18 | public void ArgumentConstructor() 19 | { 20 | const string innerMessage = "Something message-like."; 21 | 22 | var arg = ArgumentTests.GetArgument(typeof(SampleArguments), "Value"); 23 | var exn = new InvalidArgumentSetException(arg, innerMessage); 24 | exn.Argument.Should().BeSameAs(arg); 25 | exn.InnerMessage.Should().Be(innerMessage); 26 | exn.Message.Should().Contain(innerMessage); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/TestApp/MainCommandType.cs: -------------------------------------------------------------------------------- 1 | using NClap.Metadata; 2 | 3 | namespace NClap.TestApp 4 | { 5 | [ArgumentType(DisplayName = "REPL command")] 6 | enum MainCommandType 7 | { 8 | [HelpCommand(Description = "Displays command help")] 9 | Help, 10 | 11 | [Command(typeof(CliHelp), Description = "Displays toplevel help")] 12 | CliHelp, 13 | 14 | [Command(typeof(CompleteCommand), Description = "Useful only for completing")] 15 | Complete, 16 | 17 | [Command(typeof(LogoCommand), Description = "Display 'logo'")] 18 | Logo, 19 | 20 | [Command(typeof(ReadLineCommand), ShortName = "readl", Description = "Reads a line of input")] 21 | ReadLine, 22 | 23 | [Command(typeof(ReplCommand), Description = "Starts interactive loop")] 24 | Repl, 25 | 26 | [Command(typeof(CommandGroup), Description = "Test sub-command group")] 27 | SubCommand, 28 | 29 | [Command(typeof(ExitCommand), Description = "Exits the loop")] 30 | Exit 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NClap/Metadata/HelpCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Help; 3 | using NClap.Utilities; 4 | 5 | namespace NClap.Metadata 6 | { 7 | /// 8 | /// Static class useful for configuring the behavior of help commands. 9 | /// 10 | public static class HelpCommand 11 | { 12 | /// 13 | /// The default options to use for generate help. 14 | /// 15 | [Obsolete("Loop help may be customized with LoopOptions instead.")] 16 | public static ArgumentSetHelpOptions DefaultHelpOptions { get; set; } = 17 | new ArgumentSetHelpOptions 18 | { 19 | Logo = new ArgumentMetadataHelpOptions { Include = false }, 20 | Name = string.Empty 21 | }; 22 | 23 | /// 24 | /// The output handler function for this class. 25 | /// 26 | [Obsolete("Loop help may be customized with LoopOptions instead.")] 27 | public static Action OutputHandler { get; set; } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/ColoredStringExtensionMethodsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Utilities; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | [TestClass] 9 | public class ColoredStringExtensionMethodsTests 10 | { 11 | [TestMethod] 12 | public void TestThatTransformThrowsOnNullFunc() 13 | { 14 | var anyCs = AnyColoredString(); 15 | anyCs.Invoking(cs => cs.Transform(null)).Should().Throw(); 16 | } 17 | 18 | [TestMethod] 19 | public void TestThatTransformPreservesColor() 20 | { 21 | var anyCs = AnyColoredString(); 22 | const string anyString = "Something different"; 23 | var updated = anyCs.Transform(_ => anyString); 24 | 25 | updated.IsSameColorAs(anyCs).Should().BeTrue(); 26 | } 27 | 28 | private ColoredString AnyColoredString() => 29 | new ColoredString("Some text", Any.Enum(), Any.Enum()); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/NClap/Utilities/TokenizerOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Utilities 4 | { 5 | /// 6 | /// Options for tokenizing command lines. 7 | /// 8 | [Flags] 9 | internal enum TokenizerOptions 10 | { 11 | /// 12 | /// Do not apply other policy options. 13 | /// 14 | None, 15 | 16 | /// 17 | /// Allow tokenizing of partial (incomplete) input lines; this includes 18 | /// ignoring errors related to unmatched quotes around the last token. 19 | /// 20 | AllowPartialInput, 21 | 22 | /// 23 | /// Handle double quote character as a token delimiter (allowing embedded 24 | /// whitespace within the token. 25 | /// 26 | HandleDoubleQuoteAsTokenDelimiter, 27 | 28 | /// 29 | /// Handle single quote character as a token delimiter (allowing embedded 30 | /// whitespace within the token. 31 | /// 32 | HandleSingleQuoteAsTokenDelimiter, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentSetHelpSectionType.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Type of section in argument help output. 5 | /// 6 | internal enum ArgumentSetHelpSectionType 7 | { 8 | /// 9 | /// Argument set description. 10 | /// 11 | ArgumentSetDescription, 12 | 13 | /// 14 | /// Summary of separately documented enum values. 15 | /// 16 | EnumValues, 17 | 18 | /// 19 | /// Example usage information. 20 | /// 21 | Examples, 22 | 23 | /// 24 | /// Argument set logo. 25 | /// 26 | Logo, 27 | 28 | /// 29 | /// Optional parameters. 30 | /// 31 | OptionalParameters, 32 | 33 | /// 34 | /// Required parameters. 35 | /// 36 | RequiredParameters, 37 | 38 | /// 39 | /// Syntax summary. 40 | /// 41 | Syntax, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Samples/HelpApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap; 3 | using NClap.Help; 4 | 5 | namespace HelpApp 6 | { 7 | class Program 8 | { 9 | static int Main(string[] args) 10 | { 11 | Console.WriteLine("Parsing..."); 12 | 13 | // Set up help options the way we want them. 14 | var helpOptions = new ArgumentSetHelpOptions() 15 | .With() 16 | .BlankLinesBetweenArguments(1) 17 | .ShortNames(ArgumentShortNameHelpMode.IncludeWithLongName) 18 | .DefaultValues(ArgumentDefaultValueHelpMode.PrependToDescription) 19 | .TwoColumnLayout(); 20 | 21 | // Wrap help options in general parsing options. 22 | var options = new CommandLineParserOptions { HelpOptions = helpOptions }; 23 | 24 | // Try to parse. 25 | if (!CommandLineParser.TryParse(args, options, out ProgramArguments programArgs)) 26 | { 27 | return 1; 28 | } 29 | 30 | Console.WriteLine("Successfully parsed."); 31 | 32 | return 0; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NClap/Metadata/CommandGroupOptions.cs: -------------------------------------------------------------------------------- 1 | using NClap.Utilities; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Options for command groups. 7 | /// 8 | public class CommandGroupOptions : IDeepCloneable 9 | { 10 | /// 11 | /// Default constructor. 12 | /// 13 | internal CommandGroupOptions() 14 | { 15 | } 16 | 17 | /// 18 | /// Deeply cloning constructor. 19 | /// 20 | /// Template for clone. 21 | private CommandGroupOptions(CommandGroupOptions other) 22 | { 23 | ServiceConfigurer = other.ServiceConfigurer; 24 | } 25 | 26 | /// 27 | /// Service configurer. 28 | /// 29 | internal ServiceConfigurer ServiceConfigurer { get; set; } 30 | 31 | /// 32 | /// Duplicates the options. 33 | /// 34 | /// The duplicate. 35 | public CommandGroupOptions DeepClone() => new CommandGroupOptions(this); 36 | } 37 | } -------------------------------------------------------------------------------- /src/NClap/Metadata/NumberOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Options describing how to parse numeric values. 7 | /// 8 | [Flags] 9 | public enum NumberOptions 10 | { 11 | /// 12 | /// Default behavior. 13 | /// 14 | None, 15 | 16 | /// 17 | /// Allow use of metric unit suffixes (e.g. k to denote a multiplier of 18 | /// 1 thousand, M to denote a multiplier of 1 million). This option 19 | /// conflicts with AllowBinaryMetricUnitSuffix. If both flags are 20 | /// present, then AllowBinaryMetricUnitSuffix takes precedence. 21 | /// 22 | AllowMetricUnitSuffix, 23 | 24 | /// 25 | /// Allow use of binary metric unit suffixes (e.g. k to denote 1024, 26 | /// M to denote 1024 * 1024). This option conflicts with 27 | /// AllowMetricUnitSuffix. If both flags are present, then 28 | /// AllowBinaryMetricUnitSuffix takes precedence. 29 | /// 30 | AllowBinaryMetricUnitSuffix 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Microsoft Corporation 4 | 5 | All rights reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | // Use IntelliSense to find out which attributes exist for C# debugging 6 | // Use hover for the description of the existing attributes 7 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/Tests/UnitTests/bin/Debug/net6.0/NClap.Tests.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/Tests/UnitTests", 16 | // For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console 17 | "console": "internalConsole", 18 | "stopAtEntry": false 19 | }, 20 | { 21 | "name": ".NET Core Attach", 22 | "type": "coreclr", 23 | "request": "attach" 24 | } 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/NClap/Metadata/FileSystemPathValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Types; 3 | using NClap.Utilities; 4 | 5 | namespace NClap.Metadata 6 | { 7 | /// 8 | /// Abstract base class for implementing argument validation attributes 9 | /// that inspect file-system paths. 10 | /// 11 | public abstract class FileSystemPathValidationAttribute : ArgumentValidationAttribute 12 | { 13 | /// 14 | /// Checks if this validation attributes accepts values of the specified 15 | /// type. 16 | /// 17 | /// Type to check. 18 | /// True if this attribute accepts values of the specified 19 | /// type; false if not. 20 | /// Thrown when 21 | /// is null. 22 | public sealed override bool AcceptsType(IArgumentType type) 23 | { 24 | if (type == null) throw new ArgumentNullException(nameof(type)); 25 | return (type.Type == typeof(string)) || type.Type.IsEffectivelySameAs(typeof(FileSystemPath)); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/NClap/Types/IEnumArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Interface for advertising a type as being parseable 7 | /// using this assembly. The implementation provides sufficient 8 | /// functionality for command-line parsing, generating usage help 9 | /// information, etc. This interface should only be implemented 10 | /// by objects that describe .NET enum objects. 11 | /// 12 | public interface IEnumArgumentType : IArgumentType 13 | { 14 | /// 15 | /// Enumerate the values allowed for this enum. 16 | /// 17 | /// The values. 18 | IEnumerable GetValues(); 19 | 20 | /// 21 | /// Tries to look up the corresponding with 22 | /// the given object. 23 | /// 24 | /// Object to look up. 25 | /// On success, receives the object's value. 26 | /// true on success; false otherwise. 27 | bool TryGetValue(object value, out IArgumentValue argValue); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/DeepCloneTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using FluentAssertions; 4 | using NClap.Utilities; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | public static class DeepCloneTests 9 | { 10 | public static void CloneShouldYieldADistinctButEquivalentObject(T instance) 11 | where T : IDeepCloneable 12 | { 13 | var type = instance.GetType(); 14 | var clone = instance.DeepClone(); 15 | 16 | clone.Should().NotBeNull(); 17 | clone.Should().NotBeSameAs(instance); 18 | clone.Should().BeOfType(type); 19 | clone.Should().BeEquivalentTo(instance); 20 | 21 | var allProps = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); 22 | foreach (var prop in allProps.Where(p => p.PropertyType.GetTypeInfo().IsByRef)) 23 | { 24 | var instanceValue = prop.GetValue(instance); 25 | if (instanceValue != null) 26 | { 27 | var cloneValue = prop.GetValue(clone); 28 | instanceValue.Should().NotBeSameAs(cloneValue); 29 | } 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/NClap/Metadata/IntegerValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using NClap.Types; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Abstract base class for implementing argument validation attributes 7 | /// that inspect integers. 8 | /// 9 | public abstract class IntegerValidationAttribute : ArgumentValidationAttribute 10 | { 11 | /// 12 | /// Checks if this validation attributes accepts values of the specified 13 | /// type. 14 | /// 15 | /// Type to check. 16 | /// True if this attribute accepts values of the specified 17 | /// type; false if not. 18 | public sealed override bool AcceptsType(IArgumentType type) => 19 | type is IntegerArgumentType; 20 | 21 | /// 22 | /// Retrieves the the argument type associated with the provided integer 23 | /// value. 24 | /// 25 | /// The value. 26 | /// The argument type. 27 | internal static IntegerArgumentType GetArgumentType(object value) => 28 | (IntegerArgumentType)ArgumentType.GetType(value.GetType()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Types/ArgumentTypeExtensionTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | 5 | using NClap.Types; 6 | 7 | namespace NClap.Tests.Types 8 | { 9 | [TestClass] 10 | public class ArgumentTypeExtensionTests 11 | { 12 | [TestMethod] 13 | public void NonOverriddenExtensionConstructedFromType() 14 | { 15 | var argType = new ArgumentTypeExtension(typeof(int)); 16 | argType.InnerType.Type.Should().Be(typeof(int)); 17 | argType.DisplayName.Should().Be(argType.InnerType.DisplayName); 18 | argType.Type.Should().Be(argType.InnerType.Type); 19 | argType.SyntaxSummary.Should().Be(argType.InnerType.SyntaxSummary); 20 | } 21 | 22 | [TestMethod] 23 | public void NonOverriddenExtensionConstructedFromArgType() 24 | { 25 | var argType = new ArgumentTypeExtension(ArgumentType.Int); 26 | argType.InnerType.Should().Be(ArgumentType.Int); 27 | argType.DisplayName.Should().Be(argType.InnerType.DisplayName); 28 | argType.Type.Should().Be(argType.InnerType.Type); 29 | argType.SyntaxSummary.Should().Be(argType.InnerType.SyntaxSummary); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/NClap/Exceptions/InternalInvariantBrokenException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Exceptions 4 | { 5 | /// 6 | /// Exception thrown when an internal invariant is broken; if this is 7 | /// thrown, then there is a code defect in this library. 8 | /// 9 | public sealed class InternalInvariantBrokenException : Exception 10 | { 11 | /// 12 | /// Standard parameterless constructor. 13 | /// 14 | public InternalInvariantBrokenException() 15 | { 16 | } 17 | 18 | /// 19 | /// Standard constructor that takes a string message. 20 | /// 21 | /// Message. 22 | public InternalInvariantBrokenException(string message) : base(message) 23 | { 24 | } 25 | 26 | /// 27 | /// Standard constructor that wraps an inner exception. 28 | /// 29 | /// Message. 30 | /// Inner exception to wrap. 31 | public InternalInvariantBrokenException(string message, Exception innerException) : base(message, innerException) 32 | { 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NClap/Parser/ArgumentSetParseResultType.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Parser 2 | { 3 | /// 4 | /// Describes a result state for parsing an argument set. 5 | /// 6 | internal enum ArgumentSetParseResultType 7 | { 8 | /// 9 | /// Parser is ready to parse more arguments. 10 | /// 11 | Ready, 12 | 13 | /// 14 | /// Parser has encountered an unknown named argument. 15 | /// 16 | UnknownNamedArgument, 17 | 18 | /// 19 | /// Parser has encountered an unknown positional argument. 20 | /// 21 | UnknownPositionalArgument, 22 | 23 | /// 24 | /// Parser has failed to parse an argument. 25 | /// 26 | FailedParsing, 27 | 28 | /// 29 | /// Parser has failed to finalize an argument set. 30 | /// 31 | FailedFinalizing, 32 | 33 | /// 34 | /// Parser has encountered an invalid answer file. 35 | /// 36 | InvalidAnswerFile, 37 | 38 | /// 39 | /// Parser is waiting for an option argument. 40 | /// 41 | RequiresOptionArgument 42 | } 43 | } -------------------------------------------------------------------------------- /src/Tests/UnitTests/CommandLineParserOptionsTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Tests.Utilities; 4 | 5 | namespace NClap.Tests 6 | { 7 | [TestClass] 8 | public class CommandLineParserOptionsTests 9 | { 10 | [TestMethod] 11 | public void TestThatDefaultOptionsCloneCorrectly() 12 | { 13 | var options = new CommandLineParserOptions(); 14 | DeepCloneTests.CloneShouldYieldADistinctButEquivalentObject(options); 15 | } 16 | 17 | [TestMethod] 18 | public void TestThatRequiredPropertiesArePresentInDefaultOptions() 19 | { 20 | var options = new CommandLineParserOptions(); 21 | options.HelpOptions.Should().NotBeNull(); 22 | options.Reporter.Should().NotBeNull(); 23 | options.FileSystemReader.Should().NotBeNull(); 24 | } 25 | 26 | [TestMethod] 27 | public void TestThatRequiredPropertiesArePresentInQuietOptions() 28 | { 29 | var options = CommandLineParserOptions.Quiet(); 30 | options.HelpOptions.Should().NotBeNull(); 31 | options.Reporter.Should().NotBeNull(); 32 | options.FileSystemReader.Should().NotBeNull(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/IConsoleReader.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.ConsoleInput 2 | { 3 | /// 4 | /// An advanced console reader. 5 | /// 6 | public interface IConsoleReader 7 | { 8 | /// 9 | /// The beginning-of-line comment character. 10 | /// 11 | char? CommentCharacter { get; set; } 12 | 13 | /// 14 | /// The console being used for input. 15 | /// 16 | IConsoleInput ConsoleInput { get; } 17 | 18 | /// 19 | /// The console being used for output. 20 | /// 21 | IConsoleOutput ConsoleOutput { get; } 22 | 23 | /// 24 | /// The inner line input object. 25 | /// 26 | IConsoleLineInput LineInput { get; } 27 | 28 | /// 29 | /// The console key bindings used by this console reader. 30 | /// 31 | IReadOnlyConsoleKeyBindingSet KeyBindingSet { get; } 32 | 33 | /// 34 | /// Reads a line of input text from the underlying console. 35 | /// 36 | /// The line of text, or null if the end of input was 37 | /// encountered. 38 | string ReadLine(); 39 | } 40 | } -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/EnumerableUtilitiesTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Utilities; 4 | using System.Linq; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | [TestClass] 9 | public class EnumerableUtilitiesTests 10 | { 11 | [TestMethod] 12 | public void TestThatInsertBetweenLeavesEmptyEnumerationUnmodified() 13 | { 14 | Enumerable.Empty().InsertBetween("x").Should().BeEmpty(); 15 | } 16 | 17 | [TestMethod] 18 | public void TestThatInsertBetweenLeavesOneElementEnumerationUnmodified() 19 | { 20 | var input = new[] { "elt" }; 21 | input.InsertBetween("x").Should().Equal(input); 22 | } 23 | 24 | [TestMethod] 25 | public void TestThatInsertBetweenCanInsertOnce() 26 | { 27 | var input = new[] { "first", "last" }; 28 | input.InsertBetween("x").Should().Equal("first", "x", "last"); 29 | } 30 | 31 | [TestMethod] 32 | public void TestThatInsertBetweenCanInsertMultipleTimes() 33 | { 34 | var input = new[] { "first", "second", "third" }; 35 | input.InsertBetween("x").Should().Equal("first", "x", "second", "x", "third"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/MutableFieldInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Utilities; 6 | 7 | namespace NClap.Tests.Utilities 8 | { 9 | [TestClass] 10 | public class MutableFieldInfoTests 11 | { 12 | class TestObject 13 | { 14 | #pragma warning disable 0649 // Field is never assigned to, and will always have its default value 15 | public T Value; 16 | #pragma warning restore 0649 17 | } 18 | 19 | [TestMethod] 20 | public void ConversionFails() 21 | { 22 | var prop = new MutableFieldInfo(typeof(TestObject).GetTypeInfo().GetField("Value")); 23 | var obj = new TestObject(); 24 | Action setter = () => prop.SetValue(obj, 3.0); 25 | setter.Should().Throw(); 26 | } 27 | 28 | [TestMethod] 29 | public void ConversionSucceeds() 30 | { 31 | var prop = new MutableFieldInfo(typeof(TestObject).GetTypeInfo().GetField("Value")); 32 | var obj = new TestObject(); 33 | Action setter = () => prop.SetValue(obj, 3L); 34 | setter.Should().NotThrow(); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentEnumValueHelpOptions.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Help options for enum value help content. 5 | /// 6 | public class ArgumentEnumValueHelpOptions : ArgumentMetadataHelpOptions 7 | { 8 | /// 9 | /// Default constructor. 10 | /// 11 | public ArgumentEnumValueHelpOptions() 12 | { 13 | } 14 | 15 | /// 16 | /// Deeply cloning constructor. 17 | /// 18 | /// Template for clone. 19 | private ArgumentEnumValueHelpOptions(ArgumentEnumValueHelpOptions other) : base(other) 20 | { 21 | Flags = other.Flags; 22 | } 23 | 24 | /// 25 | /// Whether or not enum value summaries should be fully promoted to their 26 | /// own section, etc. 27 | /// 28 | public ArgumentEnumValueHelpFlags Flags { get; set; } = 29 | ArgumentEnumValueHelpFlags.SingleSummaryOfEnumsWithMultipleUses; 30 | 31 | /// 32 | /// Create a separate clone of this object. 33 | /// 34 | /// Clone. 35 | public override ArgumentMetadataHelpOptions DeepClone() => new ArgumentEnumValueHelpOptions(this); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/Tests/UnitTests/NClap.Tests.csproj", 11 | "/property:GenerateFullPaths=true", 12 | "/consoleloggerparameters:NoSummary" 13 | ], 14 | "problemMatcher": "$msCompile" 15 | }, 16 | { 17 | "label": "publish", 18 | "command": "dotnet", 19 | "type": "process", 20 | "args": [ 21 | "publish", 22 | "${workspaceFolder}/src/Tests/UnitTests/NClap.Tests.csproj", 23 | "/property:GenerateFullPaths=true", 24 | "/consoleloggerparameters:NoSummary" 25 | ], 26 | "problemMatcher": "$msCompile" 27 | }, 28 | { 29 | "label": "watch", 30 | "command": "dotnet", 31 | "type": "process", 32 | "args": [ 33 | "watch", 34 | "run", 35 | "--project", 36 | "${workspaceFolder}/src/Tests/UnitTests/NClap.Tests.csproj" 37 | ], 38 | "problemMatcher": "$msCompile" 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /src/NClap/ConsoleInput/IConsoleHistory.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace NClap.ConsoleInput 4 | { 5 | /// 6 | /// Abstract interface for managing console history. 7 | /// 8 | public interface IConsoleHistory 9 | { 10 | /// 11 | /// The count of entries in the history. 12 | /// 13 | int EntryCount { get; } 14 | 15 | /// 16 | /// If the cursor is valid, the current entry in the history; null 17 | /// otherwise. 18 | /// 19 | string CurrentEntry { get; } 20 | 21 | /// 22 | /// Add a new entry to the end of the history, and reset the history's 23 | /// cursor to that new entry. 24 | /// 25 | /// Entry to add. 26 | void Add(string entry); 27 | 28 | /// 29 | /// Move the current history cursor by the specified offset. 30 | /// 31 | /// Reference for movement. 32 | /// Positive or negative offset to apply to the 33 | /// specified origin. 34 | /// True on success; false if the move could not be made. 35 | /// 36 | bool MoveCursor(SeekOrigin origin, int offset); 37 | } 38 | } -------------------------------------------------------------------------------- /src/NClap/Expressions/ParenthesisExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Expressions 4 | { 5 | /// 6 | /// Parenthesis expression. 7 | /// 8 | internal class ParenthesisExpression : Expression 9 | { 10 | /// 11 | /// Basic constructor. 12 | /// 13 | /// Inner expression. 14 | public ParenthesisExpression(Expression innerExpression) 15 | { 16 | InnerExpression = innerExpression ?? throw new ArgumentNullException(nameof(innerExpression)); 17 | } 18 | 19 | /// 20 | /// Inner expression. 21 | /// 22 | public Expression InnerExpression { get; } 23 | 24 | /// 25 | /// Tries to evaluate the given expression in the context of 26 | /// the given environment. 27 | /// 28 | /// Environment in which to evaluate the 29 | /// expression. 30 | /// On success, receives the evaluation 31 | /// result. 32 | /// true on success; false otherwise. 33 | public override bool TryEvaluate(ExpressionEnvironment env, out string value) => 34 | InnerExpression.TryEvaluate(env, out value); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustNotBeEmptyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Attribute that indicates the associated string argument member cannot be 7 | /// empty. 8 | /// 9 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 10 | public sealed class MustNotBeEmptyAttribute : StringValidationAttribute 11 | { 12 | /// 13 | /// Validate the provided value in accordance with the attribute's 14 | /// policy. 15 | /// 16 | /// Context for validation. 17 | /// The value to validate. 18 | /// On failure, receives a user-readable string 19 | /// message explaining why the value is not valid. 20 | /// True if the value passes validation; false otherwise. 21 | /// 22 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 23 | { 24 | if (!string.IsNullOrEmpty(GetString(value))) 25 | { 26 | reason = null; 27 | return true; 28 | } 29 | 30 | reason = Strings.StringIsEmpty; 31 | return false; 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/NClap.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net461;netcoreapp3.1;net6.0 5 | win10-x64;linux-x64 6 | latest 7 | false 8 | 9 | 10 | 11 | true 12 | 13 | full 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/NClap/Expressions/StringLiteral.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Expressions 4 | { 5 | /// 6 | /// A string literal expression. 7 | /// 8 | internal class StringLiteral : Expression 9 | { 10 | /// 11 | /// Basic constructor. 12 | /// 13 | /// Literal string value. 14 | public StringLiteral(string value) 15 | { 16 | Value = value ?? throw new ArgumentNullException(nameof(value)); 17 | } 18 | 19 | /// 20 | /// Literal string. 21 | /// 22 | public string Value { get; } 23 | 24 | /// 25 | /// Tries to evaluate the given expression in the context of 26 | /// the given environment. 27 | /// 28 | /// Environment in which to evaluate the 29 | /// expression. 30 | /// On success, receives the evaluation 31 | /// result. 32 | /// true on success; false otherwise. 33 | public override bool TryEvaluate(ExpressionEnvironment env, out string value) 34 | { 35 | if (env == null) throw new ArgumentNullException(nameof(env)); 36 | 37 | value = Value; 38 | return true; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Parser/ArgumentSetParserTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Parser; 5 | 6 | namespace NClap.Tests.Parser 7 | { 8 | [TestClass] 9 | public class ArgumentSetParserTests 10 | { 11 | [TestMethod] 12 | public void TestThatConstructorThrowsOnNullArgumentSet() 13 | { 14 | Action a = () => new ArgumentSetParser(null, new CommandLineParserOptions()); 15 | a.Should().Throw(); 16 | } 17 | 18 | [TestMethod] 19 | public void TestThatConstructorAllowNullOptions() 20 | { 21 | var argSet = new ArgumentSetDefinition(); 22 | Action a = () => new ArgumentSetParser(argSet, null); 23 | a.Should().NotThrow(); 24 | } 25 | 26 | [TestMethod] 27 | public void TestThatConstructorAllowsOptionsWithNullProperties() 28 | { 29 | var argSet = new ArgumentSetDefinition(); 30 | var options = new CommandLineParserOptions 31 | { 32 | Context = null, 33 | FileSystemReader = null, 34 | HelpOptions = null, 35 | Reporter = null 36 | }; 37 | 38 | Action a = () => new ArgumentSetParser(argSet, options); 39 | a.Should().NotThrow(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/NClap/Types/ArgumentCompletionContext.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Context for generating argument completions. 7 | /// 8 | public class ArgumentCompletionContext 9 | { 10 | /// 11 | /// The context that should be used if completion requires parsing. 12 | /// 13 | public ArgumentParseContext ParseContext { get; set; } 14 | 15 | /// 16 | /// The current tokenized state of the input. 17 | /// 18 | public IReadOnlyList Tokens { get; set; } 19 | 20 | /// 21 | /// The zero-based index of the token being completed. 22 | /// 23 | public int TokenIndex { get; set; } 24 | 25 | /// 26 | /// If this completion is being generated for command-line arguments 27 | /// being parsed, and if this object is non-null, then it is the 28 | /// object that results from parsing and processing the tokens *before* 29 | /// the one being completed. 30 | /// 31 | public object InProgressParsedObject { get; set; } 32 | 33 | /// 34 | /// True for completion to be case-sensitive; false for case-insensitive. 35 | /// 36 | public bool CaseSensitive { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Help/ArgumentSetHelpOptionsExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Help; 5 | 6 | namespace NClap.Tests.Help 7 | { 8 | [TestClass] 9 | public class ArgumentSetHelpOptionsExtensionsTests 10 | { 11 | [TestMethod] 12 | public void TestThatColumnWidthsThrowsWithTooManyWidths() 13 | { 14 | var options = new ArgumentSetHelpOptions() 15 | .With().TwoColumnLayout(); 16 | 17 | options.Invoking(o => o.ColumnWidths(Any.Int(), Any.Int(), Any.Int()).Apply()) 18 | .Should().Throw(); 19 | } 20 | 21 | [TestMethod] 22 | public void TestThatColumnWidthsThrowsWithWrongLayout() 23 | { 24 | var options = new ArgumentSetHelpOptions() 25 | .With().OneColumnLayout(); 26 | 27 | options.Invoking(o => o.ColumnWidths(10).Apply()) 28 | .Should().Throw(); 29 | } 30 | 31 | [TestMethod] 32 | public void TestThatColumnSeparatorThrowsWithWrongLayout() 33 | { 34 | var options = new ArgumentSetHelpOptions() 35 | .With().OneColumnLayout(); 36 | 37 | options.Invoking(o => o.ColumnSeparator(" ").Apply()) 38 | .Should().Throw(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NClap/Types/IArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics.CodeAnalysis; 4 | 5 | namespace NClap.Types 6 | { 7 | /// 8 | /// Interface for advertising a type as being parseable using this 9 | /// assembly. The implementation provides sufficient functionality 10 | /// for command-line parsing, generating usage help information, 11 | /// etc. 12 | /// 13 | public interface IArgumentType : IObjectFormatter, IStringParser, IStringCompleter 14 | { 15 | /// 16 | /// The type's human-readable (display) name. 17 | /// 18 | string DisplayName { get; } 19 | 20 | /// 21 | /// The Type object associated with values described by this interface. 22 | /// 23 | [SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods", Justification = "[Legacy]")] 24 | Type Type { get; } 25 | 26 | /// 27 | /// A summary of the concrete syntax required to indicate a value of 28 | /// the type described by this interface (e.g. ">Int32<"). 29 | /// 30 | string SyntaxSummary { get; } 31 | 32 | /// 33 | /// Enumeration of all types that this type depends on / includes. 34 | /// 35 | IEnumerable DependentTypes { get; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/NClap/Metadata/HelpCommandArgumentCompleter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NClap.Repl; 5 | using NClap.Types; 6 | 7 | namespace NClap.Metadata 8 | { 9 | /// 10 | /// Helper class used by . 11 | /// 12 | internal class HelpCommandArgumentCompleter : IStringCompleter 13 | { 14 | private readonly Loop _loop; 15 | 16 | /// 17 | /// Constructs a new completer for the given loop. 18 | /// 19 | /// Loop to generate completions for. 20 | public HelpCommandArgumentCompleter(Loop loop) 21 | { 22 | _loop = loop ?? throw new ArgumentNullException(nameof(loop)); 23 | } 24 | 25 | /// 26 | public IEnumerable GetCompletions(ArgumentCompletionContext context, string valueToComplete) 27 | { 28 | // We get the entire command line here, including the token that triggered 29 | // the help command to be invoked. Any options to the help command would 30 | // also be present, which would pose a problem in the future if we add 31 | // more options to the help command. 32 | const int tokensToSkip = 1; 33 | return _loop.GetCompletions(context.Tokens.Skip(tokensToSkip), context.TokenIndex - tokensToSkip); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/NClap/Metadata/NamedArgumentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Indicates that this argument is a named argument. Attach this attribute 7 | /// to instance fields (or properties) of types used as the destination 8 | /// of command-line argument parsing. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 11 | public sealed class NamedArgumentAttribute : ArgumentBaseAttribute 12 | { 13 | /// 14 | /// Default constructor, which may be used to indicate an optional 15 | /// named argument that may appear at most once. 16 | /// 17 | public NamedArgumentAttribute() : this(ArgumentFlags.Optional) 18 | { 19 | } 20 | 21 | /// 22 | /// Constructor that requires specifying flags. 23 | /// 24 | /// Specifies the error checking to be done on the 25 | /// argument. 26 | public NamedArgumentAttribute(ArgumentFlags flags) : base(flags) 27 | { 28 | } 29 | 30 | /// 31 | /// The short name of the argument. Set to null means use the default 32 | /// short name if it does not conflict with any other parameter name. 33 | /// Set to string.Empty for no short name. 34 | /// 35 | public string ShortName { get; set; } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Tests/TestApp/ReplCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.ConsoleInput; 3 | using NClap.Metadata; 4 | using NClap.Repl; 5 | using NClap.Utilities; 6 | 7 | namespace NClap.TestApp 8 | { 9 | [ArgumentSet(Style = ArgumentSetStyle.PowerShell)] 10 | class ReplCommand : SynchronousCommand 11 | { 12 | [NamedArgument(ArgumentFlags.Optional)] 13 | public LogLevel ReplLevel { get; set; } 14 | 15 | private static int _count = 0; 16 | 17 | public override CommandResult Execute() 18 | { 19 | Console.WriteLine("Entering loop."); 20 | 21 | var keyBindingSet = ConsoleKeyBindingSet.CreateDefaultSet(); 22 | keyBindingSet.Bind('c', ConsoleModifiers.Control, ConsoleInputOperation.Abort); 23 | 24 | ++_count; 25 | 26 | var parameters = new LoopInputOutputParameters 27 | { 28 | Prompt = new ColoredString($"Loop{new string('>', _count)} ", ConsoleColor.Cyan), 29 | KeyBindingSet = keyBindingSet, 30 | EndOfLineCommentCharacter = '#' 31 | }; 32 | 33 | var attrib = new ArgumentSetAttribute 34 | { 35 | Style = ArgumentSetStyle.GetOpt 36 | }; 37 | 38 | new Loop(typeof(MainCommandType), parameters, attrib).Execute(); 39 | 40 | --_count; 41 | 42 | Console.WriteLine("Exited loop."); 43 | 44 | return CommandResult.Success; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Types; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Abstract base class for implementing argument validation attributes. 8 | /// 9 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 10 | public abstract class ArgumentValidationAttribute : Attribute 11 | { 12 | /// 13 | /// Checks if this validation attributes accepts values of the specified 14 | /// type. 15 | /// 16 | /// Type to check. 17 | /// True if this attribute accepts values of the specified 18 | /// type; false if not. 19 | public abstract bool AcceptsType(IArgumentType type); 20 | 21 | /// 22 | /// Validate the provided value in accordance with the attribute's 23 | /// policy. 24 | /// 25 | /// Context for validation. 26 | /// The value to validate. 27 | /// On failure, receives a user-readable string 28 | /// message explaining why the value is not valid. 29 | /// True if the value passes validation; false otherwise. 30 | /// 31 | public abstract bool TryValidate(ArgumentValidationContext context, object value, out string reason); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Expressions/ExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Expressions; 5 | 6 | namespace NClap.Tests.Expressions 7 | { 8 | public abstract class ExpressionTests 9 | { 10 | private const string AnyString = "test"; 11 | 12 | internal readonly TestEnvironment EmptyEnvironment = 13 | new TestEnvironment(); 14 | 15 | internal static readonly Expression AnyExpression = new StringLiteral(AnyString); 16 | internal static readonly string EvaluatedAnyExpression = AnyString; 17 | 18 | internal static readonly Expression AnyUnevaluatableExpr = new UnevaluatableExpression(); 19 | internal static readonly Expression AnyEvaluatableExpr = new StringLiteral("foo"); 20 | 21 | internal abstract Expression CreateInstance(); 22 | 23 | class UnevaluatableExpression : Expression 24 | { 25 | public override bool TryEvaluate(NClap.Expressions.ExpressionEnvironment env, out string value) 26 | { 27 | value = null; 28 | return false; 29 | } 30 | } 31 | 32 | [TestMethod] 33 | public void TestThatEvaluationThrowsOnNullEnvironment() 34 | { 35 | var expr = CreateInstance(); 36 | 37 | Action a = () => expr.TryEvaluate(null, out string value); 38 | a.Should().Throw(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NClap/Utilities/Windows/NativeMethods.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | 4 | namespace NClap.Utilities.Windows 5 | { 6 | #pragma warning disable PC003 // TODO: Native API not available in UWP 7 | 8 | /// 9 | /// Wrapper for native methods only available on traditional Windows platforms. 10 | /// 11 | internal static class NativeMethods 12 | { 13 | /// 14 | /// Extracts a Unicode character from a key press. 15 | /// 16 | /// Virtual key. 17 | /// Key scan code. 18 | /// Current modifiers key state. 19 | /// On success receives extracted characters. 20 | /// Maximum number of characters to retrieve. 21 | /// Flags. 22 | /// On success, returns number of characters extracted; otherwise zero 23 | /// or a negative number. 24 | [DllImport("user32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Unicode, ThrowOnUnmappableChar = true)] 25 | public static extern int ToUnicode( 26 | uint wVirtKey, 27 | uint wScanCode, 28 | byte[] lpKeyState, 29 | [MarshalAs(UnmanagedType.LPArray)] [Out] char[] pwszBuff, 30 | int cchBuff, 31 | uint wFlags); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Expressions/TestEnvironment.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap.Expressions 4 | { 5 | /// 6 | /// An expression environment. 7 | /// 8 | internal class TestEnvironment : NClap.Expressions.ExpressionEnvironment 9 | { 10 | private Dictionary _variables = new Dictionary(); 11 | 12 | /// 13 | /// Associates the given variable with the given value. If the 14 | /// variable is already defined, then the existing association 15 | /// will be replaced with this new one. 16 | /// 17 | /// Name of the variable. 18 | /// Value to associate with the variable. 19 | public void Define(string variableName, string value) 20 | { 21 | _variables[variableName] = value; 22 | } 23 | 24 | /// 25 | /// Tries to retrieve the value associated with the given 26 | /// variable. 27 | /// 28 | /// Name of the variable. 29 | /// On success, receives the value associated 30 | /// with the variable. 31 | /// true if the variable was found; false otherwise. 32 | public override bool TryGetVariable(string variableName, out string value) => 33 | _variables.TryGetValue(variableName, out value); 34 | } 35 | } -------------------------------------------------------------------------------- /src/Tests/UnitTests/Expressions/StringLiteralTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Expressions; 5 | 6 | namespace NClap.Tests.Expressions 7 | { 8 | [TestClass] 9 | public class StringLiteralTests : ExpressionTests 10 | { 11 | [TestMethod] 12 | public void TestThatConstructorThrowsOnNull() 13 | { 14 | Action a = () => new StringLiteral(null); 15 | a.Should().Throw(); 16 | } 17 | 18 | [TestMethod] 19 | public void TestThatLiteralIsCorrect() 20 | { 21 | const string anyString = "foo"; 22 | var literal = new StringLiteral(anyString); 23 | literal.Value.Should().Be(anyString); 24 | } 25 | 26 | [TestMethod] 27 | public void TestThatEmptyStringIsValidLiteral() 28 | { 29 | var literal = new StringLiteral(string.Empty); 30 | literal.Value.Should().Be(string.Empty); 31 | } 32 | 33 | [TestMethod] 34 | public void TestThatLiteralEvaluatesToInnerString() 35 | { 36 | const string anyString = "foo"; 37 | var literal = new StringLiteral(anyString); 38 | literal.TryEvaluate(EmptyEnvironment, out string value).Should().BeTrue(); 39 | value.Should().Be(anyString); 40 | } 41 | 42 | internal override Expression CreateInstance() => 43 | new StringLiteral("foo"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentSyntaxHelpOptions.cs: -------------------------------------------------------------------------------- 1 | using NClap.Utilities; 2 | 3 | namespace NClap.Help 4 | { 5 | /// 6 | /// Help options for argument syntax summaries. 7 | /// 8 | public class ArgumentSyntaxHelpOptions : ArgumentMetadataHelpOptions 9 | { 10 | /// 11 | /// Default constructor. 12 | /// 13 | public ArgumentSyntaxHelpOptions() 14 | { 15 | } 16 | 17 | /// 18 | /// Deeply cloning constructor. 19 | /// 20 | /// Template for clone. 21 | private ArgumentSyntaxHelpOptions(ArgumentSyntaxHelpOptions other) : base(other) 22 | { 23 | CommandNameColor = other.CommandNameColor; 24 | IncludeOptionalArguments = other.IncludeOptionalArguments; 25 | } 26 | 27 | /// 28 | /// Color of the command name. 29 | /// 30 | public TextColor CommandNameColor { get; set; } 31 | 32 | /// 33 | /// True to include optional arguments in syntax summary; false to 34 | /// exclude them. 35 | /// 36 | internal bool IncludeOptionalArguments { get; set; } = true; 37 | 38 | /// 39 | /// Create a separate clone of this object. 40 | /// 41 | /// Clone. 42 | public override ArgumentMetadataHelpOptions DeepClone() => new ArgumentSyntaxHelpOptions(this); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/NClap/Types/ICollectionArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System.Collections; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Interface for advertising a collection type as being parseable 7 | /// using this assembly. The implementation provides sufficient 8 | /// functionality for command-line parsing, generating usage help 9 | /// information, etc. This interface should only be implemented 10 | /// by objects that describe .NET collection objects. 11 | /// 12 | public interface ICollectionArgumentType : IArgumentType 13 | { 14 | /// 15 | /// Argument type of elements in the collection described by this 16 | /// object. 17 | /// 18 | IArgumentType ElementType { get; } 19 | 20 | /// 21 | /// Constructs a collection of the type described by this object, 22 | /// populated with objects from the provided input collection. 23 | /// 24 | /// Objects to add to the collection. 25 | /// Constructed collection. 26 | object ToCollection(IEnumerable objects); 27 | 28 | /// 29 | /// Enumerates the items in the collection. The input collection 30 | /// should be of the type described by this object. 31 | /// 32 | /// Collection to enumerate. 33 | /// The enumeration. 34 | IEnumerable ToEnumerable(object collection); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/NClap/Metadata/CommandAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | // CA1813: Avoid unsealed attributes 6 | #pragma warning disable CA1813 7 | 8 | // CA1019: Define accessors for attribute arguments 9 | #pragma warning disable CA1019 10 | 11 | /// 12 | /// Attribute class used to denote commands. 13 | /// 14 | [AttributeUsage(AttributeTargets.Field)] 15 | public class CommandAttribute : ArgumentValueAttribute 16 | { 17 | private readonly Type _implementingType; 18 | 19 | /// 20 | /// Default, parameterless constructor. 21 | /// 22 | protected CommandAttribute() 23 | { 24 | } 25 | 26 | /// 27 | /// Constructor that allows specifying the type that "implements" 28 | /// this command. 29 | /// 30 | /// The implementing type. 31 | public CommandAttribute(Type implementingType) 32 | { 33 | _implementingType = implementingType; 34 | } 35 | 36 | /// 37 | /// Gets the type that "implements" this command. 38 | /// 39 | /// The type of the command associated with this 40 | /// attribute. 41 | /// The type. 42 | public virtual Type GetImplementingType(Type commandType) => _implementingType; 43 | } 44 | 45 | #pragma warning restore CA1019 46 | #pragma warning restore CA1813 47 | } 48 | -------------------------------------------------------------------------------- /src/NClap/Metadata/PositionalArgumentAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Metadata 4 | { 5 | /// 6 | /// Indicates that this argument is an (unnamed) positional argument. The 7 | /// LongName property is used for usage text only and does not affect the 8 | /// usage of the argument. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] 11 | public sealed class PositionalArgumentAttribute : ArgumentBaseAttribute 12 | { 13 | /// 14 | /// Default constructor, which may be used to indicate an optional 15 | /// positional argument that may appear at most once. 16 | /// 17 | public PositionalArgumentAttribute() : this(ArgumentFlags.Optional) 18 | { 19 | } 20 | 21 | /// 22 | /// Indicates that this argument is a default, positional argument. 23 | /// 24 | /// Specifies the error checking to be done on the 25 | /// argument. 26 | public PositionalArgumentAttribute(ArgumentFlags flags) : base(flags) 27 | { 28 | } 29 | 30 | /// 31 | /// The zero-based index of this argument amongst all (positional) 32 | /// default arguments. Each default argument present within an 33 | /// object must have a unique position value, and they must be 34 | /// consecutive, with the smallest being zero. 35 | /// 36 | public int Position { get; set; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/ConsoleInput/SimulatedConsoleInput.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using NClap.ConsoleInput; 6 | 7 | namespace NClap.Tests.ConsoleInput 8 | { 9 | class SimulatedConsoleInput : IConsoleInput 10 | { 11 | private int _keyIndex; 12 | 13 | public SimulatedConsoleInput(IEnumerable keyStream = null) 14 | { 15 | KeyStream = keyStream?.ToList() ?? new List(); 16 | } 17 | 18 | /// 19 | /// True if Control-C is treated as a normal input character; false if 20 | /// it's specially handled. 21 | /// 22 | public bool TreatControlCAsInput { get; set; } 23 | 24 | /// 25 | /// The key stream surfaced from this console. 26 | /// 27 | public IReadOnlyList KeyStream { get; } 28 | 29 | /// 30 | /// Reads a key press from the console. 31 | /// 32 | /// True to suppress auto-echoing the key's 33 | /// character; false to echo it as normal. 34 | /// Info about the press. 35 | public ConsoleKeyInfo ReadKey(bool suppressEcho) 36 | { 37 | if (_keyIndex >= KeyStream.Count) 38 | { 39 | throw new InvalidOperationException("There are no more key events to read."); 40 | } 41 | 42 | return KeyStream[_keyIndex++]; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/NClap/Repl/LoopInputOutputParameters.cs: -------------------------------------------------------------------------------- 1 | using NClap.ConsoleInput; 2 | using NClap.Utilities; 3 | 4 | namespace NClap.Repl 5 | { 6 | /// 7 | /// Parameters for constructing a loop with advanced line input. The 8 | /// parameters indicate how the loop's textual input and output should 9 | /// be implemented. 10 | /// 11 | public class LoopInputOutputParameters 12 | { 13 | /// 14 | /// Line input object to use, or null for a default one to be 15 | /// constructed. 16 | /// 17 | public IConsoleLineInput LineInput { get; set; } 18 | 19 | /// 20 | /// The console input interface to use, or null to use the default one. 21 | /// 22 | public IConsoleInput ConsoleInput { get; set; } 23 | 24 | /// 25 | /// The console output interface to use, or null to use the default one. 26 | /// 27 | public IConsoleOutput ConsoleOutput { get; set; } 28 | 29 | /// 30 | /// The console key binding set to use, or null to use the default one. 31 | /// 32 | public IReadOnlyConsoleKeyBindingSet KeyBindingSet { get; set; } 33 | 34 | /// 35 | /// Input prompt, or null to use the default one. 36 | /// 37 | public ColoredString? Prompt { get; set; } 38 | 39 | /// 40 | /// The character that starts a comment. 41 | /// 42 | public char? EndOfLineCommentCharacter { get; set; } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Expressions/ParenthesisExpressionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Expressions; 5 | 6 | namespace NClap.Tests.Expressions 7 | { 8 | [TestClass] 9 | public class ParenthesisExpressionTests : ExpressionTests 10 | { 11 | internal override Expression CreateInstance() => 12 | new ParenthesisExpression(AnyExpression); 13 | 14 | [TestMethod] 15 | public void TestThatConstructorThrowsOnNullInnerExpression() 16 | { 17 | Action a = () => new ParenthesisExpression(null); 18 | a.Should().Throw(); 19 | } 20 | 21 | [TestMethod] 22 | public void TestThatExpressionWrapsCorrectInnerExpression() 23 | { 24 | var expr = new ParenthesisExpression(AnyExpression); 25 | expr.InnerExpression.Should().BeSameAs(AnyExpression); 26 | } 27 | 28 | [TestMethod] 29 | public void TestThatExpressionEvaluatesToEvaluatedInnerExpression() 30 | { 31 | var expr = new ParenthesisExpression(AnyExpression); 32 | expr.TryEvaluate(EmptyEnvironment, out string value).Should().BeTrue(); 33 | value.Should().Be(EvaluatedAnyExpression); 34 | } 35 | 36 | [TestMethod] 37 | public void TestThatEvalFailsWhenInnerExprCannotBeEvaluated() 38 | { 39 | var expr = new ParenthesisExpression(AnyUnevaluatableExpr); 40 | expr.TryEvaluate(EmptyEnvironment, out string value).Should().BeFalse(); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Types/KeyValuePairArgumentTypeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Types; 6 | 7 | namespace NClap.Tests.Types 8 | { 9 | [TestClass] 10 | public class KeyValuePairArgumentTypeTests 11 | { 12 | [TestMethod] 13 | public void InvalidUseOfGetCompletions() 14 | { 15 | var type = (KeyValuePairArgumentType)ArgumentType.GetType(typeof(KeyValuePair)); 16 | var c = new ArgumentCompletionContext { ParseContext = ArgumentParseContext.Default }; 17 | 18 | type.Invoking(t => t.GetCompletions(null, "Tr")) 19 | .Should().Throw(); 20 | 21 | type.Invoking(t => t.GetCompletions(c, null)) 22 | .Should().Throw(); 23 | } 24 | 25 | [TestMethod] 26 | public void GetCompletions() 27 | { 28 | var type = (KeyValuePairArgumentType)ArgumentType.GetType(typeof(KeyValuePair)); 29 | var c = new ArgumentCompletionContext { ParseContext = ArgumentParseContext.Default }; 30 | 31 | type.GetCompletions(c, "Tr").Should().Equal("True"); 32 | type.GetCompletions(c, string.Empty).Should().Equal("False", "True"); 33 | type.GetCompletions(c, "False=f").Should().Equal("False=False"); 34 | type.GetCompletions(c, "33=f").Should().Equal("33=False"); 35 | type.GetCompletions(c, "True=").Should().Equal("True=False", "True=True"); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/NClap/Types/StringArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Types 4 | { 5 | /// 6 | /// Implementation to describe System.Stringean. 7 | /// 8 | internal class StringArgumentType : ArgumentTypeBase 9 | { 10 | private static readonly StringArgumentType Instance = new StringArgumentType(); 11 | 12 | /// 13 | /// Primary constructor. 14 | /// 15 | private StringArgumentType() : base(typeof(string)) 16 | { 17 | } 18 | 19 | /// 20 | /// Display name. 21 | /// 22 | public override string DisplayName => "Str"; 23 | 24 | /// 25 | /// Public factory method. 26 | /// 27 | /// A constructed object. 28 | public static StringArgumentType Create() => Instance; 29 | 30 | /// 31 | /// Parses the provided string. Throws an exception if the string 32 | /// cannot be parsed. 33 | /// 34 | /// Context for parsing. 35 | /// String to parse. 36 | /// The parsed object. 37 | protected override object Parse(ArgumentParseContext context, string stringToParse) 38 | { 39 | if (string.IsNullOrEmpty(stringToParse) && !context.AllowEmpty) 40 | { 41 | throw new ArgumentOutOfRangeException(nameof(stringToParse)); 42 | } 43 | 44 | return stringToParse; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Tools/Inspector/CompleteTokensCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using NClap.Metadata; 5 | 6 | namespace NClap.Inspector 7 | { 8 | class CompleteTokensCommand : SynchronousCommand 9 | { 10 | private readonly ProgramArguments _programArgs; 11 | 12 | public CompleteTokensCommand(ProgramArguments programArgs) 13 | { 14 | _programArgs = programArgs; 15 | } 16 | 17 | [NamedArgument(ArgumentFlags.Required, LongName = "TokenIndex", 18 | Description = "0-based index of token to complete")] 19 | public int IndexOfTokenToComplete { get; set; } 20 | 21 | [NamedArgument(ArgumentFlags.RestOfLine, LongName = "Tokens", 22 | Description = "Command-line tokens")] 23 | public List Tokens { get; set; } = new List(); 24 | 25 | public override CommandResult Execute() 26 | { 27 | if (_programArgs.Verbose) 28 | { 29 | Console.WriteLine($"Completing token {IndexOfTokenToComplete} of args: [{string.Join(" ", Tokens.Select(a => "\"" + a + "\""))}]"); 30 | } 31 | 32 | // Swallow bogus requests. 33 | if (IndexOfTokenToComplete > Tokens.Count) 34 | { 35 | return CommandResult.Success; 36 | } 37 | 38 | foreach (var completion in CommandLineParser.GetCompletions(_programArgs.LoadedType, Tokens, IndexOfTokenToComplete)) 39 | { 40 | Console.WriteLine(completion); 41 | } 42 | 43 | return CommandResult.Success; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/NClap/Repl/ILoopClient.cs: -------------------------------------------------------------------------------- 1 | using NClap.ConsoleInput; 2 | using NClap.Utilities; 3 | 4 | namespace NClap.Repl 5 | { 6 | /// 7 | /// Interface provided by REPL view. 8 | /// 9 | public interface ILoopClient 10 | { 11 | /// 12 | /// The loop prompt. If you wish to use a as your 13 | /// prompt, you should use the property instead. 14 | /// 15 | string Prompt { get; set; } 16 | 17 | /// 18 | /// The loop prompt (with color). 19 | /// 20 | ColoredString? PromptWithColor { get; set; } 21 | 22 | /// 23 | /// The character that starts a comment. 24 | /// 25 | char? EndOfLineCommentCharacter { get; } 26 | 27 | /// 28 | /// Optionally provides a token completer that the loop client may choose to use. 29 | /// 30 | ITokenCompleter TokenCompleter { get; set; } 31 | 32 | /// 33 | /// Displays the loop prompt. 34 | /// 35 | void DisplayPrompt(); 36 | 37 | /// 38 | /// Reads a line of text input. 39 | /// 40 | /// The read line. 41 | string ReadLine(); 42 | 43 | /// 44 | /// Notifies the client of a continuable error. 45 | /// 46 | /// The message if one is available, or null if 47 | /// there is no more input. 48 | void OnError(string message); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # .NET Command Line Argument Parser (NClap) 2 | 3 | [![Build](https://github.com/reubeno/NClap/actions/workflows/build.yaml/badge.svg)](https://github.com/reubeno/NClap/actions/workflows/build.yaml) 4 | [![NuGet package](https://img.shields.io/nuget/vpre/NClap.svg)](https://www.nuget.org/packages/NClap) 5 | [![GitHub license](https://img.shields.io/github/license/reubeno/NClap.svg)](https://reubeno.github.io/NClap/LICENSE.txt) 6 | [![Join the chat at https://gitter.im/NClap/Lobby](https://badges.gitter.im/NClap/Lobby.svg)](https://gitter.im/NClap/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 7 | 8 | NClap is a .NET library for parsing command-line arguments and building interactive command shells. It's driven by a declarative attribute syntax, and easy to extend. It's like a lightweight serializer for command-line arguments. 9 | 10 | ## Getting NClap 11 | 12 | The easiest way to consume NClap is by adding its [NuGet package](https://www.nuget.org/packages/NClap) to your project. It is built for use with .NET 4.6.1+ and .NET Core 2.0+. 13 | 14 | NClap is shared under the MIT license, as described in [LICENSE.txt](https://reubeno.github.io/NClap/LICENSE.txt). 15 | 16 | ## The details 17 | 18 | - [Basic usage](docs/Usage.md) 19 | - [Mostly complete feature list](docs/Features.md) 20 | - [Commands (a.k.a. verbs)](docs/Commands.md) 21 | 22 | ## Contributing 23 | 24 | We welcome contributions of all kinds, whether it be submitting pull requests or even just filing issues or feature requests! 25 | 26 | For code contributions, we follow the standard [Github workflow](https://guides.github.com/introduction/flow/). 27 | 28 | Please also see our [Code of Conduct](CODE_OF_CONDUCT.md) 29 | -------------------------------------------------------------------------------- /src/NClap/Repl/LoopOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Help; 3 | using NClap.Utilities; 4 | 5 | namespace NClap.Repl 6 | { 7 | /// 8 | /// Options for executing loops. 9 | /// 10 | public class LoopOptions : IDeepCloneable 11 | { 12 | /// 13 | /// Default constructor. 14 | /// 15 | public LoopOptions() 16 | { 17 | } 18 | 19 | /// 20 | /// Deeply cloning constructor. 21 | /// 22 | /// Template for clone. 23 | private LoopOptions(LoopOptions other) 24 | { 25 | ParserOptions = other.ParserOptions?.DeepClone(); 26 | HelpOutputHandler = other.HelpOutputHandler; 27 | } 28 | 29 | /// 30 | /// Parser options; initialized with defaults. 31 | /// 32 | public CommandLineParserOptions ParserOptions { get; set; } = new CommandLineParserOptions 33 | { 34 | HelpOptions = new ArgumentSetHelpOptions 35 | { 36 | Logo = new ArgumentMetadataHelpOptions { Include = false }, 37 | Name = string.Empty 38 | } 39 | }; 40 | 41 | /// 42 | /// The output handler for help/usage information. 43 | /// 44 | public Action HelpOutputHandler { get; set; } 45 | 46 | /// 47 | /// Creates a separate clone of this object. 48 | /// 49 | /// Clone. 50 | public LoopOptions DeepClone() => new LoopOptions(this); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/NClap/Utilities/GenericCollectionFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | 5 | namespace NClap.Utilities 6 | { 7 | /// 8 | /// Factory for creating typed collections when types are not 9 | /// known at compile time. 10 | /// 11 | internal static class GenericCollectionFactory 12 | { 13 | /// 14 | /// Creates an instance of that can 15 | /// hold instance of the given type . 16 | /// 17 | /// Type of instance. 18 | /// Constructed list. 19 | public static IList CreateList(Type t) 20 | { 21 | var listType = typeof(List<>).MakeGenericType(new[] { t }); 22 | return (IList)Activator.CreateInstance(listType); 23 | } 24 | 25 | /// 26 | /// Creates an array that may hold values of type 27 | /// with the values in given enumeration . 28 | /// 29 | /// Values to store. 30 | /// Type for the array's elements. 31 | /// The constructed and initialized array. 32 | public static Array ToArray(this IEnumerable values, Type t) 33 | { 34 | var list = CreateList(t); 35 | foreach (var value in values) 36 | { 37 | list.Add(value); 38 | } 39 | 40 | var array = Array.CreateInstance(t, list.Count); 41 | list.CopyTo(array, 0); 42 | 43 | return array; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentSyntaxFlags.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Help 4 | { 5 | /// 6 | /// Flags describing the format of argument syntax summaries. 7 | /// 8 | [Flags] 9 | internal enum ArgumentSyntaxFlags 10 | { 11 | /// 12 | /// No flags. 13 | /// 14 | None = 0x0, 15 | 16 | /// 17 | /// Visibly distinguish optional arguments. 18 | /// 19 | DistinguishOptionalArguments = 0x1, 20 | 21 | /// 22 | /// Visibly indicate how many times the argument may occur. 23 | /// 24 | IndicateCardinality = 0x2, 25 | 26 | /// 27 | /// Whether or not to indicate the type of positional arguments. 28 | /// 29 | IndicatePositionalArgumentType = 0x4, 30 | 31 | /// 32 | /// Whether or not to indicate if the argument accepts an empty string. 33 | /// 34 | IndicateArgumentsThatAcceptEmptyString = 0x8, 35 | 36 | /// 37 | /// Whether or not to include the syntax of specifying the value associated with 38 | /// the argument. 39 | /// 40 | IncludeValueSyntax = 0x10, 41 | 42 | /// 43 | /// Defaults. 44 | /// 45 | Default = 46 | DistinguishOptionalArguments | 47 | IndicateCardinality | 48 | IndicateArgumentsThatAcceptEmptyString | 49 | IncludeValueSyntax, 50 | 51 | /// 52 | /// All flags. 53 | /// 54 | All = Default | IndicatePositionalArgumentType, 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tools/Inspector/CompleteLineCommand.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Metadata; 3 | 4 | namespace NClap.Inspector 5 | { 6 | class CompleteLineCommand : SynchronousCommand 7 | { 8 | private readonly ProgramArguments _programArgs; 9 | 10 | public CompleteLineCommand(ProgramArguments programArgs) 11 | { 12 | _programArgs = programArgs; 13 | } 14 | 15 | [NamedArgument(ArgumentFlags.Optional, LongName = "Skip", 16 | Description = "Tokens to skip", 17 | DefaultValue = 1)] 18 | public int TokensToSkip { get; set; } 19 | 20 | [NamedArgument(ArgumentFlags.Required, LongName = "Cursor", 21 | Description = "0-based index of cursor")] 22 | public int CursorIndex { get; set; } 23 | 24 | [NamedArgument(ArgumentFlags.Required, LongName = "CommandLine", 25 | Description = "Command line")] 26 | public string CommandLine { get; set; } 27 | 28 | public override CommandResult Execute() 29 | { 30 | var verboseMessage = $"{Guid.NewGuid()}: Completing with skip={TokensToSkip} cursor={CursorIndex} of command line: [{CommandLine}]"; 31 | if (_programArgs.Verbose) 32 | { 33 | Console.WriteLine(verboseMessage); 34 | } 35 | 36 | foreach (var completion in CommandLineParser.GetCompletions( 37 | _programArgs.LoadedType, 38 | CommandLine, 39 | CursorIndex, 40 | tokensToSkip: this.TokensToSkip, 41 | options: null)) 42 | { 43 | Console.WriteLine(completion); 44 | } 45 | 46 | return CommandResult.Success; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustBeLessThanAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Attribute that indicates the associated integer argument member must 8 | /// be less than a given value. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 11 | public sealed class MustBeLessThanAttribute : IntegerComparisonValidationAttribute 12 | { 13 | /// 14 | /// Primary constructor. 15 | /// 16 | /// Value to compare against. 17 | public MustBeLessThanAttribute(object target) : base(target) 18 | { 19 | } 20 | 21 | /// 22 | /// Validate the provided value in accordance with the attribute's 23 | /// policy. 24 | /// 25 | /// Context for validation. 26 | /// The value to validate. 27 | /// On failure, receives a user-readable string 28 | /// message explaining why the value is not valid. 29 | /// True if the value passes validation; false otherwise. 30 | /// 31 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 32 | { 33 | if (GetArgumentType(value).IsLessThan(value, Target)) 34 | { 35 | reason = null; 36 | return true; 37 | } 38 | 39 | reason = string.Format(CultureInfo.CurrentCulture, Strings.ValueIsNotLessThan, Target); 40 | return false; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/FluentBuilderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Utilities; 4 | 5 | namespace NClap.Tests.Utilities 6 | { 7 | [TestClass] 8 | public class FluentBuilderTests 9 | { 10 | private class Boxed 11 | { 12 | public T Value { get; set; } 13 | } 14 | 15 | [TestMethod] 16 | public void TestBuilderWithNoTransformationsAppliesToStartingState() 17 | { 18 | var startingValue = Any.Int(); 19 | var builder = new FluentBuilder(startingValue); 20 | builder.Apply().Should().Be(startingValue); 21 | } 22 | 23 | [TestMethod] 24 | public void TestBuilderImplicitlyCoercesFromStartingState() 25 | { 26 | var startingValue = Any.Int(); 27 | FluentBuilder builder = startingValue; 28 | builder.Apply().Should().Be(startingValue); 29 | } 30 | 31 | [TestMethod] 32 | public void TestBuilderImplicitlyCoercesToAppliedResult() 33 | { 34 | var startingValue = Any.Int(); 35 | var builder = new FluentBuilder(startingValue); 36 | int coerced = builder; 37 | builder.Apply().Should().Be(coerced); 38 | } 39 | 40 | [TestMethod] 41 | public void TestBuilderAppliesTransformationsInCorrectOrder() 42 | { 43 | var startingValue = 20; 44 | FluentBuilder> builder = new Boxed { Value = startingValue }; 45 | builder.AddTransformer(v => v.Value /= 10); 46 | builder.AddTransformer(v => v.Value -= 2); 47 | builder.Apply().Value.Should().Be(0); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustBeGreaterThanAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Attribute that indicates the associated integer argument member must 8 | /// be greater than a given value. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 11 | public sealed class MustBeGreaterThanAttribute : IntegerComparisonValidationAttribute 12 | { 13 | /// 14 | /// Primary constructor. 15 | /// 16 | /// Value to compare against. 17 | public MustBeGreaterThanAttribute(object target) : base(target) 18 | { 19 | } 20 | 21 | /// 22 | /// Validate the provided value in accordance with the attribute's 23 | /// policy. 24 | /// 25 | /// Context for validation. 26 | /// The value to validate. 27 | /// On failure, receives a user-readable string 28 | /// message explaining why the value is not valid. 29 | /// True if the value passes validation; false otherwise. 30 | /// 31 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 32 | { 33 | if (GetArgumentType(value).IsGreaterThan(value, Target)) 34 | { 35 | reason = null; 36 | return true; 37 | } 38 | 39 | reason = string.Format(CultureInfo.CurrentCulture, Strings.ValueIsNotGreaterThan, Target); 40 | return false; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/NClap/Utilities/IMutableMemberInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | namespace NClap.Utilities 5 | { 6 | /// 7 | /// Abstract interface for interacting with mutable members of types 8 | /// (e.g. fields and properties). 9 | /// 10 | public interface IMutableMemberInfo 11 | { 12 | /// 13 | /// Retrieve the member's base member info. 14 | /// 15 | MemberInfo MemberInfo { get; } 16 | 17 | /// 18 | /// True if the member can be read at arbitrary points during execution; 19 | /// false otherwise. 20 | /// 21 | bool IsReadable { get; } 22 | 23 | /// 24 | /// True if the member can be written to at arbitrary points during 25 | /// execution; false otherwise. 26 | /// 27 | bool IsWritable { get; } 28 | 29 | /// 30 | /// The type of the member. 31 | /// 32 | Type MemberType { get; } 33 | 34 | /// 35 | /// Retrieve the value associated with this field in the specified 36 | /// containing object. 37 | /// 38 | /// Object to look in. 39 | /// The field's value. 40 | object GetValue(object containingObject); 41 | 42 | /// 43 | /// Sets the value associated with this field in the specified 44 | /// containing object. 45 | /// 46 | /// Object to look in. 47 | /// Value to set. 48 | void SetValue(object containingObject, object value); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustBeLessThanOrEqualToAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Attribute that indicates the associated integer argument member must 8 | /// be less than or equal to a given value. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 11 | public sealed class MustBeLessThanOrEqualToAttribute : IntegerComparisonValidationAttribute 12 | { 13 | /// 14 | /// Primary constructor. 15 | /// 16 | /// Value to compare against. 17 | public MustBeLessThanOrEqualToAttribute(object target) : base(target) 18 | { 19 | } 20 | 21 | /// 22 | /// Validate the provided value in accordance with the attribute's 23 | /// policy. 24 | /// 25 | /// Context for validation. 26 | /// The value to validate. 27 | /// On failure, receives a user-readable string 28 | /// message explaining why the value is not valid. 29 | /// True if the value passes validation; false otherwise. 30 | /// 31 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 32 | { 33 | if (GetArgumentType(value).IsLessThanOrEqualTo(value, Target)) 34 | { 35 | reason = null; 36 | return true; 37 | } 38 | 39 | reason = string.Format(CultureInfo.CurrentCulture, Strings.ValueIsNotLessThanOrEqualTo, Target); 40 | return false; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Types/TupleArgumentTypeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Types; 6 | 7 | namespace NClap.Tests.Types 8 | { 9 | [TestClass] 10 | public class TupleArgumentTypeTests 11 | { 12 | [TestMethod] 13 | public void InvalidUseOfGetCompletions() 14 | { 15 | var type = (TupleArgumentType)ArgumentType.GetType(typeof(Tuple)); 16 | var c = new ArgumentCompletionContext { ParseContext = ArgumentParseContext.Default }; 17 | 18 | type.Invoking(t => t.GetCompletions(null, "Tr")).Should().Throw(); 19 | type.Invoking(t => t.GetCompletions(c, null)).Should().Throw(); 20 | } 21 | 22 | [TestMethod] 23 | public void GetCompletions() 24 | { 25 | var type = (TupleArgumentType)ArgumentType.GetType(typeof(Tuple)); 26 | var c = new ArgumentCompletionContext { ParseContext = ArgumentParseContext.Default }; 27 | 28 | type.GetCompletions(c, "Tr").Should().Equal("True"); 29 | type.GetCompletions(c, string.Empty).Should().Equal("False", "True"); 30 | type.GetCompletions(c, "False,3").Should().BeEmpty(); 31 | type.GetCompletions(c, "False,3,").Should().Equal("False,3,False", "False,3,True"); 32 | } 33 | 34 | [TestMethod] 35 | public void TestThatDependentTypesListIsCorrect() 36 | { 37 | var type = (TupleArgumentType)ArgumentType.GetType(typeof(Tuple)); 38 | type.DependentTypes.Should().BeEquivalentTo( 39 | new[] { typeof(bool), typeof(int) }.Select(ArgumentType.GetType)); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustNotExistAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NClap.Types; 4 | 5 | namespace NClap.Metadata 6 | { 7 | /// 8 | /// Attribute that indicates the associated file-system path argument 9 | /// member must name a directory that exists. 10 | /// 11 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 12 | public sealed class MustNotExistAttribute : FileSystemPathValidationAttribute 13 | { 14 | /// 15 | /// Validate the provided value in accordance with the attribute's 16 | /// policy. 17 | /// 18 | /// Context for validation. 19 | /// The value to validate. 20 | /// On failure, receives a user-readable string 21 | /// message explaining why the value is not valid. 22 | /// True if the value passes validation; false otherwise. 23 | /// 24 | /// Thrown when 25 | /// is null. 26 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 27 | { 28 | if (context == null) throw new ArgumentNullException(nameof(context)); 29 | 30 | var path = (FileSystemPath)value; 31 | if (!context.FileSystemReader.FileExists(path) && !context.FileSystemReader.DirectoryExists(path)) 32 | { 33 | reason = null; 34 | return true; 35 | } 36 | 37 | reason = string.Format(CultureInfo.CurrentCulture, Strings.PathExists); 38 | return false; 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustBeGreaterThanOrEqualToAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | 4 | namespace NClap.Metadata 5 | { 6 | /// 7 | /// Attribute that indicates the associated integer argument member must 8 | /// be greater than or equal to a given value. 9 | /// 10 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 11 | public sealed class MustBeGreaterThanOrEqualToAttribute : IntegerComparisonValidationAttribute 12 | { 13 | /// 14 | /// Primary constructor. 15 | /// 16 | /// Value to compare against. 17 | public MustBeGreaterThanOrEqualToAttribute(object target) : base(target) 18 | { 19 | } 20 | 21 | /// 22 | /// Validate the provided value in accordance with the attribute's 23 | /// policy. 24 | /// 25 | /// Context for validation. 26 | /// The value to validate. 27 | /// On failure, receives a user-readable string 28 | /// message explaining why the value is not valid. 29 | /// True if the value passes validation; false otherwise. 30 | /// 31 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 32 | { 33 | if (GetArgumentType(value).IsGreaterThanOrEqualTo(value, Target)) 34 | { 35 | reason = null; 36 | return true; 37 | } 38 | 39 | reason = string.Format(CultureInfo.CurrentCulture, Strings.ValueIsNotGreaterThanOrEqualTo, Target); 40 | return false; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/NClap/Types/IArgumentValue.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NClap.Types 5 | { 6 | /// 7 | /// Interface for advertising a value as being parseable using this 8 | /// assembly. 9 | /// 10 | public interface IArgumentValue 11 | { 12 | /// 13 | /// The value. 14 | /// 15 | object Value { get; } 16 | 17 | /// 18 | /// True if the value has been disallowed from parsing use; false 19 | /// otherwise. 20 | /// 21 | bool Disallowed { get; } 22 | 23 | /// 24 | /// True if the value has been marked to be hidden from help and usage 25 | /// information; false otherwise. 26 | /// 27 | bool Hidden { get; } 28 | 29 | /// 30 | /// Display name for this value. 31 | /// 32 | string DisplayName { get; } 33 | 34 | /// 35 | /// Long name of this value. 36 | /// 37 | string LongName { get; } 38 | 39 | /// 40 | /// Short name of this value, if it has one; null if it has none. 41 | /// 42 | string ShortName { get; } 43 | 44 | /// 45 | /// Description of this value, if it has one; null if it has none. 46 | /// 47 | string Description { get; } 48 | 49 | /// 50 | /// Get any attributes of the given type associated with the value. 51 | /// 52 | /// Type of attribute to look for. 53 | /// The attributes. 54 | IEnumerable GetAttributes() 55 | where T : Attribute; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/NClap/Utilities/Windows/InputUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Utilities.Windows 4 | { 5 | /// 6 | /// Windows-specific input utilities. 7 | /// 8 | internal static class InputUtilities 9 | { 10 | /// 11 | /// Converts the indicated key (with modifiers) to the generated 12 | /// characters, in accordance with the currently active keyboard 13 | /// layout. 14 | /// 15 | /// The key to translate. 16 | /// Key modifiers. 17 | /// The characters. 18 | public static char[] GetChars(ConsoleKey key, ConsoleModifiers modifiers) 19 | { 20 | var virtKey = (uint)key; 21 | var output = new char[32]; 22 | 23 | var result = NativeMethods.ToUnicode(virtKey, 0, GetKeyState(modifiers), output, output.Length, 0 /* flags */); 24 | if (result < 0) result = 0; 25 | if (result == 1 && output[0] == '\0') result = 0; 26 | 27 | var relevantOutput = new char[result]; 28 | Array.Copy(output, relevantOutput, result); 29 | 30 | return relevantOutput; 31 | } 32 | 33 | private static byte[] GetKeyState(ConsoleModifiers modifiers) 34 | { 35 | const byte keyDownFlag = 0x80; 36 | 37 | var keyState = new byte[256]; 38 | 39 | if (modifiers.HasFlag(ConsoleModifiers.Alt)) keyState[(int)ConsoleModifierKeys.Alt] |= keyDownFlag; 40 | if (modifiers.HasFlag(ConsoleModifiers.Control)) keyState[(int)ConsoleModifierKeys.Control] |= keyDownFlag; 41 | if (modifiers.HasFlag(ConsoleModifiers.Shift)) keyState[(int)ConsoleModifierKeys.Shift] |= keyDownFlag; 42 | 43 | return keyState; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/MutablePropertyInfoTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Utilities; 6 | 7 | namespace NClap.Tests.Utilities 8 | { 9 | [TestClass] 10 | public class MutablePropertyInfoTests 11 | { 12 | class TestObject 13 | { 14 | public T Value { get; set; } 15 | } 16 | 17 | class TestThrowingObject 18 | { 19 | public T Value 20 | { 21 | get => default; 22 | set => throw new ArgumentOutOfRangeException(nameof(value)); 23 | } 24 | } 25 | 26 | [TestMethod] 27 | public void ConversionFails() 28 | { 29 | var prop = new MutablePropertyInfo(typeof(TestObject).GetTypeInfo().GetProperty("Value")); 30 | var obj = new TestObject(); 31 | Action setter = () => prop.SetValue(obj, 3.0); 32 | setter.Should().Throw(); 33 | } 34 | 35 | [TestMethod] 36 | public void ConversionSucceeds() 37 | { 38 | var prop = new MutablePropertyInfo(typeof(TestObject).GetTypeInfo().GetProperty("Value")); 39 | var obj = new TestObject(); 40 | Action setter = () => prop.SetValue(obj, 3L); 41 | setter.Should().NotThrow(); 42 | } 43 | 44 | [TestMethod] 45 | public void ConversionSucceedsButSettingFails() 46 | { 47 | var prop = new MutablePropertyInfo(typeof(TestThrowingObject).GetTypeInfo().GetProperty("Value")); 48 | var obj = new TestThrowingObject(); 49 | Action setter = () => prop.SetValue(obj, 3L); 50 | setter.Should().Throw(); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/MaybeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Utilities; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | [TestClass] 9 | public class MaybeTests 10 | { 11 | private const string AnyString = "Any string"; 12 | 13 | [TestMethod] 14 | public void TestThatNoneValueAsMaybeYieldsNoValue() 15 | { 16 | Maybe value = new None(); 17 | value.HasValue.Should().BeFalse(); 18 | value.IsNone.Should().BeTrue(); 19 | value.Invoking(v => { var _ = v.Value; }).Should().Throw(); 20 | value.GetValueOrDefault(AnyString).Should().Be(AnyString); 21 | value.GetValueOrDefault().Should().BeNull(); 22 | } 23 | 24 | [TestMethod] 25 | public void TestThatSomeNullValueAsMaybeYieldsNullValue() 26 | { 27 | var value = new Maybe(null); 28 | value.HasValue.Should().BeTrue(); 29 | value.IsNone.Should().BeFalse(); 30 | value.Value.Should().BeNull(); 31 | value.GetValueOrDefault(AnyString).Should().BeNull(); 32 | value.GetValueOrDefault().Should().BeNull(); 33 | } 34 | 35 | [TestMethod] 36 | public void TestThatSomeNonNulValueAsMaybeYieldsCorrectValue() 37 | { 38 | const string anyOtherString = "Any other string"; 39 | 40 | var value = new Maybe(AnyString); 41 | value.HasValue.Should().BeTrue(); 42 | value.IsNone.Should().BeFalse(); 43 | value.Value.Should().BeSameAs(AnyString); 44 | value.GetValueOrDefault(anyOtherString).Should().BeSameAs(AnyString); 45 | value.GetValueOrDefault().Should().BeSameAs(AnyString); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Samples/MultilevelCommandApp/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Microsoft.Extensions.Logging; 4 | using NClap; 5 | using NClap.Metadata; 6 | using NClap.Repl; 7 | 8 | namespace MultilevelCommandApp 9 | { 10 | class Program 11 | { 12 | static int Main(string[] args) 13 | { 14 | Console.WriteLine("Setting up logging..."); 15 | 16 | var loggerFactory = LoggerFactory.Create(builder => builder.AddConsole().SetMinimumLevel(LogLevel.Debug)); 17 | 18 | var logger = loggerFactory.CreateLogger(); 19 | 20 | Console.WriteLine("Parsing..."); 21 | 22 | var options = new CommandLineParserOptions().With() 23 | .ConfigureServices(s => s.AddSingleton(logger)); 24 | 25 | if (!CommandLineParser.TryParse(args, options, out ProgramArguments programArgs)) 26 | { 27 | return 1; 28 | } 29 | 30 | Console.WriteLine("Successfully parsed; reserialized:"); 31 | Console.WriteLine(" " + string.Join(" ", CommandLineParser.Format(programArgs))); 32 | 33 | Console.WriteLine("Executing..."); 34 | 35 | CommandResult result; 36 | if (programArgs.Command != null) 37 | { 38 | result = programArgs.Command.Execute(); 39 | } 40 | else 41 | { 42 | var loopOptions = new LoopOptions { ParserOptions = options }; 43 | var loop = new Loop(typeof(ProgramArguments.ToplevelCommandType), options: loopOptions); 44 | loop.Execute(); 45 | 46 | result = CommandResult.Success; 47 | } 48 | 49 | loggerFactory.Dispose(); 50 | 51 | Console.WriteLine($"Result: {result}"); 52 | 53 | return 0; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/NClap/IFileSystemReader.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace NClap 4 | { 5 | /// 6 | /// Abstract interface for reading/querying file system contents. 7 | /// 8 | public interface IFileSystemReader 9 | { 10 | /// 11 | /// Checks if the path exists as a file. 12 | /// 13 | /// Path to check. 14 | /// True if the path exists and references a non-directory 15 | /// file; false otherwise. 16 | bool FileExists(string path); 17 | 18 | /// 19 | /// Checks if the path exists as a directory. 20 | /// 21 | /// Path to check. 22 | /// True if the path exists and references a directory; false 23 | /// otherwise. 24 | bool DirectoryExists(string path); 25 | 26 | /// 27 | /// Enumerates the names of the files and directories that exist in the 28 | /// indicated directory, and which match the provided file pattern. 29 | /// 30 | /// Path to the containing directory. 31 | /// 32 | /// The file pattern to match. 33 | /// An enumeration of the names of the files. 34 | IEnumerable EnumerateFileSystemEntries(string directoryPath, string filePattern); 35 | 36 | /// 37 | /// Enumerate the textual lines in the specified file. Throws an 38 | /// IOException if I/O errors occur while accessing the file. 39 | /// 40 | /// Path to the file. 41 | /// The line enumeration. 42 | IEnumerable GetLines(string filePath); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Types/EnumArgumentValueTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Types; 4 | using NSubstitute; 5 | using System; 6 | using System.Reflection; 7 | 8 | namespace NClap.Tests.Types 9 | { 10 | [TestClass] 11 | public class EnumArgumentValueTests 12 | { 13 | [TestMethod] 14 | public void TestThatConstructorSucceedsEvenIfFieldValueIsOnlyAvailableAsRawConstant() 15 | { 16 | var anyInt = Any.PositiveInt(); 17 | 18 | var fieldInfo = Substitute.For(); 19 | fieldInfo.GetValue(Arg.Any()).Returns(o => throw new InvalidOperationException()); 20 | fieldInfo.GetRawConstantValue().ReturnsForAnyArgs(anyInt); 21 | 22 | var value = new EnumArgumentValue(fieldInfo); 23 | value.Value.Should().Be(anyInt); 24 | } 25 | 26 | [TestMethod] 27 | public void TestThatConstructorThrowsWhenEvenRawConstantValueIsNotAvailable() 28 | { 29 | var anyInt = Any.PositiveInt(); 30 | 31 | var fieldInfo = Substitute.For(); 32 | fieldInfo.GetValue(Arg.Any()).Returns(o => throw new InvalidOperationException()); 33 | fieldInfo.When(f => f.GetRawConstantValue()).Do(f => throw new NotImplementedException()); 34 | 35 | Action a = () => new EnumArgumentValue(fieldInfo); 36 | a.Should().Throw(); 37 | } 38 | 39 | [TestMethod] 40 | public void TestThatConstructorThrowsWhenValueIsNull() 41 | { 42 | var fieldInfo = Substitute.For(); 43 | fieldInfo.GetValue(Arg.Any()).ReturnsForAnyArgs(null); 44 | 45 | Action a = () => new EnumArgumentValue(fieldInfo); 46 | a.Should().Throw(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/NClap/Types/MergedEnumArgumentType.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NClap.Types 5 | { 6 | /// 7 | /// Implementation to describe merged enum types. 8 | /// 9 | internal class MergedEnumArgumentType : EnumArgumentType 10 | { 11 | /// 12 | /// Constructs a type that merges the given enum types. 13 | /// 14 | /// Types to merge. 15 | /// Thrown when zero types are 16 | /// provided. 17 | public MergedEnumArgumentType(IEnumerable types) 18 | { 19 | var typeCount = 0; 20 | foreach (var type in types) 21 | { 22 | AddValuesFromType(type); 23 | ++typeCount; 24 | } 25 | 26 | // We throw if 0 types are merged. 27 | if (typeCount == 0) 28 | { 29 | throw new ArgumentOutOfRangeException(nameof(types)); 30 | } 31 | } 32 | 33 | /// 34 | /// Constructs a type that merges the given enum types. 35 | /// 36 | /// Types to merge. 37 | /// Thrown when zero types are 38 | /// provided. 39 | public MergedEnumArgumentType(IEnumerable types) 40 | { 41 | var typeCount = 0; 42 | foreach (var type in types) 43 | { 44 | AddValuesFromType(type); 45 | ++typeCount; 46 | } 47 | 48 | // We throw if 0 types are merged. 49 | if (typeCount == 0) 50 | { 51 | throw new ArgumentOutOfRangeException(nameof(types)); 52 | } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/NClap/Expressions/ConcatenationExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace NClap.Expressions 4 | { 5 | /// 6 | /// Concatenation expression. 7 | /// 8 | internal class ConcatenationExpression : Expression 9 | { 10 | /// 11 | /// Basic constructor. 12 | /// 13 | /// Left-hand expression. 14 | /// Right-hand expression. 15 | public ConcatenationExpression(Expression left, Expression right) 16 | { 17 | Left = left ?? throw new ArgumentNullException(nameof(left)); 18 | Right = right ?? throw new ArgumentNullException(nameof(right)); 19 | } 20 | 21 | /// 22 | /// Left-hand expression. 23 | /// 24 | public Expression Left { get; } 25 | 26 | /// 27 | /// Right-hand expression. 28 | /// 29 | public Expression Right { get; } 30 | 31 | /// 32 | /// Tries to evaluate the given expression in the context of 33 | /// the given environment. 34 | /// 35 | /// Environment in which to evaluate the 36 | /// expression. 37 | /// On success, receives the evaluation 38 | /// result. 39 | /// true on success; false otherwise. 40 | public override bool TryEvaluate(ExpressionEnvironment env, out string value) 41 | { 42 | if (env == null) throw new ArgumentNullException(nameof(env)); 43 | 44 | if (!Left.TryEvaluate(env, out string left) || 45 | !Right.TryEvaluate(env, out string right)) 46 | { 47 | value = null; 48 | return false; 49 | } 50 | 51 | value = left + right; 52 | return true; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/NClap/Metadata/StringValidationAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Types; 3 | using NClap.Utilities; 4 | 5 | namespace NClap.Metadata 6 | { 7 | /// 8 | /// Abstract base class for implementing argument validation attributes 9 | /// that inspect strings. 10 | /// 11 | public abstract class StringValidationAttribute : ArgumentValidationAttribute 12 | { 13 | /// 14 | /// Checks if this validation attributes accepts values of the specified 15 | /// type. 16 | /// 17 | /// Type to check. 18 | /// True if this attribute accepts values of the specified 19 | /// type; false if not. 20 | /// Thrown when 21 | /// is null. 22 | public sealed override bool AcceptsType(IArgumentType type) 23 | { 24 | if (type == null) throw new ArgumentNullException(nameof(type)); 25 | return (type.Type == typeof(string)) || 26 | type.Type.IsEffectivelySameAs(typeof(FileSystemPath)); 27 | } 28 | 29 | /// 30 | /// Retrieves a string from the object, for use in validation. 31 | /// 32 | /// The object to retrieve the string from. 33 | /// The string. 34 | protected static string GetString(object value) 35 | { 36 | object valueToCast; 37 | if (value is string) 38 | { 39 | valueToCast = value; 40 | } 41 | else 42 | { 43 | if (!typeof(string).TryConvertFrom(value, out valueToCast)) 44 | { 45 | throw new InvalidCastException(); 46 | } 47 | } 48 | 49 | return (string)valueToCast; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/NClap/Metadata/ArgumentValueAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NClap.Exceptions; 4 | 5 | namespace NClap.Metadata 6 | { 7 | // CA1813: Avoid unsealed attributes 8 | #pragma warning disable CA1813 9 | 10 | /// 11 | /// Attribute for annotating values that can be used with arguments. It is 12 | /// most frequently used with values on enum types. 13 | /// 14 | [AttributeUsage(AttributeTargets.Field)] 15 | public class ArgumentValueAttribute : Attribute 16 | { 17 | private string _longName; 18 | 19 | /// 20 | /// Flags controlling the use of this value. 21 | /// 22 | public ArgumentValueFlags Flags { get; set; } 23 | 24 | /// 25 | /// The short name used to identify this value. 26 | /// 27 | public string ShortName { get; set; } 28 | 29 | /// 30 | /// The long name used to identify this value; null indicates that the 31 | /// "default" long name should be used. The long name for every value 32 | /// in the containing type must unique. It is an error to specify a 33 | /// long name of string.Empty. 34 | /// 35 | public string LongName 36 | { 37 | get => _longName; 38 | set 39 | { 40 | if ((value != null) && (value.Length == 0)) 41 | { 42 | throw new InvalidArgumentSetException(string.Format( 43 | CultureInfo.CurrentCulture, 44 | Strings.InvalidValueLongName)); 45 | } 46 | 47 | _longName = value; 48 | } 49 | } 50 | 51 | /// 52 | /// The description of the value, exposed via help/usage information. 53 | /// 54 | public string Description { get; set; } 55 | } 56 | 57 | #pragma warning restore CA1813 58 | } 59 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/StringValidationAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Metadata; 5 | using NClap.Utilities; 6 | 7 | namespace NClap.Tests.Metadata 8 | { 9 | [TestClass] 10 | public class StringValidationAttributeTests 11 | { 12 | [TestMethod] 13 | public void TryValidateThrowsOnNonString() 14 | { 15 | var attrib = new MustNotBeEmptyAttribute(); 16 | 17 | attrib.Invoking(a => a.TryValidate(CreateContext(), new StringValidationAttributeTests(), out string reason)) 18 | .Should().Throw(); 19 | } 20 | 21 | [TestMethod] 22 | public void NonEmptyStringCorrectlyChecksAsNotBeingEmpty() 23 | { 24 | var attrib = new MustNotBeEmptyAttribute(); 25 | attrib.TryValidate(CreateContext(), "non-empty", out string reason) 26 | .Should().BeTrue(); 27 | reason.Should().BeNull(); 28 | } 29 | 30 | [TestMethod] 31 | public void EmptyStringCorrectlyFailsAtNotBeingEmpty() 32 | { 33 | var attrib = new MustNotBeEmptyAttribute(); 34 | attrib.TryValidate(CreateContext(), string.Empty, out string reason) 35 | .Should().BeFalse(); 36 | reason.Should().NotBeNullOrEmpty(); 37 | } 38 | 39 | [TestMethod] 40 | public void ObjectConvertibleToStringValidatesCorrectly() 41 | { 42 | var attrib = new MustNotBeEmptyAttribute(); 43 | 44 | attrib.TryValidate(CreateContext(), new ColoredString("non-empty"), out string reason) 45 | .Should().BeTrue(); 46 | reason.Should().BeNull(); 47 | 48 | attrib.TryValidate(CreateContext(), ColoredString.Empty, out reason) 49 | .Should().BeFalse(); 50 | reason.Should().NotBeNullOrEmpty(); 51 | } 52 | 53 | private ArgumentValidationContext CreateContext() => 54 | new ArgumentValidationContext(null); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/AssemblyTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using FluentAssertions; 6 | using FluentAssertions.Types; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | 9 | namespace NClap.Tests 10 | { 11 | [TestClass] 12 | public class AssemblyTests 13 | { 14 | public static readonly Type RepresentativeType = typeof(CommandLineParser); 15 | 16 | public static readonly Assembly AssemblyUnderTest = RepresentativeType.GetTypeInfo().Assembly; 17 | 18 | [TestMethod] 19 | public void TestThatAllPublicTypesInAssemblyStartWithCorrectNamespacePrefix() 20 | { 21 | const string expectedNs = nameof(NClap); 22 | const string expectedNsWithDot = expectedNs + "."; 23 | 24 | foreach (var type in AllPublicTypes()) 25 | { 26 | var ns = type.Namespace; 27 | if (ns != expectedNs) 28 | { 29 | ns.Should().StartWith(expectedNsWithDot); 30 | } 31 | } 32 | } 33 | 34 | [TestMethod] 35 | public void TestThatNoPublicTypeInAssemblyContainsNonPrivateField() 36 | { 37 | var noNonPrivateFields = true; 38 | foreach (var type in AllPublicTypes().Where(t => !t.IsEnum)) 39 | { 40 | foreach (var member in type.GetTypeInfo().GetFields().Where(f => !IsConstant(f))) 41 | { 42 | if (!member.IsPrivate) 43 | { 44 | Console.Error.WriteLine($"Found non-private field in public type: {type.FullName} :: {member.Name}"); 45 | noNonPrivateFields = false; 46 | } 47 | } 48 | } 49 | 50 | noNonPrivateFields.Should().BeTrue(); 51 | } 52 | 53 | private static bool IsConstant(FieldInfo field) => 54 | field.IsLiteral && !field.IsInitOnly; 55 | 56 | private static IEnumerable AllPublicTypes() => AllTypes.From(AssemblyUnderTest).Where(t => t.GetTypeInfo().IsPublic); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/TypeUtilitiesTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Utilities; 4 | using System; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | [TestClass] 9 | public class TypeUtilitiesTests 10 | { 11 | private class TestClass 12 | { 13 | public TestClass() 14 | { 15 | } 16 | 17 | public TestClass(TestClass tc, string st) 18 | { 19 | } 20 | 21 | public TestClass(string st, TestClass tc) 22 | { 23 | } 24 | } 25 | 26 | [TestMethod] 27 | public void TestThatATypeIsEffectivelyTheSameAsItself() 28 | { 29 | GetType().IsEffectivelySameAs(GetType()).Should().BeTrue(); 30 | } 31 | 32 | [TestMethod] 33 | public void TestThatATypeIsNotEffectivelyTheSameAsAVeryDifferentType() 34 | { 35 | GetType().IsEffectivelySameAs(typeof(int)).Should().BeFalse(); 36 | } 37 | 38 | [TestMethod] 39 | public void GetConstructorIgnoresParameterlessConstructorWhenAsked() 40 | { 41 | Action find = () => typeof(TestClass).GetConstructor( 42 | Array.Empty(), considerParameterlessConstructor: false); 43 | 44 | find.Should().Throw(); 45 | } 46 | 47 | [TestMethod] 48 | public void GetConstructorFindsParameterlessConstructorWhenAsked() 49 | { 50 | var constructor = typeof(TestClass).GetConstructor( 51 | Array.Empty(), considerParameterlessConstructor: true); 52 | 53 | constructor.Should().NotBeNull(); 54 | 55 | var instance = constructor; 56 | constructor().Should().NotBeNull().And.BeOfType(); 57 | } 58 | 59 | [TestMethod] 60 | public void GetConstructorThrowsWhenTooMultipleConstructorsMatch() 61 | { 62 | Action find = () => typeof(TestClass).GetConstructor( 63 | new object[] { new TestClass(), "test" }, 64 | considerParameterlessConstructor: false); 65 | 66 | find.Should().Throw(); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/NClap/Help/TwoColumnArgumentHelpLayout.cs: -------------------------------------------------------------------------------- 1 | namespace NClap.Help 2 | { 3 | /// 4 | /// Describes a two-column argument help layout: name(s) in the first 5 | /// column, then description (if applicable) in the second. 6 | /// 7 | public class TwoColumnArgumentHelpLayout : ArgumentHelpLayout 8 | { 9 | /// 10 | /// Default constructor. 11 | /// 12 | public TwoColumnArgumentHelpLayout() 13 | { 14 | } 15 | 16 | /// 17 | /// Deeply cloning constructor. 18 | /// 19 | /// Template for clone. 20 | private TwoColumnArgumentHelpLayout(TwoColumnArgumentHelpLayout other) 21 | { 22 | for (var i = 0; i < ColumnWidths.Length; ++i) 23 | { 24 | ColumnWidths[i] = other.ColumnWidths[i]; 25 | } 26 | 27 | FirstLineColumnSeparator = other.FirstLineColumnSeparator; 28 | DefaultColumnSeparator = other.DefaultColumnSeparator; 29 | } 30 | 31 | /// 32 | /// Optional maximum widths of columns; null indicates no preference. 33 | /// 34 | #pragma warning disable CA1819 // Properties should not return arrays 35 | public int?[] ColumnWidths { get; } = new int?[2] { null, null }; 36 | #pragma warning restore CA1819 // Properties should not return arrays 37 | 38 | /// 39 | /// Optionally specifies separator string to be used between columns, 40 | /// but only on the first line; for all subsequent lines, or in case 41 | /// this property is left null, is otherwise 42 | /// used. 43 | /// 44 | public string FirstLineColumnSeparator { get; set; } = " - "; 45 | 46 | /// 47 | /// Separator string between columns. 48 | /// 49 | public string DefaultColumnSeparator { get; set; } = " "; 50 | 51 | /// 52 | /// Create a separate clone of this object. 53 | /// 54 | /// Clone. 55 | public override ArgumentHelpLayout DeepClone() => 56 | new TwoColumnArgumentHelpLayout(this); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/NClap/Parser/CommandGroupDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using NClap.Types; 4 | 5 | namespace NClap.Parser 6 | { 7 | /// 8 | /// Describes a command group. 9 | /// 10 | internal class CommandGroupDefinition 11 | { 12 | private readonly Dictionary _commandsByKey = new Dictionary(); 13 | 14 | /// 15 | /// Constructs a new command group definition. 16 | /// 17 | /// Argument type associated with this command group definition. 18 | internal CommandGroupDefinition(IArgumentType argType) 19 | { 20 | ArgumentType = argType; 21 | } 22 | 23 | /// 24 | /// Argument type associated with this command group definition. 25 | /// 26 | internal IArgumentType ArgumentType { get; } 27 | 28 | /// 29 | /// Enumerates all commands defined in this group. 30 | /// 31 | internal IEnumerable Commands => _commandsByKey.Values; 32 | 33 | /// 34 | /// Add a new command to this group. 35 | /// 36 | /// Command to add. 37 | internal void Add(CommandDefinition command) 38 | { 39 | if (command == null) 40 | { 41 | throw new ArgumentNullException(nameof(command)); 42 | } 43 | 44 | if (_commandsByKey.ContainsKey(command.Key)) 45 | { 46 | throw new ArgumentOutOfRangeException(nameof(command)); 47 | } 48 | 49 | _commandsByKey.Add(command.Key, command); 50 | } 51 | 52 | /// 53 | /// Tries to retrieve the definition of the command associated with the 54 | /// given key. 55 | /// 56 | /// Key to look up. 57 | /// On success, receives the matching command definition. 58 | /// true on success; false otherwise. 59 | internal bool TryGetCommand(object key, out CommandDefinition command) => 60 | _commandsByKey.TryGetValue(key, out command); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Utilities/StringWrapperTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Microsoft.VisualStudio.TestTools.UnitTesting; 4 | using NClap.Utilities; 5 | 6 | namespace NClap.Tests.Utilities 7 | { 8 | [TestClass] 9 | public class StringWrapperTests 10 | { 11 | [TestMethod] 12 | public void TestThatWrappingNullThrows() 13 | { 14 | Action a = () => new StringWrapper(null); 15 | a.Should().Throw(); 16 | } 17 | 18 | [TestMethod] 19 | public void TestThatWrappedStringYieldsOriginalString() 20 | { 21 | const string anyString = "Something here"; 22 | 23 | var wrapper = new StringWrapper(anyString); 24 | wrapper.Content.Should().Be(anyString); 25 | wrapper.ToString().Should().Be(anyString); 26 | } 27 | 28 | [TestMethod] 29 | public void TestThatImplicitOperatorsAreOkayWithNonNullStrings() 30 | { 31 | const string anyString = "Something here"; 32 | 33 | StringWrapper wrapper = anyString; 34 | wrapper.Should().NotBeNull(); 35 | 36 | string unwrapped = wrapper; 37 | unwrapped.Should().NotBeNull().And.Be(anyString); 38 | } 39 | 40 | [TestMethod] 41 | public void TestThatImplicitOperatorsPreserveNullness() 42 | { 43 | string nullString = null; 44 | 45 | StringWrapper wrapper = nullString; 46 | wrapper.Should().BeNull(); 47 | 48 | string unwrapped = wrapper; 49 | unwrapped.Should().BeNull(); 50 | } 51 | 52 | [TestMethod] 53 | public void TestThatTruncatingWrapperBuilderToLongerLengthThrows() 54 | { 55 | var builder = CreateStringBuilderWrapper(); 56 | 57 | const string anyString = "Hello"; 58 | builder.Append(anyString); 59 | 60 | builder.Invoking(b => b.Truncate(anyString.Length + 1)) 61 | .Should().Throw(); 62 | } 63 | 64 | private IStringBuilder CreateStringBuilderWrapper() 65 | { 66 | const string anyString = "Something here"; 67 | 68 | var wrapper = new StringWrapper(anyString); 69 | return wrapper.CreateNewBuilder(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Metadata/ArgumentAttributeTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using FluentAssertions; 4 | using Microsoft.VisualStudio.TestTools.UnitTesting; 5 | using NClap.Exceptions; 6 | using NClap.Metadata; 7 | using NClap.Utilities; 8 | 9 | namespace NClap.Tests.Metadata 10 | { 11 | [TestClass] 12 | public class ArgumentAttributeTests 13 | { 14 | [ArgumentSet(Style = ArgumentSetStyle.WindowsCommandLine)] 15 | public class SimpleTestClass 16 | { 17 | [NamedArgument(Description = "My value")] 18 | public int Value { get; set; } 19 | } 20 | 21 | [TestMethod] 22 | public void ParameterlessConstructorDefaults() 23 | { 24 | var namedAttribute = new NamedArgumentAttribute(); 25 | namedAttribute.Flags.Should().Be(ArgumentFlags.Optional); 26 | 27 | var positionalAttribute = new PositionalArgumentAttribute(); 28 | positionalAttribute.Flags.Should().Be(ArgumentFlags.Optional); 29 | } 30 | 31 | [TestMethod] 32 | public void EmptyLongNameThrowsOnRetrieval() 33 | { 34 | var attribute = new NamedArgumentAttribute(ArgumentFlags.AtMostOnce); 35 | 36 | Action setEmpty = () => attribute.LongName = string.Empty; 37 | setEmpty.Should().NotThrow(); 38 | 39 | Action getEmpty = () => { var x = attribute.LongName; }; 40 | getEmpty.Should().Throw(); 41 | } 42 | 43 | [TestMethod] 44 | public void NullLongNameIsOkay() 45 | { 46 | var attribute = new NamedArgumentAttribute(ArgumentFlags.AtMostOnce) { LongName = null }; 47 | attribute.LongName.Should().BeNull(); 48 | } 49 | 50 | [TestMethod] 51 | public void NonEmptyLongNameIsOkay() 52 | { 53 | var attribute = new NamedArgumentAttribute(ArgumentFlags.AtMostOnce) { LongName = "Xyzzy" }; 54 | attribute.LongName.Should().Be("Xyzzy"); 55 | } 56 | 57 | [TestMethod] 58 | public void NullConflictArrayThrows() 59 | { 60 | var attribute = new NamedArgumentAttribute(ArgumentFlags.AtMostOnce); 61 | 62 | Action setNull = () => attribute.ConflictsWith = null; 63 | setNull.Should().Throw(); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use Ubuntu 20.04 with .NET 6.0 installed 2 | FROM mcr.microsoft.com/vscode/devcontainers/dotnet:6.0-focal 3 | 4 | # Make sure pipe failures trigger build failures 5 | SHELL ["/bin/bash", "-o", "pipefail", "-c"] 6 | 7 | # Add ppa for .NET components from Microsoft 8 | # Install native components 9 | WORKDIR /tmp/work 10 | RUN curl https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb -L --output ./packages-microsoft-prod.deb && \ 11 | dpkg -i ./packages-microsoft-prod.deb && \ 12 | rm ./packages-microsoft-prod.deb && \ 13 | \ 14 | apt-get update && \ 15 | apt-get install -y --no-install-recommends \ 16 | curl \ 17 | dos2unix \ 18 | git \ 19 | gnupg2 \ 20 | sudo \ 21 | zsh \ 22 | \ 23 | dotnet-sdk-3.1 \ 24 | && \ 25 | apt-get clean && \ 26 | rm -rf /var/lib/apt/lists/* && \ 27 | \ 28 | rm -rf /tmp/work 29 | 30 | # Switch to non-root user 31 | USER vscode 32 | 33 | # Download external components 34 | # Validate components against known hashes 35 | # Clone additional oh-my-zsh plugins 36 | # Setup oh-my-zsh plugins 37 | # Install 'trunk' 38 | WORKDIR /tmp/work 39 | COPY hashes.txt . 40 | RUN curl https://github.com/sharkdp/bat/releases/download/v0.22.1/bat-musl_0.22.1_amd64.deb -L --output ./bat-musl_0.22.1_amd64.deb && \ 41 | curl https://github.com/ogham/exa/releases/download/v0.10.1/exa-linux-x86_64-v0.10.1.zip -L --output ./exa-linux-x86_64-v0.10.1.zip && \ 42 | sha256sum --check hashes.txt && \ 43 | \ 44 | git clone https://github.com/zsh-users/zsh-autosuggestions.git ~/.oh-my-zsh/custom/plugins/zsh-autosuggestions && \ 45 | git clone https://github.com/zsh-users/zsh-syntax-highlighting.git ~/.oh-my-zsh/custom/plugins/zsh-syntax-highlighting && \ 46 | sed -i "s/^plugins=.*/plugins=(git zsh-autosuggestions zsh-syntax-highlighting)/" ~/.zshrc && \ 47 | \ 48 | sudo dpkg -i ./bat-musl_0.22.1_amd64.deb && \ 49 | sudo apt-get install -f && \ 50 | \ 51 | unzip ./exa-linux-x86_64-v0.10.1.zip && \ 52 | sudo mv ./bin/* /usr/bin && \ 53 | sudo mv ./completions/exa.zsh /usr/share/zsh/vendor-completions/_exa && \ 54 | \ 55 | curl https://get.trunk.io -fsSL | sudo bash -s - -y && \ 56 | sudo chmod a+r /usr/local/bin/trunk && \ 57 | \ 58 | rm -rf /tmp/work 59 | 60 | # Use zsh as the default shell 61 | ENV SHELL /bin/zsh 62 | -------------------------------------------------------------------------------- /src/NClap/Parser/CommandDefinition.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using NClap.Exceptions; 5 | using NClap.Metadata; 6 | 7 | namespace NClap.Parser 8 | { 9 | /// 10 | /// Describes a command. 11 | /// 12 | internal class CommandDefinition 13 | { 14 | /// 15 | /// Defines a command. 16 | /// 17 | /// Value that was used / can be used to select this command. 18 | /// Type that implements this command. 19 | public CommandDefinition(object key, Type implementingType) 20 | { 21 | Key = key; 22 | ImplementingType = implementingType ?? throw new ArgumentNullException(nameof(implementingType)); 23 | 24 | if (!typeof(ICommand).GetTypeInfo().IsAssignableFrom(ImplementingType.GetTypeInfo())) 25 | { 26 | throw new InvalidCommandException($"Type {ImplementingType.FullName} does not implement required {typeof(ICommand).FullName} interface."); 27 | } 28 | } 29 | 30 | /// 31 | /// Value that selects this command. 32 | /// 33 | public object Key { get; } 34 | 35 | /// 36 | /// Implementing type for this command. 37 | /// 38 | public Type ImplementingType { get; } 39 | 40 | /// 41 | /// Instantiate the command. 42 | /// 43 | /// Service configurer. 44 | /// Instantiated command object. 45 | internal ICommand Instantiate(ServiceConfigurer serviceConfigurer) 46 | { 47 | var services = new ServiceCollection(); 48 | 49 | serviceConfigurer?.Invoke(services); 50 | 51 | services.AddTransient(typeof(ICommand), ImplementingType); 52 | 53 | var provider = services.BuildServiceProvider(); 54 | 55 | try 56 | { 57 | return provider.GetService(); 58 | } 59 | catch (InvalidOperationException ex) 60 | { 61 | throw new InvalidCommandException($"No matching command constructor could be found on type '{ImplementingType.FullName}'.", ex); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/NClap/Help/ArgumentMetadataHelpOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Utilities; 3 | 4 | namespace NClap.Help 5 | { 6 | /// 7 | /// Options for a piece of argument metadata. 8 | /// 9 | public class ArgumentMetadataHelpOptions : IDeepCloneable 10 | { 11 | /// 12 | /// Default constructor. 13 | /// 14 | public ArgumentMetadataHelpOptions() 15 | { 16 | } 17 | 18 | /// 19 | /// Deeply cloning constructor. 20 | /// 21 | /// Template for clone. 22 | protected ArgumentMetadataHelpOptions(ArgumentMetadataHelpOptions other) 23 | { 24 | if (other == null) throw new ArgumentNullException(nameof(other)); 25 | 26 | Include = other.Include; 27 | HeaderTitle = other.HeaderTitle; 28 | Color = other.Color; 29 | BlockIndent = other.BlockIndent; 30 | HangingIndent = other.HangingIndent; 31 | } 32 | 33 | /// 34 | /// Should this piece of metadata be included. 35 | /// 36 | public bool Include { get; set; } = true; 37 | 38 | /// 39 | /// Optionally provides header text; if left unspecified, default is used. 40 | /// 41 | public string HeaderTitle { get; set; } 42 | 43 | /// 44 | /// Color for this piece of metadata. 45 | /// 46 | public TextColor Color { get; set; } 47 | 48 | /// 49 | /// Number of characters to block-indent the body of this item; if present, 50 | /// will override the default specified in . 51 | /// 52 | public int? BlockIndent { get; set; } 53 | 54 | /// 55 | /// Number of characters to hanging-indent the body of this item; if present, 56 | /// will override the default specified in . 57 | /// 58 | public int? HangingIndent { get; set; } 59 | 60 | /// 61 | /// Create a separate clone of this object. 62 | /// 63 | /// Clone. 64 | public virtual ArgumentMetadataHelpOptions DeepClone() => new ArgumentMetadataHelpOptions(this); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/NClap/Metadata/MustNotBeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using NClap.Types; 4 | 5 | namespace NClap.Metadata 6 | { 7 | /// 8 | /// Attribute that indicates the associated string argument member cannot be 9 | /// empty. 10 | /// 11 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = true)] 12 | public sealed class MustNotBeAttribute : ArgumentValidationAttribute 13 | { 14 | /// 15 | /// Constructs a new validation attribute that requires the associated 16 | /// argument's value to not be equal to the specified value. 17 | /// 18 | /// The value that the argument may not equal. 19 | /// 20 | public MustNotBeAttribute(object value) 21 | { 22 | Value = value; 23 | } 24 | 25 | /// 26 | /// The value that this attribute checks against. 27 | /// 28 | public object Value { get; } 29 | 30 | /// 31 | /// Checks if this validation attributes accepts values of the specified 32 | /// type. 33 | /// 34 | /// Type to check. 35 | /// True if this attribute accepts values of the specified 36 | /// type; false if not. 37 | public override bool AcceptsType(IArgumentType type) => true; 38 | 39 | /// 40 | /// Validate the provided value in accordance with the attribute's 41 | /// policy. 42 | /// 43 | /// Context for validation. 44 | /// The value to validate. 45 | /// On failure, receives a user-readable string 46 | /// message explaining why the value is not valid. 47 | /// True if the value passes validation; false otherwise. 48 | /// 49 | public override bool TryValidate(ArgumentValidationContext context, object value, out string reason) 50 | { 51 | if (!Value.Equals(value)) 52 | { 53 | reason = null; 54 | return true; 55 | } 56 | 57 | reason = string.Format(CultureInfo.CurrentCulture, Strings.ValueMayNotBe, Value); 58 | return false; 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/NClap/Utilities/FluentBuilder`1.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | 4 | namespace NClap.Utilities 5 | { 6 | /// 7 | /// Utility class for fluent builders that manipulate state of the given type. 8 | /// 9 | /// Type of the state. 10 | public class FluentBuilder 11 | { 12 | private readonly TState _startingState; 13 | private readonly List> _transformers = new List>(); 14 | 15 | /// 16 | /// Basic constructor. 17 | /// 18 | /// The starting state. 19 | public FluentBuilder(TState startingState) 20 | { 21 | _startingState = startingState; 22 | } 23 | 24 | // CA1062: Validate arguments of public methods 25 | #pragma warning disable CA1062 26 | /// 27 | /// Operator that allows implicit casting from a builder to its applied result. 28 | /// 29 | /// Fluent builder to apply and generate state from. 30 | public static implicit operator TState(FluentBuilder builder) => builder.Apply(); 31 | #pragma warning restore CA1062 32 | 33 | /// 34 | /// Operator that allows implicitly forming a fluent builder from a starting 35 | /// state. 36 | /// 37 | /// Input state object to create a fluent builder from. 38 | public static implicit operator FluentBuilder(TState state) => 39 | new FluentBuilder(state); 40 | 41 | /// 42 | /// Appends a new transformer function. 43 | /// 44 | /// Function. 45 | public void AddTransformer(Action transformer) => 46 | _transformers.Add(transformer); 47 | 48 | /// 49 | /// Applies all accumulated transformers, producing a final state. 50 | /// 51 | /// The final transformed state. 52 | public TState Apply() 53 | { 54 | var state = _startingState; 55 | foreach (var transformer in _transformers) 56 | { 57 | transformer(state); 58 | } 59 | 60 | return state; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/NClap/Expressions/OperatorExpression.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using NClap.Exceptions; 3 | 4 | namespace NClap.Expressions 5 | { 6 | /// 7 | /// Operator expression. 8 | /// 9 | internal class OperatorExpression : Expression 10 | { 11 | /// 12 | /// Basic constructor. 13 | /// 14 | /// Operator. 15 | /// Operand. 16 | public OperatorExpression(Operator op, Expression operand) 17 | { 18 | if (op == Operator.Unspecified) 19 | { 20 | throw new ArgumentOutOfRangeException(nameof(op)); 21 | } 22 | 23 | Operator = op; 24 | Operand = operand ?? throw new ArgumentNullException(nameof(operand)); 25 | } 26 | 27 | /// 28 | /// Operator type. 29 | /// 30 | public Operator Operator { get; } 31 | 32 | /// 33 | /// Operand expression. 34 | /// 35 | public Expression Operand { get; } 36 | 37 | /// 38 | /// Tries to evaluate the given expression in the context of 39 | /// the given environment. 40 | /// 41 | /// Environment in which to evaluate the 42 | /// expression. 43 | /// On success, receives the evaluation 44 | /// result. 45 | /// true on success; false otherwise. 46 | public override bool TryEvaluate(ExpressionEnvironment env, out string value) 47 | { 48 | if (env == null) throw new ArgumentNullException(nameof(env)); 49 | 50 | if (!Operand.TryEvaluate(env, out string operand)) 51 | { 52 | value = null; 53 | return false; 54 | } 55 | 56 | switch (Operator) 57 | { 58 | case Operator.ConvertToUpperCase: 59 | value = operand.ToUpper(); 60 | return true; 61 | case Operator.ConvertToLowerCase: 62 | value = operand.ToLower(); 63 | return true; 64 | case Operator.Unspecified: 65 | default: 66 | throw new InternalInvariantBrokenException("Unknown operator"); 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Tests/UnitTests/Expressions/StringExpanderTests.cs: -------------------------------------------------------------------------------- 1 | using FluentAssertions; 2 | using Microsoft.VisualStudio.TestTools.UnitTesting; 3 | using NClap.Expressions; 4 | 5 | namespace NClap.Tests.Expressions 6 | { 7 | [TestClass] 8 | public class StringExpanderTests 9 | { 10 | private readonly ExpressionEnvironment _env = new TestEnvironment(); 11 | 12 | [TestMethod] 13 | public void TestThatStringWithoutExpressionExpandsToItself() 14 | { 15 | const string anyString = "foo"; 16 | TryExpand(anyString, out string result).Should().BeTrue(); 17 | result.Should().Be(anyString); 18 | } 19 | 20 | [TestMethod] 21 | public void TestThatEmptyStringExpandsToItself() 22 | { 23 | TryExpand(string.Empty, out string result).Should().BeTrue(); 24 | result.Should().Be(string.Empty); 25 | } 26 | 27 | [TestMethod] 28 | public void TestThatUnterminatedExpressionFailsExpansion() 29 | { 30 | TryExpand("something {", out string result).Should().BeFalse(); 31 | result.Should().BeNull(); 32 | 33 | TryExpand("something {\"foo\"", out result).Should().BeFalse(); 34 | result.Should().BeNull(); 35 | } 36 | 37 | [TestMethod] 38 | public void TestThatEmptyExpressionFailsExpansion() 39 | { 40 | TryExpand("{}", out string result).Should().BeFalse(); 41 | result.Should().BeNull(); 42 | } 43 | 44 | [TestMethod] 45 | public void TestThatInvalidExpressionFailsExpansion() 46 | { 47 | TryExpand("{INVALIDEXPRESSION}", out string result).Should().BeFalse(); 48 | result.Should().BeNull(); 49 | } 50 | 51 | [TestMethod] 52 | public void TestThatValidExpressionIsCorrectlyExpanded() 53 | { 54 | TryExpand("Hello {\"world\"}, foo", out string result).Should().BeTrue(); 55 | result.Should().Be("Hello world, foo"); 56 | } 57 | 58 | [TestMethod] 59 | public void TestThatMultipleExpansionsAreSupported() 60 | { 61 | TryExpand("{\"Hello\"}{\", \"} {\"world\"}!", out string result).Should().BeTrue(); 62 | result.Should().Be("Hello, world!"); 63 | } 64 | 65 | private bool TryExpand(string value, out string result) => 66 | StringExpander.TryExpand(_env, value, out result); 67 | } 68 | } 69 | --------------------------------------------------------------------------------