diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 88ccb6c..3de1292 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -47,9 +47,6 @@ jobs: - name: "dotnet pack: ${{ env.VER_STR }}" run: dotnet pack Src/RT.CommandLine.csproj --configuration Release -p:InformationalVersion="${{env.VER_STR}}" -p:VersionPrefix=${{env.VER_NUM}} -p:VersionSuffix=${{env.VER_SUF}} -p:FileVersion=${{env.VER_NUM}} -p:AssemblyVersion=${{env.VER_NUM}} -o Publish - - name: "dotnet pack Lingo: ${{ env.VER_STR }}" - run: dotnet pack SrcLingo/RT.CommandLine.Lingo.csproj --configuration Release -p:InformationalVersion="${{env.VER_STR}}" -p:VersionPrefix=${{env.VER_NUM}} -p:VersionSuffix=${{env.VER_SUF}} -p:FileVersion=${{env.VER_NUM}} -p:AssemblyVersion=${{env.VER_NUM}} -o Publish - - name: Push to NuGet run: dotnet nuget push Publish/*.nupkg -k ${{ secrets.NUGET_API_KEY }} -s https://api.nuget.org/v3/index.json diff --git a/External/RT.Util b/External/RT.Util index 0b24fb8..cd73ade 160000 --- a/External/RT.Util +++ b/External/RT.Util @@ -1 +1 @@ -Subproject commit 0b24fb8af9318edf2fb1b45d6d97b728133fcde8 +Subproject commit cd73ade4acf2f491d0f7a49a5c2e50392a1ad7f0 diff --git a/RT.CommandLine.sln b/RT.CommandLine.sln index f41c714..636e524 100644 --- a/RT.CommandLine.sln +++ b/RT.CommandLine.sln @@ -4,8 +4,6 @@ VisualStudioVersion = 17.8.34330.188 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RT.CommandLine", "Src\RT.CommandLine.csproj", "{DED68431-337E-439C-94E4-ACE2E5363178}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RT.CommandLine.Lingo", "SrcLingo\RT.CommandLine.Lingo.csproj", "{DC6401A1-14F1-44E3-A121-43CC2F192809}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "RT.CommandLine.Tests", "Tests\RT.CommandLine.Tests.csproj", "{294D2B02-3FA6-4B9A-82BF-F80396943892}" EndProject Global @@ -18,10 +16,6 @@ Global {DED68431-337E-439C-94E4-ACE2E5363178}.Debug|Any CPU.Build.0 = Debug|Any CPU {DED68431-337E-439C-94E4-ACE2E5363178}.Release|Any CPU.ActiveCfg = Release|Any CPU {DED68431-337E-439C-94E4-ACE2E5363178}.Release|Any CPU.Build.0 = Release|Any CPU - {DC6401A1-14F1-44E3-A121-43CC2F192809}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {DC6401A1-14F1-44E3-A121-43CC2F192809}.Debug|Any CPU.Build.0 = Debug|Any CPU - {DC6401A1-14F1-44E3-A121-43CC2F192809}.Release|Any CPU.ActiveCfg = Release|Any CPU - {DC6401A1-14F1-44E3-A121-43CC2F192809}.Release|Any CPU.Build.0 = Release|Any CPU {294D2B02-3FA6-4B9A-82BF-F80396943892}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {294D2B02-3FA6-4B9A-82BF-F80396943892}.Debug|Any CPU.Build.0 = Debug|Any CPU {294D2B02-3FA6-4B9A-82BF-F80396943892}.Release|Any CPU.ActiveCfg = Release|Any CPU diff --git a/Src/CommandLine.cs b/Src/CommandLine.cs index af72185..ad6789f 100644 --- a/Src/CommandLine.cs +++ b/Src/CommandLine.cs @@ -1,4 +1,4 @@ -using System.Reflection; +using System.Reflection; using RT.Internal; using RT.PostBuild; using RT.Util; diff --git a/Src/RT.CommandLine.csproj b/Src/RT.CommandLine.csproj index f94364e..286b1fa 100644 --- a/Src/RT.CommandLine.csproj +++ b/Src/RT.CommandLine.csproj @@ -16,8 +16,8 @@ - - + + diff --git a/SrcLingo/CommandLine.cs b/SrcLingo/CommandLine.cs deleted file mode 100644 index 755f000..0000000 --- a/SrcLingo/CommandLine.cs +++ /dev/null @@ -1,2127 +0,0 @@ -using System.Diagnostics; -using System.Reflection; -using RT.Lingo; -using RT.PostBuild; -using RT.Serialization; -using RT.Util; -using RT.Util.Consoles; -using RT.Util.ExtensionMethods; -using RT.Util.Text; - -namespace RT.CommandLine.Lingo; - -/// -/// Implements a command-line parser that can turn the commands and options specified by the user on the command line into -/// a strongly-typed instance of a specific class. See remarks for more details. -/// -/// -/// The following conditions must be met by the class wishing to receive the options and parameters: -/// -/// -/// It must be a reference type (a class), must have , and it must have a -/// parameterless constructor (unless it has subcommands, see below). -/// -/// -/// Every field in the class must have one of the following custom attributes: -/// -/// -/// (allowed for all supported types except bool) — specifies -/// that the parameter is positional; the user specifies the value(s) in place without an option preceding -/// it. -/// -/// (allowed for all supported types) — specifies that the parameter invoked -/// by an option, e.g. -x, which may or may not be followed by a value. (This does not imply that -/// the parameter is necessarily optional.) -/// -/// (allowed for enum types only) — specifies that the parameter can be -/// invoked by one of several options, which are specified on the enum values in the enum type. -/// -/// — specifies that shall completely ignore -/// the field. -/// -/// -/// Each field may optionally have any of the following custom attributes: -/// -/// -/// (allowed for all supported types except bool) — specifies -/// that the parameter must be specified by the user. For a string[] field, it means that at least -/// one value must be specified. -/// -/// — specifies that the option or command does not appear in the help -/// screen generated by CommandLineParser. -/// -/// -/// Each field in the class must be of one of the following types: -/// -/// -/// string, any integer type, float, double, or any nullable version of these. The -/// field can be positional () or not (). -/// -/// string[]. The field can be positional () or not (), but if it is positional, it must be the last positional parameter. -/// -/// bool. The field must have an and cannot be positional or -/// mandatory. -/// -/// -/// Any enum type. There are three ways that enum types can be used. To explain these, the following -/// enum type declaraction is used as an example: -/// -/// enum OutputFormat { PlainText, Xml } -/// -/// -/// -/// — The user can specify a single parameter (e.g. -/// plain or xml) to select an enum value. Every value in the enum type must -/// have a to specify the name by which that enum value is -/// selected: -/// -/// enum OutputFormat -/// { -/// [CommandName("plain")] -/// PlainText, -/// [CommandName("xml")] -/// Xml -/// } -/// -/// — The user can select an enum value by specifying an option -/// followed by a parameter that identifies the enum value (e.g. -f plain or -f -/// xml). As above, every value in the enum type must have a to specify the name by which that enum value is selected. -/// -/// -/// — The user can select an enum value by specifying just -/// an option (e.g. -p or -x). Every value in the enum type must have an to specify the option by which that enum value is selected: -/// -/// enum OutputFormat -/// { -/// [Option("-p", "--plain")] -/// PlainText, -/// [Option("-x", "--xml")] -/// Xml -/// } -/// -/// A parameter on the attribute determines whether the user is allowed to specify only one -/// enum value or multiple (which will be combined using bitwise or). -/// -/// If the field is optional, the enum value that corresponds to the field’s initial (default) -/// value may omit the or . -/// -/// -/// Every field must have documentation or be explicitly marked with -/// (except for fields that use or ). For -/// every field whose type is an enum type, the values in the enum type must also have documentation or , except for the enum value that corresponds to the field’s default value if -/// the field is not mandatory. -/// -/// Documentation is provided in one of the following ways: -/// -/// -/// Monolingual, translation-agnostic (unlocalizable) applications use the to specify documentation directly. -/// -/// -/// Translatable applications must declare methods with the following signature: -/// -/// static string FieldNameDoc(Translation) -/// -/// The first parameter must be of the same type as the object passed in for the applicationTr -/// parameter of . The name of the method is the name of the field or enum value -/// followed by Doc. The return value is the translated string. -/// -/// and can be used together. However, a -/// positional field can only be made mandatory if all the positional fields preceding it are also mandatory. -/// -/// Subcommands can be implemented by using derived classes. For example, in order to allow the user to invoke -/// commands of the following form: -/// -/// MyTool.exe create new_item -/// MyTool.exe rename old_name new_name -/// -/// you would declare the following classes: -/// -/// [CommandLine] -/// abstract class CmdBase { } -/// -/// [CommandName("create")] -/// sealed class CmdCreate : CmdBase -/// { -/// [IsPositional, IsMandatory] -/// public string ItemName; -/// } -/// -/// [CommandName("rename")] -/// sealed class CmdRename : CmdBase -/// { -/// [IsPositional, IsMandatory] -/// public string OldName; -/// [IsPositional, IsMandatory] -/// public string NewName; -/// } -/// -/// In this example, we have omitted the documentation attributes, but in practice they would be required. The -/// following points are of note here: -/// -/// -/// -/// The class CmdBase is abstract to indicate that the subcommand is mandatory. The class could be made -/// non-abstract to indicate that the subcommand is optional. -/// -/// -/// The class CmdBase does not need to have a parameterless constructor because only CmdCreate -/// and CmdRename would actually be instantiated by CommandLineParser. However, if it were -/// non-abstract, it would need a parameterless constructor. -/// -/// -/// Parameters that pertain to all subcommands can be added in CmdBase and the user would specify those -/// before the command name. -/// -/// -/// You can have any arbitrary multi-level class hierarchy. Only classes marked with become subcommands. -public static class CommandLineParser -{ - /// - /// Parses the specified command-line arguments into an instance of the specified type. See the remarks section of the - /// documentation for for features and limitations. - /// - /// The class containing the fields and attributes which define the command-line syntax. - /// - /// The command-line arguments to be parsed. - /// - /// Specifies the application’s translation object which contains the localised strings that document the command-line - /// options and commands. This object is passed in to the FieldNameDoc methods described in the documentation - /// for . This should be null for monoligual applications. - /// - /// Specifies a callback which is invoked on every documentation string retrieved from the s to generate the help text. This callback can modify the text arbitrarily. - /// - /// An instance of the class containing the options and parameters specified by the user - /// on the command line. - public static TArgs Parse(string[] args, TranslationBase applicationTr = null, Func helpProcessor = null) - { - return (TArgs) parseCommandLine(getCommandInfo(typeof(TArgs)), args, 0, applicationTr, helpProcessor); - } - - /// - /// Parses the specified command-line arguments into an instance of the specified type. In case of failure, prints - /// usage information to the console and returns default(TArgs). See the remarks section of the documentation - /// for for features and limitations. - /// - /// The class containing the fields and attributes which define the command-line syntax. - /// - /// The command-line arguments to be parsed. - /// - /// Specifies the application’s translation object which contains the localized strings that document the command-line - /// options and commands. This object is passed in to the FieldNameDoc() methods described in the documentation for - /// . This should be null for monoligual applications. - /// - /// Specifies a translation object that contains the localized strings for CommandLineParser’s own text. - /// - /// Specifies a callback which is invoked on every documentation string retrieved from the s to generate the help text. This callback can modify the text arbitrarily. - /// - /// An instance of the class containing the options and parameters specified by the user - /// on the command line. - public static TArgs ParseOrWriteUsageToConsole(string[] args, TranslationBase applicationTr = null, Translation cmdLineTr = null, Func helpProcessor = null) - { - try - { - return Parse(args, applicationTr, helpProcessor); - } - catch (CommandLineParseException e) - { - e.WriteUsageInfoToConsole(applicationTr, cmdLineTr, helpProcessor); - return default(TArgs); - } - } - - private static CommandInfo getCommandInfo(Type type) - { - return new CommandInfo { Elements = getCommandLineElements(type).ToArray(), Type = type }; - } - - private static IEnumerable getCommandLineElements(Type type) - { - var haveSeenOptionalPositional = false; - foreach (var fieldForeach in getEligibleFields(type)) - { - var field = fieldForeach; // This is necessary for the lambda expressions to work - - if (field.IsDefined()) - continue; - - var positional = field.IsDefined(); - var mandatory = field.IsDefined(); - - if (positional && mandatory && haveSeenOptionalPositional) - throw new InternalErrorException("Cannot have positional mandatory parameter after a positional optional one."); - - if (positional && !mandatory) - haveSeenOptionalPositional = true; - - // ### ENUM fields - if (field.FieldType.IsEnum) - { - // ### ENUM fields, positional - if (positional) - yield return new CmdLineEnumFieldPositional { IsMandatory = mandatory, Field = field }; - - // ### ENUM fields, non-positional - else - { - // Take care of both option+name scheme (e.g. “-x foo -x bar”) and option scheme (e.g. “-x -y”) - var behavior = field.GetCustomAttributes().Select(eoa => eoa.Behavior).FirstOrDefault(EnumBehavior.SingleValue); - var underlyingType = field.FieldType.GetEnumUnderlyingType(); - var option = field.GetCustomAttributes().FirstOrDefault(); - if (option == null) - { - var enumFields = field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public) - .SelectMany(f => f.GetOrderedOptionAttributeNames().Select(name => Ut.KeyValuePair(name, f.GetRawConstantValue()))) - .ToArray(); - yield return new CmdLineEnumOptions - { - Behavior = behavior, - Field = field, - IsMandatory = mandatory, - Options = enumFields.Select(inf => inf.Key).ToArray(), - OptionToValue = enumFields.ToDictionary() - }; - } - else - { - yield return new CmdLineEnumOptionWithNames - { - Behavior = behavior, - Options = option.Names, - Field = field, - IsMandatory = mandatory, - NameToValue = field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public) - .SelectMany(f => f.GetCustomAttributes().SelectMany(cna => cna.Names).Select(name => Ut.KeyValuePair(name, f.GetRawConstantValue()))) - .ToDictionary() - }; - } - } - } - - // ### BOOL fields - else if (field.FieldType == typeof(bool)) - yield return new CmdLineBoolOption { IsMandatory = mandatory, Field = field, Options = field.GetOrderedOptionAttributeNames() }; - - // ### STRING and INTEGER fields (including nullable) - else if (field.FieldType == typeof(string) || ExactConvert.IsTrueIntegerType(field.FieldType) || ExactConvert.IsTrueIntegerNullableType(field.FieldType) || - field.FieldType == typeof(float) || field.FieldType == typeof(float?) || field.FieldType == typeof(double) || field.FieldType == typeof(double?)) - { - if (positional) - yield return new CmdLineOtherPositional { Field = field, IsMandatory = mandatory }; - else - yield return new CmdLineOtherOption { Field = field, IsMandatory = mandatory, Options = field.GetOrderedOptionAttributeNames() }; - } - - // ### STRING[] fields - else if (field.FieldType == typeof(string[])) - { - if (positional) - yield return new CmdLineStringArrayPositional { Field = field, IsMandatory = mandatory }; - else - yield return new CmdLineStringArrayOption { Field = field, IsMandatory = mandatory, Options = field.GetOrderedOptionAttributeNames() }; - } - else - // This only happens if the post-build check didn't run - throw new InternalErrorException("{0}.{1} is not of a supported type.".Fmt(type.FullName, field.Name)); - } - - // ### Command names - - // See if the class has subclasses that represent subcommands - var derivedTypes = getDirectSubcommands(type); - if (derivedTypes.Length > 0) - { - yield return new CmdLineSubcommand - { - IsMandatory = type.IsAbstract, - Type = type, - Subcommands = derivedTypes.Select(t => new SubcommandInfo - { - Elements = getCommandLineElements(t).ToArray(), - Names = t.GetCustomAttributes().First().Names, - Type = t - }).ToArray() - }; - } - } - - private static FieldInfo[] getEligibleFields(Type type) - { - var bindingFlags = BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; - - // Get all fields from the type - var fields = type.GetFields(bindingFlags).ToList(); - - // Keep adding fields from the base type until we find one with CommandNameAttribute or CommandLineAttribute - var testType = type.BaseType; - while (testType != typeof(object) && !testType.IsDefined() && !testType.IsDefined()) - { - fields.AddRange(testType.GetFields(bindingFlags)); - testType = testType.BaseType; - } - - return fields.ToArray(); - } - - private static Type[] getDirectSubcommands(Type type) - { - var types = AppDomain.CurrentDomain.GetAssemblies().SelectMany(asm => asm.GetTypes()).Where(t => !t.IsGenericTypeDefinition && t.IsSubclassOf(type) && t.IsDefined()).ToList(); - types.RemoveAll(t => types.Any(t.IsSubclassOf)); - return types.ToArray(); - } - - private static object parseCommandLine(CommandInfo cmd, string[] args, int i, TranslationBase applicationTr, Func helpProcessor) - { - if (i < args.Length) - if (args[i] == "-?" || args[i] == "/?" || args[i] == "--?" || args[i] == "/h" || args[i] == "--help" || args[i] == "-help" || args[i] == "help") - throw new CommandLineHelpRequestedException(cmd); - - var elements = cmd.Elements; - var missingMandatories = new HashSet(elements.Where(e => e.IsMandatory)); - var positionals = elements.Where(e => e.IsPositional).ToQueue(); - var actionsToPerform = new List>(); - - bool suppressOptions = false; - object ret = null; - - while (i < args.Length) - { - if (args[i] == "--" && !suppressOptions) - { - suppressOptions = true; - i++; - } - else if (!suppressOptions && args[i].StartsWith('-')) - { - CmdLineElement el = null; - foreach (var element in elements.Where(e => !e.IsPositional)) - { - if (element.ProcessParameter(args, ref i, actionsToPerform, suppressOptions, helpProcessor, cmd)) - { - el = element; - break; - } - } - if (el == null) - throw new UnrecognizedCommandOrOptionException(args[i], cmd); - missingMandatories.Remove(el); - } - else // positional - { - if (positionals.Count == 0) - throw new UnexpectedArgumentException(args.Subarray(i), cmd); - var positional = positionals.Dequeue(); - // This should only return true or throw an exception - Ut.Assert(positional.ProcessParameter(args, ref i, actionsToPerform, suppressOptions, helpProcessor, cmd)); - if (positional is CmdLineSubcommand) - { - // Special case: recursive call - ret = parseCommandLine(((CmdLineSubcommand) positional).Subcommand, args, i, applicationTr, helpProcessor); - i = args.Length; - } - else if (positional is CmdLineStringArrayPositional) - { - // Special case: this positional remains in the queue forever - positionals.Enqueue(positional); - } - missingMandatories.Remove(positional); - } - } - - if (positionals.Count > 0) - positionals.Dequeue().ProcessEndOfParameters(actionsToPerform, cmd); - - foreach (var m in missingMandatories) - m.ProcessEndOfParameters(actionsToPerform, cmd); - - if (ret == null) // there was no subcommand - ret = Activator.CreateInstance(cmd.Type, true); - - foreach (var action in actionsToPerform) - action(ret); - - Type[] typeParam; - ConsoleColoredString error = null; - if (cmd.Type.TryGetGenericParameters(typeof(ICommandLineValidatable<>), out typeParam)) - { - var tp = typeof(ICommandLineValidatable<>).MakeGenericType(typeParam[0]); - if (typeParam[0] != applicationTr.GetType()) - throw new CommandLineValidationException(@"The type {0} implements {1}, but ApplicationTr is of type {2}. If ApplicationTr is right, the interface implemented should be {3}.".Fmt( - cmd.Type.FullName, - tp.FullName, - applicationTr.GetType().FullName, - typeof(ICommandLineValidatable<>).MakeGenericType(applicationTr.GetType()).FullName - ), cmd); - - var meth = tp.GetMethod("Validate"); - if (meth == null || !meth.GetParameters().Select(p => p.ParameterType).SequenceEqual(new Type[] { typeParam[0] })) - throw new CommandLineValidationException(@"Couldn’t find the Validate method in the {0} type.".Fmt(tp.FullName), cmd); - - error = (ConsoleColoredString) meth.Invoke(ret, new object[] { applicationTr }); - } - else if (typeof(ICommandLineValidatable).IsAssignableFrom(cmd.Type)) - error = ((ICommandLineValidatable) ret).Validate(); - - if (error != null) - throw new CommandLineValidationException(error, cmd); - - return ret; - } - - internal static ConsoleColoredString GenerateHelp(CommandInfo cmd, int? wrapWidth = null, TranslationBase applicationTr = null, Translation tr = null, Func helpProcessor = null) - { - helpProcessor = helpProcessor ?? (s => s); - - if (tr == null) - tr = new Translation(); - - int leftMargin = 3; - var wrapToWidth = wrapWidth ?? ConsoleUtil.WrapToWidth(); - - var helpString = new List(); - var commandNameAttr = cmd.Type.GetCustomAttributes().FirstOrDefault(); - string commandName = commandNameAttr == null ? Process.GetCurrentProcess().ProcessName : "... " + commandNameAttr.Names.OrderByDescending(c => c.Length).First(); - - // - // ## CONSTRUCT THE “USAGE” LINE - // - var usage = new List(); - usage.Add(new ConsoleColoredString(tr.Usage + " ", CmdLineColor.UsageLinePrefix)); - usage.Add(commandName); - - // Options must be listed before positionals because if a positional is a subcommand, all the options must be before it. - // Optional positionals must come after mandatory positionals because that is the order they must be specified in. - // If any mandatory positional is a subcommand, then you can’t have any optional positionals anyway. - var elements = cmd.Elements.Order().ToArray(); - foreach (var elem in elements) - usage.Add(" " + elem.UsageString); - - // Word-wrap the usage line - foreach (var line in new ConsoleColoredString(usage.ToArray()).WordWrap(wrapToWidth, tr.Usage.Translation.Length + 1)) - { - helpString.Add(line); - helpString.Add(ConsoleColoredString.NewLine); - } - helpString.Add(ConsoleColoredString.NewLine); - - // - // ## CONSTRUCT THE TABLES - // - var anyCommandsWithSuboptions = false; - - // Word-wrap the documentation for the command (if any) - var doc = cmd.Type.GetDocumentation(cmd.Type, applicationTr, helpProcessor); - foreach (var line in doc.WordWrap(wrapToWidth)) - { - helpString.Add(line); - helpString.Add(ConsoleColoredString.NewLine); - } - - // Table of required parameters - if (elements.Any(e => e.IsMandatory)) - { - var requiredParamsTable = new TextTable { MaxWidth = wrapToWidth - leftMargin, ColumnSpacing = 3, RowSpacing = 1, LeftMargin = leftMargin }; - int requiredRow = 0; - foreach (var f in elements.Where(e => e.IsMandatory)) - anyCommandsWithSuboptions |= f.AddHelpRow(requiredParamsTable, ref requiredRow, applicationTr, helpProcessor); - - helpString.Add(ConsoleColoredString.NewLine); - helpString.Add(new ConsoleColoredString(tr.ParametersHeader, CmdLineColor.HelpHeading)); - helpString.Add(ConsoleColoredString.NewLine); - helpString.Add(ConsoleColoredString.NewLine); - requiredParamsTable.RemoveEmptyColumns(); - helpString.Add(requiredParamsTable.ToColoredString()); - } - - // Table of optional parameters - if (elements.Any(e => !e.IsMandatory)) - { - var optionalParamsTable = new TextTable { MaxWidth = wrapToWidth - leftMargin, ColumnSpacing = 3, RowSpacing = 1, LeftMargin = leftMargin }; - int optionalRow = 0; - foreach (var f in elements.Where(e => !e.IsMandatory)) - anyCommandsWithSuboptions |= f.AddHelpRow(optionalParamsTable, ref optionalRow, applicationTr, helpProcessor); - - helpString.Add(ConsoleColoredString.NewLine); - helpString.Add(new ConsoleColoredString(tr.OptionsHeader, CmdLineColor.HelpHeading)); - helpString.Add(ConsoleColoredString.NewLine); - helpString.Add(ConsoleColoredString.NewLine); - optionalParamsTable.RemoveEmptyColumns(); - helpString.Add(optionalParamsTable.ToColoredString()); - } - - // “This command accepts further arguments on the command line.” - if (anyCommandsWithSuboptions) - { - helpString.Add(ConsoleColoredString.NewLine); - foreach (var line in (new ConsoleColoredString("* ", CmdLineColor.SubcommandsPresentAsterisk) + ConsoleColoredString.FromEggsNode(EggsML.Parse(tr.AdditionalOptions.Translation))).WordWrap(wrapToWidth, 2)) - { - helpString.Add(line); - helpString.Add(ConsoleColoredString.NewLine); - } - } - - return new ConsoleColoredString(helpString.ToArray()); - } - - #region Post-build step check - - /// - /// Performs safety checks to ensure that the structure of your command-line syntax defining class is valid according - /// to the criteria laid out in the documentation of . Run this method as a post-build - /// step to ensure reliability of execution. For an example of use, see . - /// - /// The class containing the fields and attributes which define the command-line syntax. - /// - /// Object to report post-build errors to. - /// - /// The type of the translation object, derived from , which would be passed in for the - /// “applicationTr” parameter of at normal run-time. - public static void PostBuildStep(IPostBuildReporter rep, Type applicationTrType) - { - var type = typeof(TArgs); - if (!type.IsDefined()) - rep.Error(@"To use {0} as a command-line type, it must have the [CommandLine] attribute.".Fmt(type.FullName), (type.IsEnum ? "enum " : type.IsInterface ? "interface " : typeof(Delegate).IsAssignableFrom(type) ? "delegate " : type.IsValueType ? "struct " : "class ") + type.Name); - postBuildStep(rep, typeof(TArgs), applicationTrType, false); - } - - private static void postBuildStep(IPostBuildReporter rep, Type cmdType, Type applicationTrType, bool classDocRecommended) - { - if (!cmdType.IsClass) - rep.Error(@"{0} is not a class.".Fmt(cmdType.FullName), (cmdType.IsEnum ? "enum " : cmdType.IsInterface ? "interface " : typeof(Delegate).IsAssignableFrom(cmdType) ? "delegate " : "struct ") + cmdType.Name); - - object instance; - var type = cmdType; - try - { - if (type.IsAbstract) - type = type.Assembly.GetTypes().FirstOrDefault(t => t.IsClass && !t.IsAbstract && cmdType.IsAssignableFrom(t) && t.GetConstructor(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static, null, Type.EmptyTypes, null) != null); - if (type == null) - { - rep.Error(@"The class {0} does not have a derived non-abstract class type with a default constructor.".Fmt(cmdType.FullName), "class " + cmdType.Name); - return; - } - instance = Activator.CreateInstance(type, true); - } - catch (Exception e) - { - rep.Error(@"{0} could not be instantiated ({1}). Does it have a default constructor?".Fmt(type.FullName, e.Message), "class " + type.Name); - return; - } - - if (applicationTrType != null) - { - Type[] typeParam; - if (cmdType.TryGetGenericParameters(typeof(ICommandLineValidatable<>), out typeParam) && typeParam[0] != applicationTrType) - rep.Error(@"The type {0} implements {1}, but the ApplicationTr type is {2}. If ApplicationTr is right, the interface implemented should be {3}.".Fmt( - cmdType.FullName, - typeof(ICommandLineValidatable<>).MakeGenericType(typeParam[0]).FullName, - applicationTrType.FullName, - typeof(ICommandLineValidatable<>).MakeGenericType(applicationTrType).FullName - ), "class " + cmdType.Name); - } - - var optionTaken = new Dictionary(); - var sensibleDocMethods = new List(); - FieldInfo lastField = null; - bool haveSeenOptionalPositional = false; - - checkDocumentation(rep, cmdType, cmdType, applicationTrType, sensibleDocMethods, classDocRecommended); - - foreach (var field in getEligibleFields(cmdType)) - { - if (field.IsDefined()) - continue; - - // Every field must have one of the following - var positional = field.IsDefined(); - var options = field.GetOrderedOptionAttributeNames(); - var enumOpt = field.GetCustomAttributes().FirstOrDefault(); - - if (positional && lastField != null) - rep.Error(@"The type of {0}.{1} necessitates that no positional fields can follow it in the same class.".Fmt(lastField.DeclaringType.FullName, lastField.Name), "class " + cmdType.Name, field.Name); - - if (!positional && options == null && enumOpt == null) - { - rep.Error(@"{0}.{1}: Every field must have one of the following attributes: [IsPositional], [Option], [EnumOptions] (fields of an enum type only), or [Ignore].".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - continue; - } - - // EnumOptionsAttribute can only be used on enum fields - if (enumOpt != null && !field.FieldType.IsEnum) - rep.Error(@"{0}.{1}: Cannot use [EnumOptions] attribute on a field whose type is not an enum type.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - // Can’t combine IsPositional and Option - else if (positional && options != null) - rep.Error(@"{0}.{1}: Cannot use [IsPositional] and [Option] attributes on the same field.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - // Can’t combine IsPositional and EnumOptions - else if (positional && enumOpt != null) - rep.Error(@"{0}.{1}: Cannot use [IsPositional] and [EnumOptions] attributes on the same field. For a positional enum value, use only [IsPositional].".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - // Can’t have [Option] without an option name - else if (options != null && options.Length == 0) - rep.Error(@"{0}.{1}: An [Option] attribute must specify at least one option name.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - - // Option names must start with a dash - if (options != null && options.Any(o => !o.StartsWith('-'))) - rep.Error(@"{0}.{1}: All names in an [Option] attribute must start with at least one dash ('-'). Offending option name: ""{2}""".Fmt(field.DeclaringType.FullName, field.Name, options.First(o => !o.StartsWith('-'))), "class " + cmdType.Name, field.Name); - - var mandatory = field.IsDefined(); - - if (mandatory && field.IsDefined()) - rep.Error(@"{0}.{1}: Fields cannot simultaneously be mandatory and also undocumented.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - - if (positional && mandatory && haveSeenOptionalPositional) - rep.Error(@"{0}.{1}: Positional fields can only be marked mandatory if all preceding positional fields are also marked mandatory.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - else if (positional && !mandatory) - haveSeenOptionalPositional = true; - - // ### ENUM fields - if (field.FieldType.IsEnum) - { - // Can’t have a mandatory or a positional multi-value enum - if (mandatory && enumOpt != null && enumOpt.Behavior == EnumBehavior.MultipleValues) - rep.Error(@"{0}.{1}: A mandatory enum field cannot use multi-value behavior.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - if (positional && enumOpt != null && enumOpt.Behavior == EnumBehavior.MultipleValues) - rep.Error(@"{0}.{1}: A positional enum field cannot use multi-value behavior.".Fmt(field.DeclaringType.FullName, field.Name), "class " + cmdType.Name, field.Name); - - var commandsTaken = new Dictionary(); - var defaultValue = field.GetValue(instance); - - foreach (var enumField in field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public)) - { - if (enumField.IsDefined()) - continue; - // If the field is not mandatory, it is allowed to have a default value - if (!mandatory && enumField.GetValue(null).Equals(defaultValue)) - continue; - - // check that the enum values all have documentation - checkDocumentation(rep, enumField, cmdType, applicationTrType, sensibleDocMethods, true); - - if (options != null || positional) - { - // check that the enum values all have at least one CommandName, and they do not clash - var cmdNames = enumField.GetCustomAttributes().FirstOrDefault(); - if (cmdNames == null || cmdNames.Names.Length == 0) - rep.Error(@"{0}.{1} (used by {2}.{3}): Enum value must have a [CommandName] attribute (unless it is the field's default value and the field is optional).".Fmt(field.FieldType.FullName, enumField.Name, cmdType.FullName, field.Name), "enum " + field.FieldType.Name, enumField.Name); - else - checkCommandNamesUnique(rep, cmdNames.Names, commandsTaken, cmdType, field, enumField); - } - else - { - // check that the non-default enum values’ Options are present and do not clash - var optionNames = enumField.GetOrderedOptionAttributeNames(); - if (optionNames == null || !optionNames.Any()) - rep.Error(@"{0}.{1} (used by {2}.{3}): Enum value must have an [Option] attribute with at least one option name (unless it is the field's default value and the field is optional).".Fmt(field.FieldType.FullName, enumField.Name, cmdType.FullName, field.Name), "enum " + field.FieldType.Name, enumField.Name); - else - checkOptionsUnique(rep, optionNames, optionTaken, cmdType, field, enumField); - } - } - - // If the enum field has an Option attribute, it needs documentation too - if (options != null) - checkDocumentation(rep, field, cmdType, applicationTrType, sensibleDocMethods, true); - } - // ### BOOL fields - else if (field.FieldType == typeof(bool)) - { - if (positional || mandatory) - rep.Error(@"{0}.{1}: Fields of type bool cannot be positional or mandatory.".Fmt(cmdType.FullName, field.Name), "class " + cmdType.Name, field.Name); - else - // Here we have checked that the field is not positional, not an enum, and not [Ignore]’d, so it must have an [Option] attribute - checkOptionsUnique(rep, options, optionTaken, cmdType, field); - checkDocumentation(rep, field, cmdType, applicationTrType, sensibleDocMethods, true); - } - // ### STRING, STRING[], INTEGER and FLOATING fields (including nullable) - else if (field.FieldType == typeof(string) || field.FieldType == typeof(string[]) || - (ExactConvert.IsTrueIntegerType(field.FieldType) && !field.FieldType.IsEnum) || - (ExactConvert.IsTrueIntegerNullableType(field.FieldType) && !field.FieldType.GetGenericArguments()[0].IsEnum) || - field.FieldType == typeof(float) || field.FieldType == typeof(float?) || field.FieldType == typeof(double) || field.FieldType == typeof(double?)) - { - // options is null if and only if this field is positional - if (options != null) - checkOptionsUnique(rep, options, optionTaken, cmdType, field); - checkDocumentation(rep, field, cmdType, applicationTrType, sensibleDocMethods, true); - - // A positional string[] can only be the last field - if (positional && field.FieldType == typeof(string[])) - lastField = field; - } - else - rep.Error(@"{0}.{1} is not of a supported type. Currently accepted types are: enum types, bool, string, string[], numeric types (byte, sbyte, short, ushort, int, uint, long, ulong, float and double) and nullable numeric types.".Fmt(cmdType.FullName, field.Name), "class " + cmdType.Name, field.Name); - } - - // Check for derived classes - var subcommandsTaken = new Dictionary(); - var anyDerived = false; - foreach (var derivedType in getDirectSubcommands(cmdType)) - { - anyDerived = true; - checkCommandNamesUnique(rep, derivedType.GetCustomAttributes().First().Names, subcommandsTaken, derivedType); - postBuildStep(rep, derivedType, applicationTrType, true); - } - - if (anyDerived && lastField != null) - rep.Error(@"The type of {0}.{1} precludes the use of subcommands.".Fmt(lastField.DeclaringType.FullName, lastField.Name), "class " + cmdType.Name, lastField.Name); - - if (applicationTrType != null) - // Warn if the class has unused documentation methods - foreach (var meth in cmdType.GetMethods(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic).Where(m => m.Name.EndsWith("Doc") && m.ReturnType == typeof(string) && m.GetParameters().Select(p => p.ParameterType).SequenceEqual(new Type[] { applicationTrType }))) - if (!sensibleDocMethods.Contains(meth)) - rep.Error(@"{0}.{1} looks like a documentation method, but has no corresponding field, or the corresponding field does not require documentation because it is a positional enum or has an [EnumOptions] attribute.".Fmt(cmdType.FullName, meth.Name), "class " + cmdType.Name, meth.Name); - } - - private static void checkOptionsUnique(IPostBuildReporter rep, IEnumerable options, Dictionary optionTaken, Type type, FieldInfo field, FieldInfo enumField) - { - foreach (var option in options) - { - if (optionTaken.ContainsKey(option)) - { - rep.Error(@"{0}.{1}: Option ""{2}"" is used more than once.".Fmt(field.FieldType.FullName, enumField.Name, option), "enum " + field.FieldType.Name, enumField.Name); - rep.Error(@" -- It is used by {0}.{1}...".Fmt(type.FullName, field.Name), "class " + type.Name, field.Name); - rep.Error(@" -- ... and by {0}.{1}.".Fmt(optionTaken[option].DeclaringType.FullName, optionTaken[option].Name), "class " + optionTaken[option].DeclaringType.Name, optionTaken[option].Name); - } - optionTaken[option] = field; - } - } - - private static void checkOptionsUnique(IPostBuildReporter rep, IEnumerable options, Dictionary optionTaken, Type type, FieldInfo field) - { - foreach (var option in options) - { - if (optionTaken.ContainsKey(option)) - { - rep.Error(@"Option ""{2}"" is used by {0}.{1}...".Fmt(type.FullName, field.Name, option), "class " + type.Name, field.Name); - rep.Error(@" -- ... and by {0}.{1}.".Fmt(optionTaken[option].DeclaringType.FullName, optionTaken[option].Name), "class " + optionTaken[option].DeclaringType.Name, optionTaken[option].Name); - } - optionTaken[option] = field; - } - } - - private static void checkCommandNamesUnique(IPostBuildReporter rep, string[] commandNames, Dictionary commandsTaken, Type subclass) - { - foreach (var cmd in commandNames) - { - if (commandsTaken.ContainsKey(cmd)) - { - rep.Error(@"CommandName ""{1}"" is used by {0}...".Fmt(subclass.FullName, cmd), "class " + subclass.Name); - rep.Error(@" -- ... and by {0}.".Fmt(commandsTaken[cmd].FullName), "class " + commandsTaken[cmd].Name); - } - commandsTaken[cmd] = subclass; - } - } - - private static void checkCommandNamesUnique(IPostBuildReporter rep, string[] commandNames, Dictionary commandsTaken, Type type, FieldInfo field, FieldInfo enumField) - { - foreach (var cmd in commandNames) - { - if (commandsTaken.ContainsKey(cmd)) - { - rep.Error(@"{0}.{1}: Option ""{2}"" is used more than once.".Fmt(field.FieldType.FullName, enumField.Name, cmd), "enum " + field.FieldType.Name, enumField.Name); - rep.Error(@" -- It is used by {0}.{1}...".Fmt(type.FullName, field.Name), "class " + type.Name, field.Name); - rep.Error(@" -- ... and by {0}.{1}.".Fmt(commandsTaken[cmd].DeclaringType.FullName, commandsTaken[cmd].Name), "class " + commandsTaken[cmd].DeclaringType.Name, commandsTaken[cmd].Name); - } - commandsTaken[cmd] = enumField; - } - } - - private static Dictionary _applicationTrCacheField = null; - private static Dictionary _applicationTrCache - { - get - { - if (_applicationTrCacheField == null) - _applicationTrCacheField = new Dictionary(); - return _applicationTrCacheField; - } - } - - private static void checkDocumentation(IPostBuildReporter rep, MemberInfo member, Type inType, Type applicationTrType, List sensibleDocMethods, bool classDocRecommended) - { - if (member.IsDefined()) - return; - - if (!(member is Type) && inType.IsSubclassOf(member.DeclaringType)) - inType = member.DeclaringType; - - var attr = member.GetCustomAttributes().FirstOrDefault(); - ConsoleColoredString toCheck = null; - if (attr != null) - { - try - { - toCheck = attr.Text; // this property can throw the first time it's accessed - } - catch (Exception e) - { - if (member is Type) - rep.Error(@"{0}: Type documentation could not be parsed as {1}: {2}".Fmt(((Type) member).FullName, attr.OriginalFormat, e.Message), "class " + member.Name); - else - rep.Error(@"{0}.{1}: Field documentation could not be parsed as {2}: {3}".Fmt(member.DeclaringType.FullName, member.Name, attr.OriginalFormat, e.Message), "class " + member.DeclaringType.Name, member.Name); - return; - } - } - else if (applicationTrType != null) - { - var meth = inType.GetMethod(member.Name + "Doc", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { applicationTrType }, null); - if (meth != null && meth.ReturnType == typeof(string)) - { - sensibleDocMethods.Add(meth); - if (!_applicationTrCache.ContainsKey(applicationTrType)) - _applicationTrCache[applicationTrType] = Activator.CreateInstance(applicationTrType); - var appTr = _applicationTrCache[applicationTrType]; - toCheck = (string) meth.Invoke(null, new object[] { appTr }); - if (toCheck == null) - { - rep.Error(@"{0}." + member.Name + @"Doc() returned null.".Fmt(inType.FullName), "class " + inType.Name, member.Name + "Doc"); - return; - } - } - } - - if (classDocRecommended && toCheck == null) - { - if (member is Type) - { - rep.Warning((@"{0} does not have any documentation. " + - (applicationTrType == null ? "U" : @"To provide localised documentation, declare a method ""static string {1}Doc({2})"" on {3}. Otherwise, u") + - @"se the [DocumentationLiteral] attribute to specify unlocalisable documentation. " + - @"Use [Undocumented] to completely hide an option or command from the help screen.").Fmt(((Type) member).FullName, member.Name, applicationTrType != null ? applicationTrType.FullName : null, inType.FullName), - ((Type) member).Namespace, - "CommandName", - "class " + member.Name); - } - else - { - rep.Warning((@"{0}.{1} does not have any documentation. " + - (applicationTrType == null ? "U" : @"To provide localised documentation, declare a method ""static string {1}Doc({2})"" on {3}. Otherwise, u") + - @"se the [DocumentationLiteral] attribute to specify unlocalisable documentation. " + - @"Use [Undocumented] to completely hide an option or command from the help screen.").Fmt(member.DeclaringType.FullName, member.Name, applicationTrType != null ? applicationTrType.FullName : null, inType.FullName), - member.DeclaringType.Namespace, - (member.DeclaringType.IsEnum ? "enum " : member.DeclaringType.IsValueType ? "struct " : "class ") + member.DeclaringType.Name, - member.Name); - } - return; - } - } - - #endregion - - /// - /// Converts the specified parse tree into a console colored string according to - /// CommandLineParser-specific rules. This method is used to convert - /// documentation into colored text. See Remarks. - /// - /// A number of named tags have a special meaning. Any tag named after a value of results - /// in that color. Both spellings of gray/grey are supported. The {h}...{} named tag stands for the highlight color - /// (white). {nowrap}...{} can be placed around text that must not be broken into multiple lines by the word wrapper. - /// The tags {field}, {option}, {command} and {enum} are used to refer to the corresponding command line syntax - /// element, and is highlighted the same way the documentation generator would highlight references to these entities. - public static ConsoleColoredString Colorize(RhoElement text) - { - var strings = new List(); - if (text.Name == null) - colorizeChildren(text, strings, ConsoleColor.Gray, false); - else - colorizeWalk(text, strings, ConsoleColor.Gray, false); - return new ConsoleColoredString(strings); - } - - /// - /// Converts the specified parse tree into a console colored string using the rules described in - /// . This method is used to convert documentation into colored text, as well as any documentation using the - /// legacy . - public static ConsoleColoredString Colorize(EggsNode text) - { - return text.ToConsoleColoredStringWordWrap(int.MaxValue).JoinColoredString(Environment.NewLine); - } - - private static void colorizeChildren(RhoElement text, List strings, ConsoleColor curColor, bool curNowrap) - { - foreach (var child in text.Children) - { - if (child is RhoText) - strings.Add(nowrap((child as RhoText).Text, curNowrap).Color(curColor)); - else - colorizeWalk(child as RhoElement, strings, curColor, curNowrap); - } - } - - private static void colorizeWalk(RhoElement text, List strings, ConsoleColor curColor, bool curNowrap) - { - var name = text.Name.ToLower(); - if (name == "field") - { - validateNoAttributes(text); - validateOnlyTextChild(text); - strings.Add("<".Color(CmdLineColor.FieldBrackets) + nowrap((text.Children[0] as RhoText).Text).Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - else if (name == "option") - { - validateNoAttributes(text); - validateOnlyTextChild(text); - strings.Add(nowrap((text.Children[0] as RhoText).Text).Color(CmdLineColor.Option)); - } - else if (name == "command") - { - validateNoAttributes(text); - validateOnlyTextChild(text); - strings.Add(nowrap((text.Children[0] as RhoText).Text).Color(CmdLineColor.Command)); - } - else if (name == "enum") - { - validateNoAttributes(text); - validateOnlyTextChild(text); - strings.Add(nowrap((text.Children[0] as RhoText).Text).Color(CmdLineColor.EnumValue)); - } - else if (name == "nowrap") - { - validateNoAttributes(text); - colorizeChildren(text, strings, curColor, true); - } - else if (name == "n") // newline - { - validateNoAttributes(text); - validateNoChildren(text); - strings.Add("\n"); - } - else if (name == "h") // highlight - { - validateNoAttributes(text); - colorizeChildren(text, strings, CmdLineColor.Highlight, curNowrap); - } - else - { - if (!EnumStrong.TryParse(name, out curColor, true)) - { - if (name == "grey") - curColor = ConsoleColor.Gray; - else if (name == "darkgrey") - curColor = ConsoleColor.DarkGray; - else - throw new ArgumentException("Unsupported element: {0}.".Fmt(text.Name), "text"); - } - validateNoAttributes(text); - colorizeChildren(text, strings, curColor, curNowrap); - } - } - - private static string nowrap(string text, bool doNowrap = true) - { - if (doNowrap) - return text.Replace(' ', '\xA0'); // non-breaking space - else - return text; - } - - private static void validateNoAttributes(RhoElement text) - { - if (text.Value != null || text.Attributes.Any()) - throw new ArgumentException("Element {0} must not have any attributes.".Fmt(text.Name), "text"); - } - - private static void validateNoChildren(RhoElement text) - { - if (text.Children.Any()) - throw new ArgumentException("Element {0} must not have any child nodes.".Fmt(text.Name), "text"); - } - - private static void validateOnlyTextChild(RhoElement text) - { - if (text.Children.Count != 1 || !(text.Children[0] is RhoText)) - throw new ArgumentException("Element {0} must only contain text, and no other elements.".Fmt(text.Name), "text"); - } -} - -internal class CommandInfo -{ - public Type Type; - public CmdLineElement[] Elements; -} - -internal sealed class SubcommandInfo : CommandInfo -{ - public string[] Names; -} - -internal abstract class CmdLineElement : IComparable -{ - public bool IsMandatory; - public virtual bool IsPositional { get { return false; } } - public abstract bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd); - public abstract void ProcessEndOfParameters(List> actionsToPerform, CommandInfo cmd); - public abstract ConsoleColoredString UsageString { get; } - public abstract bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor); - - public int CompareTo(CmdLineElement other) - { - // Options must be listed before positionals because if a positional is a subcommand, all the options must be before it. - if (IsPositional && !other.IsPositional) - return 1; - if (!IsPositional && other.IsPositional) - return -1; - - // Optional positionals must come after mandatory positionals because that is the order they must be specified in. - // If any mandatory positional is a subcommand, then you can’t have any optional positionals anyway. - if (IsMandatory && !other.IsMandatory) - return -1; - if (!IsMandatory && other.IsMandatory) - return 1; - - return 0; - } - - protected ConsoleColoredString FormatField(string name) - { - return "<".Color(CmdLineColor.FieldBrackets) + name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets); - } - - protected static bool tryConvertString(string value, List> listToAddActionTo, FieldInfo field) - { - object result; - - if (field.FieldType == typeof(string)) - result = value; - else - { - Type type = field.FieldType.IsGenericType && field.FieldType.GetGenericTypeDefinition() == typeof(Nullable<>) - ? field.FieldType.GetGenericArguments()[0] - : field.FieldType; - if (!ExactConvert.Try(type, value, out result)) - return false; - } - listToAddActionTo.Add(obj => { field.SetValue(obj, result); }); - return true; - } -} - -internal abstract class CmdLineFieldPositional : CmdLineElement -{ - public override bool IsPositional { get { return true; } } - - public FieldInfo Field; - public override void ProcessEndOfParameters(List> actionsToPerform, CommandInfo cmd) - { - if (IsMandatory) - throw new MissingParameterException(Field, null, false, cmd); - } - public override ConsoleColoredString UsageString - { - get - { - return (IsMandatory ? "{0}" : "[{0}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - "<".Color(CmdLineColor.FieldBrackets) + Field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - } - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - table.SetCell(0, row, FormatField(Field.Name), noWrap: true, colSpan: 2); - table.SetCell(2, row, Field.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor), colSpan: 4); - row++; - return false; - } -} - -internal sealed class CmdLineEnumFieldPositional : CmdLineFieldPositional -{ - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - var name = args[i]; - if (name.StartsWith('-') && !suppressOptions) - return false; - foreach (var enumField in Field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public)) - { - if (enumField.GetCustomAttributes().First().Names.Any(c => c.Equals(name, StringComparison.OrdinalIgnoreCase))) - { - actionsToPerform.Add(obj => { Field.SetValue(obj, enumField.GetValue(null)); }); - i++; - return true; - } - } - throw new UnrecognizedCommandOrOptionException(args[i], cmd); - } - - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - var topRow = row; - var doc = Field.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor); - if (doc.Length > 0 || Field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public).All(el => el.IsDefined() || !el.GetCustomAttributes().Any())) - { - table.SetCell(2, row, doc, colSpan: 4); - row++; - } - foreach (var el in Field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public)) - { - if (el.IsDefined()) - continue; - var attr = el.GetCustomAttributes().FirstOrDefault(); - if (attr == null) // skip the default value - continue; - table.SetCell(2, row, attr.Names.Where(n => n.Length <= 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(", "), noWrap: true); - table.SetCell(3, row, attr.Names.Where(n => n.Length > 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(Environment.NewLine), noWrap: true); - table.SetCell(4, row, el.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor), colSpan: 2); - row++; - } - table.SetCell(0, topRow, FormatField(Field.Name), noWrap: true, colSpan: 2, rowSpan: row - topRow); - return false; - } -} - -internal abstract class CmdLineOption : CmdLineElement -{ - public string[] Options; -} - -internal abstract class CmdLineFieldOption : CmdLineOption -{ - public FieldInfo Field; - - public override void ProcessEndOfParameters(List> actionsToPerform, CommandInfo cmd) - { - if (IsMandatory) - throw new MissingParameterException(Field, null, true, cmd); - } - - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - table.SetCell(0, row, Field.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true); - table.SetCell(1, row, Field.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true); - table.SetCell(2, row, Field.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor), colSpan: 4); - row++; - return false; - } -} - -internal abstract class CmdLineEnumOption : CmdLineFieldOption -{ - public EnumBehavior Behavior; - public string AlreadyProcessedOptionOrCommand = null; - public object AlreadyProcessedValue = null; - - protected abstract object getValue(string[] args, ref int i, out string optionOrCommand, CommandInfo cmd); - - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - if (suppressOptions || !Options.Contains(args[i])) - return false; - string optionOrCommand; - var value = getValue(args, ref i, out optionOrCommand, cmd); - if (value == null) - return false; - if (Behavior == EnumBehavior.SingleValue) - { - if (AlreadyProcessedOptionOrCommand == null) - { - AlreadyProcessedOptionOrCommand = optionOrCommand; - AlreadyProcessedValue = value; - actionsToPerform.Add(obj => { Field.SetValue(obj, value); }); - } - else if (AlreadyProcessedValue.Equals(value)) - { - // Don’t throw an error if the same value is simply specified multiple times. Just ignore the second occurrence - } - else - { - // Since only a single value is allowed, throw an error if another value is specified later - throw new IncompatibleCommandOrOptionException(AlreadyProcessedOptionOrCommand, optionOrCommand, cmd); - } - } - else - { - if (AlreadyProcessedValue == null) - AlreadyProcessedValue = value; - else - // Bitwise OR - value = AlreadyProcessedValue = (dynamic) AlreadyProcessedValue | (dynamic) value; - - actionsToPerform.Add(obj => { Field.SetValue(obj, value); }); - } - return true; - } -} - -internal sealed class CmdLineEnumOptionWithNames : CmdLineEnumOption -{ - public Dictionary NameToValue; - - protected override object getValue(string[] args, ref int i, out string optionOrCommand, CommandInfo cmd) - { - i++; - if (i >= args.Length) - throw new IncompleteOptionException(args[i - 1], cmd); - optionOrCommand = args[i]; - object value; - if (!NameToValue.TryGetValue(optionOrCommand, out value)) - throw new UnrecognizedCommandOrOptionException(optionOrCommand, cmd); - i++; - return value; - } - - public override ConsoleColoredString UsageString - { - get - { - if (Behavior == EnumBehavior.MultipleValues) - { - // -t name [-t name [...]] — multi-value enums with CommandNames - return (IsMandatory ? "{0} {1} [{0} {1} [...]]" : "[{0} {1} [{0} {1} [...]]]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - Options[0].Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + Field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - else - { - // -t name - return (IsMandatory ? "{0} {1}" : "[{0} {1}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - Options[0].Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + Field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - } - } - - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - var topRow = row; - row++; - foreach (var el in Field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public).Where(e => !e.IsDefined())) - { - var attr = el.GetCustomAttributes().FirstOrDefault(); - if (attr == null) // skip the default value - continue; - table.SetCell(3, row, attr.Names.Where(n => n.Length <= 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(", "), noWrap: true); - table.SetCell(4, row, attr.Names.Where(n => n.Length > 2).Select(s => s.Color(CmdLineColor.EnumValue)).JoinColoredString(Environment.NewLine), noWrap: true); - table.SetCell(5, row, el.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor)); - row++; - } - if (row == topRow + 1) - throw new InvalidOperationException("Enum type {2}.{3} has no values (apart from default value for field {0}.{1}).".Fmt(Field.DeclaringType.FullName, Field.Name, Field.FieldType.DeclaringType.FullName, Field.FieldType)); - table.SetCell(0, topRow, Field.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true, rowSpan: row - topRow); - table.SetCell(1, topRow, Field.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true, rowSpan: row - topRow); - table.SetCell(2, topRow, Field.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor), colSpan: 4); - table.SetCell(2, topRow + 1, FormatField(Field.Name), noWrap: true, rowSpan: row - topRow - 1); - return false; - } -} - -internal sealed class CmdLineEnumOptions : CmdLineEnumOption -{ - public Dictionary OptionToValue; - - protected override object getValue(string[] args, ref int i, out string optionOrCommand, CommandInfo cmd) - { - optionOrCommand = args[i]; - i++; - return OptionToValue[optionOrCommand]; - } - - public override ConsoleColoredString UsageString - { - get - { - var options = Field.FieldType.GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(fld => fld.IsDefined() && !fld.IsDefined()) - .Select(fi => fi.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option)) - .ToArray(); - - if (Behavior == EnumBehavior.MultipleValues) - // [-t] [-u] [-v] — multi-value enums with Option names - return options.Select(opt => "[{0}]".Color(CmdLineColor.OptionalityDelimiters).Fmt(opt)).JoinColoredString(" "); - - // {-t|-u} — single-value enums with Options - return (IsMandatory ? (options.Length > 1 ? "{{{0}{1}" : "{0}") : "[{0}]").Color(CmdLineColor.OptionalityDelimiters).Fmt(options.JoinColoredString("|".Color(CmdLineColor.OptionalityDelimiters)), "}"); - } - } - - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - foreach (var el in Field.FieldType.GetFields(BindingFlags.Static | BindingFlags.Public).Where(e => e.IsDefined() && !e.IsDefined())) - { - table.SetCell(0, row, el.GetOrderedOptionAttributeNames().Where(o => !o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(", "), noWrap: true); - table.SetCell(1, row, el.GetOrderedOptionAttributeNames().Where(o => o.StartsWith("--")).OrderBy(cmd => cmd.Length).Select(cmd => cmd.Color(CmdLineColor.Option)).JoinColoredString(Environment.NewLine), noWrap: true); - table.SetCell(2, row, el.GetDocumentation(Field.DeclaringType, applicationTr, helpProcessor), colSpan: 4); - row++; - } - return false; - } -} - -internal sealed class CmdLineBoolOption : CmdLineFieldOption -{ - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - if (suppressOptions || !Options.Contains(args[i])) - return false; - actionsToPerform.Add(obj => { Field.SetValue(obj, true); }); - i++; - return true; - } - - public override ConsoleColoredString UsageString - { - get - { - // [-t] - return "[{0}]".Color(CmdLineColor.OptionalityDelimiters).Fmt(Field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option)); - } - } -} - -// Covers string, integers, float/double, and their nullables -internal sealed class CmdLineOtherOption : CmdLineFieldOption -{ - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - var optionName = args[i]; - if (suppressOptions || !Options.Contains(optionName)) - return false; - - i++; - if (i >= args.Length) - throw new IncompleteOptionException(optionName, cmd); - - if (!tryConvertString(args[i], actionsToPerform, Field)) - throw new InvalidNumericParameterException(Field.Name, cmd); - - i++; - return true; - } - - public override ConsoleColoredString UsageString - { - get - { - // -t name - return (IsMandatory ? "{0} {1}" : "[{0} {1}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - Field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + Field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - } -} - -// Covers string, integers, float/double, and their nullables -internal sealed class CmdLineOtherPositional : CmdLineFieldPositional -{ - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - if (!tryConvertString(args[i], actionsToPerform, Field)) - throw new InvalidNumericParameterException(Field.Name, cmd); - i++; - return true; - } -} - -internal sealed class CmdLineStringArrayOption : CmdLineFieldOption -{ - private bool Already = false; - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - var optionName = args[i]; - if (suppressOptions || !Options.Contains(optionName)) - return false; - - i++; - if (i >= args.Length) - throw new IncompleteOptionException(optionName, cmd); - - var value = args[i]; - if (Already) - actionsToPerform.Add(obj => { Field.SetValue(obj, ((string[]) Field.GetValue(obj)).Concat(value).ToArray()); }); - else - { - actionsToPerform.Add(obj => { Field.SetValue(obj, new string[] { value }); }); - Already = true; - } - - i++; - return true; - } - - public override ConsoleColoredString UsageString - { - get - { - // -t name [-t name [...]] - return (IsMandatory ? "{0} {1} [{0} {1} [...]]" : "[{0} {1} [{0} {1} [...]]]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - Field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + Field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - } -} - -internal sealed class CmdLineStringArrayPositional : CmdLineFieldPositional -{ - private List Already = null; - - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - if (Already == null) - Already = new List(); - Already.Add(args[i]); - i++; - return true; - } - - public override void ProcessEndOfParameters(List> actionsToPerform, CommandInfo cmd) - { - if (IsMandatory && Already == null) - throw new MissingParameterException(Field, null, false, cmd); - actionsToPerform.Add(obj => { Field.SetValue(obj, Already == null ? new string[] { } : Already.ToArray()); }); - } -} - -internal sealed class CmdLineSubcommand : CmdLineElement -{ - public override bool IsPositional { get { return true; } } - - public Type Type; - public SubcommandInfo[] Subcommands; - public SubcommandInfo Subcommand; - - public override bool ProcessParameter(string[] args, ref int i, List> actionsToPerform, bool suppressOptions, Func helpProcessor, CommandInfo cmd) - { - var name = args[i]; - Subcommand = Subcommands.FirstOrDefault(s => s.Names.Contains(name)); - if (Subcommand == null) - throw new UnrecognizedCommandOrOptionException(name, cmd); - i++; - return true; - } - - public override void ProcessEndOfParameters(List> actionsToPerform, CommandInfo cmd) - { - if (IsMandatory) - throw new MissingSubcommandException(cmd); - } - - public override ConsoleColoredString UsageString - { - get - { - return (IsMandatory ? "{0}" : "[{0}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - "<".Color(CmdLineColor.FieldBrackets) + "...".Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - } - - public override bool AddHelpRow(TextTable table, ref int row, TranslationBase applicationTr, Func helpProcessor) - { - var anyCommandsWithSuboptions = false; - int origRow = row; - foreach (var subcmd in Subcommands) - { - var cell1 = ConsoleColoredString.Empty; - var cell2 = ConsoleColoredString.Empty; - anyCommandsWithSuboptions |= subcmd.Elements.Length > 0; - var asterisk = subcmd.Elements.Length > 0 ? "*".Color(CmdLineColor.SubcommandsPresentAsterisk) : ConsoleColoredString.Empty; - table.SetCell(2, row, subcmd.Names.Where(n => n.Length <= 2).Select(n => n.Color(CmdLineColor.Command) + asterisk).JoinColoredString(", "), noWrap: true); - table.SetCell(3, row, subcmd.Names.Where(n => n.Length > 2).Select(n => n.Color(CmdLineColor.Command) + asterisk).JoinColoredString(Environment.NewLine), noWrap: true); - table.SetCell(4, row, subcmd.Type.GetDocumentation(subcmd.Type, applicationTr, helpProcessor), colSpan: 2); - row++; - } - table.SetCell(0, origRow, FormatField("..."), colSpan: 2, rowSpan: row - origRow, noWrap: true); - return anyCommandsWithSuboptions; - } -} - -internal static class CmdLineColor -{ - public const ConsoleColor Option = ConsoleColor.Yellow; - public const ConsoleColor FieldBrackets = ConsoleColor.DarkCyan; - public const ConsoleColor Field = ConsoleColor.Cyan; - public const ConsoleColor Command = ConsoleColor.Green; - public const ConsoleColor EnumValue = ConsoleColor.Green; - public const ConsoleColor UsageLinePrefix = ConsoleColor.Green; - public const ConsoleColor OptionalityDelimiters = ConsoleColor.DarkGray; // e.g. [foo|bar] has [, ] and | in this color - public const ConsoleColor SubcommandsPresentAsterisk = ConsoleColor.DarkYellow; - public const ConsoleColor UnexpectedArgument = ConsoleColor.Magenta; - public const ConsoleColor Error = ConsoleColor.Red; - public const ConsoleColor HelpHeading = ConsoleColor.White; - public const ConsoleColor Highlight = ConsoleColor.White; -} - -/// -/// Contains methods to validate a set of parameters passed by the user on the command-line and parsed by . Use this class only in monolingual (unlocalisable) applications. Use otherwise. -public interface ICommandLineValidatable -{ - /// - /// When overridden in a derived class, returns an error message if the contents of the class are invalid, otherwise - /// returns null. - ConsoleColoredString Validate(); -} - -/// -/// Contains methods to validate a set of parameters passed by the user on the command-line and parsed by . -/// -/// A translation-string class containing the error messages that can occur during validation. -public interface ICommandLineValidatable where TTranslation : TranslationBase -{ - /// - /// When implemented in a class, returns an error message if the contents of the class are invalid, otherwise returns - /// null. - /// - /// Contains translations for the messages that may occur during validation. - ConsoleColoredString Validate(TTranslation tr); -} - -/// Groups the translatable strings in the class into categories. -public enum TranslationGroup -{ - /// Error messages produced by the command-line parser. - [LingoGroup("Command-line errors", "Contains messages informing the user of invalid command-line syntax.")] - CommandLineError, - /// Messages used by the command-line parser to produce help pages. - [LingoGroup("Command-line help", "Contains messages used to construct help pages for command-line options and parameters.")] - CommandLineHelp -} - -/// Contains translatable strings pertaining to the command-line parser, including error messages and usage help. -public sealed class Translation : TranslationBase -{ -#pragma warning disable 1591 // Missing XML comment for publicly visible type or member - public Translation() : base(Language.EnglishUS) { } - - [LingoInGroup(TranslationGroup.CommandLineError)] - public TrString - IncompatibleCommandOrOption = @"The command or option, {0}, cannot be used in conjunction with {1}. Please specify only one of the two.", - IncompleteOption = @"The {0} option must be followed by an additional parameter.", - InvalidNumber = @"The {0} option expects a number. The specified parameter does not constitute a valid number.", - MissingOption = @"The option {0} is mandatory and must be specified.", - MissingOptionBefore = @"The option {0} is mandatory and must be specified before the {1} parameter.", - MissingParameter = @"The parameter {0} is mandatory and must be specified.", - MissingParameterBefore = @"The parameter {0} is mandatory and must be specified before the {1} parameter.", - MissingSubcommand = @"The command line options must be followed by a command name.", - UnexpectedParameter = @"Unexpected parameter: {0}", - UnrecognizedCommandOrOption = @"The specified command or option, {0}, is not recognized.", - UserRequestedHelp = @"The user has requested help using one of the help options."; - - [LingoInGroup(TranslationGroup.CommandLineHelp)] - public TrString - AdditionalOptions = @"This command accepts further arguments on the command line. Type the command followed by *-?* or *help* to list them.", - Error = @"Error:", - OptionsHeader = @"Optional parameters:", - ParametersHeader = @"Required parameters:", - Usage = @"Usage:"; - -#pragma warning restore 1591 // Missing XML comment for publicly visible type or member -} - -/// Use this on a class to specify that it represent a command-line syntax. -[AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = false)] -public sealed class CommandLineAttribute : Attribute -{ - /// Constructor. - public CommandLineAttribute() { } -} - -/// -/// Use this on a derived class or on an enum value to specify the command the user must use to invoke that class or enum -/// value. -[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public sealed class CommandNameAttribute : Attribute -{ - /// - /// Constructor. - /// - /// The command(s) the user can specify to invoke this class or enum value. - public CommandNameAttribute(params string[] names) { Names = names; } - /// The command the user can specify to invoke this class. - public string[] Names { get; private set; } -} - -/// Use this to specify that a command-line parameter is mandatory. -[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public sealed class IsMandatoryAttribute : Attribute -{ - /// Constructor. - public IsMandatoryAttribute() { } -} - -/// -/// Use this to specify that a command-line parameter is positional, i.e. is not invoked by an option that starts with -/// "-". -[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public sealed class IsPositionalAttribute : Attribute -{ - /// Constructor. - public IsPositionalAttribute() { } -} - -/// -/// Use this to specify that a field in a class can be specified on the command line using an option, for example -/// -a or --option-name. The option name(s) MUST begin with a dash (-). -[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public sealed class OptionAttribute : Attribute -{ - /// - /// Constructor. - /// - /// The name of the option. Specify several names as synonyms if required. - public OptionAttribute(params string[] names) { Names = names; } - /// All of the names of the option. - public string[] Names { get; private set; } -} - -/// -/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable -/// for single-language applications only. See Remarks. -/// -/// This attribute specifies the documentation in plain text. All characters are printed exactly as specified. You may -/// wish to use to specify documentation with special markup for -/// command-line-related concepts, as well as for an alternative markup -/// language without command-line specific concepts. -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public class DocumentationAttribute : Attribute -{ - /// - /// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed - /// where applicable. - public virtual ConsoleColoredString Text { get { return OriginalText; } } - /// Gets a string describing the documentation format to the programmer (not seen by the users). - public virtual string OriginalFormat { get { return "Plain text"; } } - /// Gets the original documentation string exactly as specified in the attribute. - public string OriginalText { get; private set; } - - /// Constructor. - public DocumentationAttribute(string documentation) - { - OriginalText = documentation; - } -} - -/// -/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable -/// for single-language applications only. The documentation is to be specified in , which is -/// interpreted as described in . See also . -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public class DocumentationRhoMLAttribute : DocumentationAttribute -{ - /// Gets a string describing the documentation format to the programmer (not seen by the users). - public override string OriginalFormat { get { return "RhoML"; } } - /// - /// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed - /// where applicable. - public override ConsoleColoredString Text - { - get { return _parsed ?? (_parsed = CommandLineParser.Colorize(RhoML.Parse(OriginalText))); } - } - private ConsoleColoredString _parsed; - /// Constructor. - public DocumentationRhoMLAttribute(string documentation) : base(documentation) { } -} - -/// -/// Use this attribute to link a command-line option or command with the help text that describes (documents) it. Suitable -/// for single-language applications only. The documentation is to be specified in , which is -/// interpreted as described in . See also and . -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public class DocumentationEggsMLAttribute : DocumentationAttribute -{ - /// Gets a string describing the documentation format to the programmer (not seen by the users). - public override string OriginalFormat { get { return "EggsML"; } } - /// - /// Gets the console-colored documentation string. Note that this property may throw if the text couldn't be parsed - /// where applicable. - public override ConsoleColoredString Text - { - get { return _parsed ?? (_parsed = CommandLineParser.Colorize(EggsML.Parse(OriginalText))); } - } - private ConsoleColoredString _parsed; - /// Constructor. - public DocumentationEggsMLAttribute(string documentation) : base(documentation) { } -} - -/// -/// This is a legacy attribute. Do not use in new programs. This attribute is equivalent to . -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class | AttributeTargets.Method, Inherited = false, AllowMultiple = false), RummageKeepUsersReflectionSafe] -public class DocumentationLiteralAttribute : DocumentationEggsMLAttribute -{ - /// Constructor. - public DocumentationLiteralAttribute(string documentation) : base(documentation) { } -} - -/// -/// Specifies that a specific command-line option should not be printed in help pages, i.e. the option should explicitly -/// be undocumented. -[AttributeUsage(AttributeTargets.Field | AttributeTargets.Class, Inherited = false, AllowMultiple = true)] -public sealed class UndocumentedAttribute : Attribute -{ - /// Constructor. - public UndocumentedAttribute() { } -} - -/// Describes the behavior of an enum-typed field with the . -public enum EnumBehavior -{ - /// Specifies that an enum is considered to represent a single value. - SingleValue, - /// Specifies that an enum is considered to represent a bitfield containing multiple values. - MultipleValues -} - -/// -/// Specifies that a field of an enum type should be interpreted as multiple possible options, each specified by an on the enum values in the enum type. -[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] -public sealed class EnumOptionsAttribute : Attribute -{ - /// Constructor. - public EnumOptionsAttribute(EnumBehavior behavior) { Behavior = behavior; } - - /// - /// Specifies whether the enum is considered to represent a single value or a bitfield containing multiple values. - public EnumBehavior Behavior { get; private set; } -} - -/// Specifies that the command-line parser should ignore a field. -[AttributeUsage(AttributeTargets.Field, Inherited = false, AllowMultiple = false)] -public sealed class IgnoreAttribute : Attribute -{ - /// Constructor. - public IgnoreAttribute() { } -} - -/// Represents any error encountered while parsing a command line. This class is abstract. -[Serializable] -public abstract class CommandLineParseException : TranslatableException -{ - /// Specifies the command-line type for which a help screen is to be output to the user on the console. - internal CommandInfo CommandInfo { get; private set; } - - /// Contains the error message that describes the cause of this exception. - public Func GetColoredMessage { get; private set; } - - /// - /// Generates a printable description of the error represented by this exception, typically used to tell the user what - /// they did wrong. - /// - /// The translation class containing the translated text, or null for English. - /// - /// The character width at which the output should be word-wrapped. The default (null) uses . - public ConsoleColoredString GenerateErrorText(Translation tr, int? wrapWidth = null) - { - if (tr == null) - tr = new Translation(); - - var strings = new List(); - var message = tr.Error.Translation.Color(CmdLineColor.Error) + " " + GetColoredMessage(tr); - foreach (var line in message.WordWrap(wrapWidth ?? ConsoleUtil.WrapToWidth(), tr.Error.Translation.Length + 1)) - { - strings.Add(line); - strings.Add(Environment.NewLine); - } - return new ConsoleColoredString(strings); - } - - /// Constructor. - internal CommandLineParseException(Func getMessage, CommandInfo commandInfo) : this(getMessage, commandInfo, null) { } - /// Constructor. - internal CommandLineParseException(Func getMessage, CommandInfo commandInfo, Exception inner) - : base(tr => getMessage(tr).ToString(), inner) - { - CommandInfo = commandInfo; - GetColoredMessage = getMessage; - } - - /// - /// Prints usage information, followed by an error message describing to the user what it was that the parser didn't - /// understand. - /// - /// An object containing translations for the documentation strings. Set this to null only if your application - /// is definitely monolingual (unlocalisable). - /// - /// Contains translations for the messages used by the command-line parser. Set this to null only if your - /// application is definitely monolingual (unlocalisable). - /// - /// Specifies a callback which is invoked on every documentation string retrieved from the s to generate the help text. This callback can modify the text arbitrarily. - public virtual void WriteUsageInfoToConsole(TranslationBase applicationTr = null, Translation tr = null, Func helpProcessor = null) - { - ConsoleUtil.Write(GetUsageInfo(applicationTr, tr, helpProcessor)); - } - - /// - /// Generates and returns usage information, followed by an error message describing to the user what it was that the - /// parser didn't understand. - /// - /// An object containing translations for the documentation strings. Set this to null only if your application - /// is definitely monolingual (unlocalisable). - /// - /// Contains translations for the messages used by the command-line parser. Set this to null only if your - /// application is definitely monolingual (unlocalisable). - /// - /// Specifies a callback which is invoked on every documentation string retrieved from the s to generate the help text. This callback can modify the text arbitrarily. - public ConsoleColoredString GetUsageInfo(TranslationBase applicationTr = null, Translation tr = null, Func helpProcessor = null) - { - if (tr == null) - tr = new Translation(); - var str = CommandLineParser.GenerateHelp(CommandInfo, ConsoleUtil.WrapToWidth(), applicationTr, tr, helpProcessor); - if (WriteErrorText) - str += Environment.NewLine + GenerateErrorText(tr, ConsoleUtil.WrapToWidth()); - return str; - } - - /// - /// Determines whether should call and output it - /// to the console. Default is true. - /// - /// Only set this to false if the user explicitly asked to see the help screen. Otherwise its appearance - /// without explanation is confusing. - protected internal virtual bool WriteErrorText { get { return true; } } -} - -/// Indicates that the user supplied one of the standard options we recognize as a help request. -[Serializable] -public sealed class CommandLineHelpRequestedException : CommandLineParseException -{ - /// Constructor. - internal CommandLineHelpRequestedException(CommandInfo commandInfo) - : base(tr => tr.UserRequestedHelp.Color(ConsoleColor.Gray), commandInfo) - { - } - - /// Overrides the base to indicate that no error message should be output along with the help screen. - protected internal override bool WriteErrorText { get { return false; } } -} - -/// Specifies that the arguments specified by the user on the command-line do not pass the custom validation checks. -[Serializable] -public sealed class CommandLineValidationException : CommandLineParseException -{ - /// Constructor. - internal CommandLineValidationException(ConsoleColoredString message, CommandInfo commandInfo) : base(tr => message, commandInfo) { } -} - -/// -/// Specifies that the command-line parser encountered a command or option that was not recognised (there was no or attribute with a matching option or command name). -[Serializable] -public sealed class UnrecognizedCommandOrOptionException : CommandLineParseException -{ - /// The unrecognized command name or option name. - public string CommandOrOptionName { get; private set; } - /// Constructor. - internal UnrecognizedCommandOrOptionException(string commandOrOptionName, CommandInfo commandInfo) : this(commandOrOptionName, commandInfo, null) { } - /// Constructor. - internal UnrecognizedCommandOrOptionException(string commandOrOptionName, CommandInfo commandInfo, Exception inner) - : base(tr => tr.UnrecognizedCommandOrOption.ToConsoleColoredString().Fmt(commandOrOptionName.Color(ConsoleColor.White)), commandInfo, inner) - { - CommandOrOptionName = commandOrOptionName; - } -} - -/// -/// Specifies that the command-line parser encountered a command or option that is not allowed in conjunction with a -/// previously-encountered command or option. -[Serializable] -public sealed class IncompatibleCommandOrOptionException : CommandLineParseException -{ - /// - /// The earlier option or command, which by itself is valid, but conflicts with the . - public string EarlierCommandOrOption { get; private set; } - /// The later option or command, which conflicts with the . - public string LaterCommandOrOption { get; private set; } - /// Constructor. - internal IncompatibleCommandOrOptionException(string earlier, string later, CommandInfo commandInfo) : this(earlier, later, commandInfo, null) { } - /// Constructor. - internal IncompatibleCommandOrOptionException(string earlier, string later, CommandInfo commandInfo, Exception inner) - : base(tr => tr.IncompatibleCommandOrOption.ToConsoleColoredString().Fmt(later.Color(ConsoleColor.White), earlier.Color(ConsoleColor.White)), commandInfo, inner) - { - EarlierCommandOrOption = earlier; - LaterCommandOrOption = later; - } -} - -/// -/// Specifies that the command-line parser encountered the end of the command line when it expected an argument to an -/// option. -[Serializable] -public sealed class IncompleteOptionException : CommandLineParseException -{ - /// The name of the option that was missing an argument. - public string OptionName { get; private set; } - /// Constructor. - internal IncompleteOptionException(string optionName, CommandInfo commandInfo) : this(optionName, commandInfo, null) { } - /// Constructor. - internal IncompleteOptionException(string optionName, CommandInfo commandInfo, Exception inner) - : base(tr => tr.IncompleteOption.ToConsoleColoredString().Fmt(optionName.Color(ConsoleColor.White)), commandInfo, inner) - { - OptionName = optionName; - } -} - -/// -/// Specifies that the command-line parser encountered additional command-line arguments when it expected the end of the -/// command line. -[Serializable] -public sealed class UnexpectedArgumentException : CommandLineParseException -{ - /// Contains the first unexpected argument and all of the subsequent arguments. - public string[] UnexpectedParameters { get; private set; } - /// Constructor. - internal UnexpectedArgumentException(string[] unexpectedArgs, CommandInfo commandInfo) : this(unexpectedArgs, commandInfo, null) { } - /// Constructor. - internal UnexpectedArgumentException(string[] unexpectedArgs, CommandInfo commandInfo, Exception inner) - : base(tr => tr.UnexpectedParameter.ToConsoleColoredString().Fmt(unexpectedArgs.Select(prm => prm.Length > 50 ? prm.Substring(0, 47) + "..." : prm).FirstOrDefault().Color(CmdLineColor.UnexpectedArgument)), commandInfo, inner) - { - UnexpectedParameters = unexpectedArgs; - } -} - -/// -/// Specifies that a parameter that expected a numerical value was passed a string by the user that doesn’t parse as a -/// number. -[Serializable] -public sealed class InvalidNumericParameterException : CommandLineParseException -{ - /// Contains the name of the field pertaining to the parameter that was passed an invalid value. - public string FieldName { get; private set; } - /// Constructor. - internal InvalidNumericParameterException(string fieldName, CommandInfo commandInfo) : this(fieldName, commandInfo, null) { } - /// Constructor. - internal InvalidNumericParameterException(string fieldName, CommandInfo commandInfo, Exception inner) - : base(tr => tr.InvalidNumber.ToConsoleColoredString().Fmt("<".Color(CmdLineColor.FieldBrackets) + fieldName.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)), commandInfo, inner) - { - FieldName = fieldName; - } -} - -/// -/// Specifies that the command-line parser encountered the end of the command line when it expected additional mandatory -/// options. -[Serializable] -public sealed class MissingParameterException : CommandLineParseException -{ - /// Contains the field pertaining to the parameter that was missing. - public FieldInfo Field { get; private set; } - /// Contains an optional reference to a field which the missing parameter must precede. - public FieldInfo BeforeField { get; private set; } - /// - /// Specifies whether the missing parameter was a missing option (true) or a missing positional parameter (false). - public bool IsOption { get; private set; } - /// Constructor. - internal MissingParameterException(FieldInfo paramField, FieldInfo beforeField, bool isOption, CommandInfo commandInfo) : this(paramField, beforeField, isOption, commandInfo, null) { } - /// Constructor. - internal MissingParameterException(FieldInfo paramField, FieldInfo beforeField, bool isOption, CommandInfo commandInfo, Exception inner) - : base(tr => getMessage(tr, paramField, beforeField, isOption), commandInfo, inner) - { Field = paramField; BeforeField = beforeField; IsOption = isOption; } - - private static ConsoleColoredString getMessage(Translation tr, FieldInfo field, FieldInfo beforeField, bool isOption) - { - if (beforeField == null) - return (isOption ? tr.MissingOption : tr.MissingParameter).ToConsoleColoredString().Fmt(field.FormatParameterUsage(true)); - - return (isOption ? tr.MissingOptionBefore : tr.MissingParameterBefore).ToConsoleColoredString().Fmt( - field.FormatParameterUsage(true), - "<".Color(CmdLineColor.FieldBrackets) + beforeField.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } -} - -/// -/// Specifies that the command-line parser encountered the end of the command line when it expected a mandatory -/// subcommand. -[Serializable] -public sealed class MissingSubcommandException : CommandLineParseException -{ - /// Constructor. - internal MissingSubcommandException(CommandInfo commandInfo) : this(commandInfo, null) { } - /// Constructor. - internal MissingSubcommandException(CommandInfo commandInfo, Exception inner) - : base(tr => tr.MissingSubcommand.ToConsoleColoredString(), commandInfo, inner) - { } -} - -static class CmdLineExtensions -{ - public static string[] GetOrderedOptionAttributeNames(this MemberInfo member) - { - var attr = member.GetCustomAttributes().FirstOrDefault(); - return attr == null ? null : attr.Names.OrderBy(compareOptionNames).ToArray(); - } - - private static int compareOptionNames(string opt1, string opt2) - { - bool long1 = opt1.StartsWith("--"); - bool long2 = opt2.StartsWith("--"); - if (long1 == long2) - return StringComparer.OrdinalIgnoreCase.Compare(opt1, opt2); - else if (long1) - return 1; // --blah comes after -blah - else - return -1; - } - - public static ConsoleColoredString FormatParameterUsage(this FieldInfo field, bool isMandatory) - { - // Positionals - if (field.IsDefined()) - return (isMandatory ? "{0}" : "[{0}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - "<".Color(CmdLineColor.FieldBrackets) + field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - - // -t name [-t name [...]] — arrays, multi-value enums with CommandNames - if (field.FieldType.IsArray || - (field.FieldType.IsEnum && - field.IsDefined() && - field.IsDefined() && - field.GetCustomAttributes().First().Behavior == EnumBehavior.MultipleValues)) - { - return (isMandatory ? "{0} {1} [{0} {1} [...]]" : "[{0} {1} [{0} {1} [...]]]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - - // Enums with Option names - if (field.FieldType.IsEnum && !field.IsDefined()) - { - var options = field.FieldType.GetFields(BindingFlags.Public | BindingFlags.Static) - .Where(fld => fld.IsDefined() && !fld.IsDefined()) - .Select(fi => fi.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option)) - .ToArray(); - - if (field.IsDefined() && field.GetCustomAttributes().First().Behavior == EnumBehavior.MultipleValues) - // [-t] [-u] [-v] — multi-value enums with Option names - return options.Select(opt => "[{0}]".Color(CmdLineColor.OptionalityDelimiters).Fmt(opt)).JoinColoredString(" "); - - // {-t|-u} — single-value enums with Options - return (isMandatory ? (options.Length > 1 ? "{{{0}{1}" : "{0}") : "[{0}]").Color(CmdLineColor.OptionalityDelimiters).Fmt(options.JoinColoredString("|".Color(CmdLineColor.OptionalityDelimiters)), "}"); - } - - // -t — bools - if (field.FieldType == typeof(bool)) - return "[{0}]".Color(CmdLineColor.OptionalityDelimiters).Fmt(field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option)); - - // -t name - return (isMandatory ? "{0} {1}" : "[{0} {1}]").Color(CmdLineColor.OptionalityDelimiters).Fmt( - field.GetOrderedOptionAttributeNames().First().Color(CmdLineColor.Option), - "<".Color(CmdLineColor.FieldBrackets) + field.Name.Color(CmdLineColor.Field) + ">".Color(CmdLineColor.FieldBrackets)); - } - - public static ConsoleColoredString GetDocumentation(this MemberInfo member, Type inType, TranslationBase applicationTr, Func helpProcessor) - { - if (member.IsDefined()) - return helpProcessor(member.GetCustomAttributes().Select(d => d.Text ?? "").First()); - if (applicationTr == null) - return ""; - - if (!(member is Type) && inType.IsSubclassOf(member.DeclaringType)) - inType = member.DeclaringType; - var meth = inType.GetMethod(member.Name + "Doc", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic, null, new Type[] { applicationTr.GetType() }, null); - if (meth == null || meth.ReturnType != typeof(string)) - return ""; - var str = (string) meth.Invoke(null, new object[] { applicationTr }); - return str == null ? "" : helpProcessor(CommandLineParser.Colorize(EggsML.Parse(str))); - } -} diff --git a/SrcLingo/RT.CommandLine.Lingo.csproj b/SrcLingo/RT.CommandLine.Lingo.csproj deleted file mode 100644 index c4b81b0..0000000 --- a/SrcLingo/RT.CommandLine.Lingo.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - netstandard2.0;net8.0 - enable - latest - $(OutputPath)\$(AssemblyName).xml - - Timwi;rstarkov - A command line parser that populates a class or a set of classes, with support for advanced help text formatting and translations using RT.Lingo. - rt.commandline;command line;parser - MIT - snupkg - true - - - - - - - - - - diff --git a/Tests/CommandLineLingoTests.cs b/Tests/CommandLineLingoTests.cs deleted file mode 100644 index b18b7f1..0000000 --- a/Tests/CommandLineLingoTests.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Xunit; - -namespace RT.CommandLine.Lingo.Tests; - -public sealed class CmdLineLingoTests -{ -#pragma warning disable 0649 // Field is never assigned to, and will always have its default value null - private class commandLine - { - [Option("--stuff")] - public string Stuff; - [IsPositional] - public string[] Args; - } -#pragma warning restore 0649 // Field is never assigned to, and will always have its default value null - - [Fact] - public static void Test() - { - var c = CommandLineParser.Parse("--stuff blah abc def".Split(' ')); - Assert.Equal("blah", c.Stuff); - Assert.True(c.Args.SequenceEqual(new[] { "abc", "def" })); - - c = CommandLineParser.Parse("def --stuff thingy abc".Split(' ')); - Assert.Equal("thingy", c.Stuff); - Assert.True(c.Args.SequenceEqual(new[] { "def", "abc" })); - - c = CommandLineParser.Parse("--stuff stuff -- abc --stuff blah -- def".Split(' ')); - Assert.Equal("stuff", c.Stuff); - Assert.True(c.Args.SequenceEqual(new[] { "abc", "--stuff", "blah", "--", "def" })); - } -} diff --git a/Tests/CommandLineTests.cs b/Tests/CommandLineTests.cs index 7894c5c..1ea4104 100644 --- a/Tests/CommandLineTests.cs +++ b/Tests/CommandLineTests.cs @@ -1,4 +1,6 @@ -using RT.Util.Consoles; +using RT.Json; +using RT.Serialization; +using RT.Util.Consoles; using Xunit; namespace RT.CommandLine.Tests; @@ -92,50 +94,6 @@ public static void TestInvalidOption() } } - - class Test1Cmd : ICommandLineValidatable - { - [IsPositional, IsMandatory] - public string Base; - - [IsPositional, IsMandatory] - public Test1SubcommandBase Subcommand; - - public static int ValidateCalled = 0; - - public ConsoleColoredString Validate() - { - ValidateCalled++; - return null; - } - } - - [CommandGroup] - abstract class Test1SubcommandBase : ICommandLineValidatable - { - public static int ValidateCalled = 0; - public abstract ConsoleColoredString Validate(); - } - - [CommandName("sub1")] - sealed class Test1Subcommand1 : Test1SubcommandBase - { - [IsPositional, IsMandatory] - public string ItemName; - - public override ConsoleColoredString Validate() - { - ValidateCalled++; - return null; - } - } - - [CommandName("sub2")] - sealed class Test1Subcommand2 : Test1SubcommandBase - { - public override ConsoleColoredString Validate() { return null; } - } - [Fact] public static void TestSubcommandValidation() { @@ -157,4 +115,16 @@ public static void TestSubcommandValidation() Assert.Equal(1, Test1Cmd.ValidateCalled); Assert.Equal(0, Test1SubcommandBase.ValidateCalled); } + + [Fact] + public static void TestMore() + { + Assert.True( + JsonValue.Parse(@"{""Boolean"":true,""Subcommand"":{""SharedString"":null,""Name"":""this"","":type"":""Test2SubcommandAdd""}}") + == ClassifyJson.Serialize(CommandLineParser.Parse(["-b", "add", "this"]))); + + Assert.True( + JsonValue.Parse(@"{""Boolean"":false,""Subcommand"":{""SharedString"":null,""Id"":""this"","":type"":""Test2SubcommandDelete""}}") + == ClassifyJson.Serialize(CommandLineParser.Parse(["del", "this"]))); + } } diff --git a/Tests/RT.CommandLine.Tests.csproj b/Tests/RT.CommandLine.Tests.csproj index a9d613d..06f867f 100644 --- a/Tests/RT.CommandLine.Tests.csproj +++ b/Tests/RT.CommandLine.Tests.csproj @@ -9,8 +9,10 @@ - - + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -22,7 +24,6 @@ - diff --git a/Tests/Test1.cs b/Tests/Test1.cs new file mode 100644 index 0000000..f772c80 --- /dev/null +++ b/Tests/Test1.cs @@ -0,0 +1,46 @@ +using RT.Util.Consoles; + +namespace RT.CommandLine.Tests; + +class Test1Cmd : ICommandLineValidatable +{ + [IsPositional, IsMandatory] + public string Base; + + [IsPositional, IsMandatory] + public Test1SubcommandBase Subcommand; + + public static int ValidateCalled = 0; + + public ConsoleColoredString Validate() + { + ValidateCalled++; + return null; + } +} + +[CommandGroup] +abstract class Test1SubcommandBase : ICommandLineValidatable +{ + public static int ValidateCalled = 0; + public abstract ConsoleColoredString Validate(); +} + +[CommandName("sub1")] +sealed class Test1Subcommand1 : Test1SubcommandBase +{ + [IsPositional, IsMandatory] + public string ItemName; + + public override ConsoleColoredString Validate() + { + ValidateCalled++; + return null; + } +} + +[CommandName("sub2")] +sealed class Test1Subcommand2 : Test1SubcommandBase +{ + public override ConsoleColoredString Validate() { return null; } +} diff --git a/Tests/Test2.cs b/Tests/Test2.cs new file mode 100644 index 0000000..aaf9619 --- /dev/null +++ b/Tests/Test2.cs @@ -0,0 +1,30 @@ +namespace RT.CommandLine.Tests; + +class Test2Cmd +{ + [Option("-b")] + public bool Boolean; + + public Test2Subcommand Subcommand; +} + +[CommandGroup] +abstract class Test2Subcommand +{ + [Option("-k")] + public string SharedString; +} + +[CommandName("add")] +class Test2SubcommandAdd : Test2Subcommand +{ + [IsPositional, IsMandatory] + public string Name; +} + +[CommandName("del")] +class Test2SubcommandDelete : Test2Subcommand +{ + [IsPositional, IsMandatory] + public string Id; +}