From: Jonathan Pryor Date: Tue, 31 Jan 2017 20:27:32 +0000 (-0500) Subject: [Mono.Options] Add Mono.Options.Command, .CommandSet X-Git-Url: http://wien.tomnetworks.com/gitweb/?a=commitdiff_plain;h=c1b9fc566a365cf6923218eae9098d0fd0e632ae;p=mono.git [Mono.Options] Add Mono.Options.Command, .CommandSet Mono.Options.CommandSet allows easily having separate commands and associated command options, allowing creation of a *suite* along the lines of **git**(1), **svn**(1), etc. CommandSet allows intermixing plain text strings for `--help` output, Option values -- as supported by OptionSet -- and Command instances, which have a name, optional help text, and an optional OptionSet. var suite = new CommandSet ("suite-name") { // Use strings and option values, as with OptionSet "usage: suite-name COMMAND [OPTIONS]+", { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 }, // Commands may also be specified new Command ("command-name", "command help") { Options = new OptionSet {/*...*/}, Run = args => { /*...*/}, }, new MyCommandSubclass (), }; suite.Run (new string[]{...}); CommandSet provides a `help` command, and forwards `help COMMAND` to the registered Command instance by invoking Command.Invoke() with `--help` as an option. --- diff --git a/mcs/class/Mono.Options/Documentation/en/Mono.Options/Command.xml b/mcs/class/Mono.Options/Documentation/en/Mono.Options/Command.xml new file mode 100644 index 00000000000..de2cda6e17a --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/Mono.Options/Command.xml @@ -0,0 +1,286 @@ + + + + + Mono.Options + 0.2.3.0 + + + Public static members of this type are thread safe. + Any instance members are not guaranteed to be thread safe. + + + System.Object + + + + + Represents a program command. + + + + Many command-line utilities are suites of commands, with a single + "outer" command and multiple commands. Examples of this style of + utility includes git, svn, and mdoc. + + + A Command represents a specific command in such a suite. + It has a which is the + command name for invocation purposes, optional help text through + the property, an optional + accessible through the + property for command-line + parsing, and two ways to have code executed when a command is + invoked: the property and + the method. + + + + + + + + Constructor + + 0.2.3.0 + + + + + + + + A which is the command name. + + + A which is the command help text. + + + Creates and initializes a new instance of the Command class. + + + + This constructor initializes the + property of the new + instance using and initializes the + property of the new + instance using . + + + + is . + + + + + + + Property + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + A instance which owns the + Command. + + + A instance which owns the + Command. + + + + A Command instance may belong to only one + instance. + The CommandSet property is set upon calling + . + + + If the Command instance has not yet been added to a + CommandSet, then this property is . + + + Use the CommandSet instance from either the + property or an overridden + method to access + localization facilities through + , the + preferred message output stream through + , and other features. + + + + + + + + Property + + 0.2.3.0 + + + System.String + + + + A short, one-line, description of the Command. + + + A containing the optional help text + of the Command. + + + + The Help property text is shown when the help + command is invoked. + + + + + + + + Method + + 0.2.3.0 + + + System.Int32 + + + + + + + A + which contains the unprocessed command-line arguments. + + + Invoked by when a command + has been executed. + + + A which should be treated as the process + exit value. + + + + The value returned by Invoke() is the return value of + , and should be treated + as a possible process exit value. + + + + If the Invoke() method is not overridden by a subclass, + the Invoke() method will use + to parse + , and pass any un-processed values + on to . + + + If the Options property is , then no + option processing will occur, and + will be provided to the Run property as-is. + + + If the Run property is , then + no further processing occurs. + + + + + Method overrides do not need to call the base class method. + + + + + + + + + Property + + 0.2.3.0 + + + System.String + + + + The name of the Command, which is used for command invocation. + + + A which is the name of the Command. + + + + The Name value must be unique across all Commmand instances + referred to by a . + + + + + + + + Property + + 0.2.3.0 + + + Mono.Options.OptionSet + + + + Optional command-line option information. + + + A instance which contains the + available command-line options for the Command. + + + + If the Options property is not when + the command is processed, + will be invoked on + the Options instance, and the return value of + OptionSet.Parse() will be forwarded to + . + + + + + + + + Property + + 0.2.3.0 + + + System.Action<System.Collections.Generic.IEnumerable<System.String>> + + + + Optional command handler. + + + A + delegate which is executed by + . + + + + The Run property is executed by the the + method when + dispatches to a + Command instance. + + + + + + diff --git a/mcs/class/Mono.Options/Documentation/en/Mono.Options/CommandSet.xml b/mcs/class/Mono.Options/Documentation/en/Mono.Options/CommandSet.xml new file mode 100644 index 00000000000..5d82495a052 --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/Mono.Options/CommandSet.xml @@ -0,0 +1,1349 @@ + + + + + + Mono.Options + 0.2.3.0 + + + Public static members of this type are thread safe. + Any instance members are not guaranteed to be thread safe. + + + System.Collections.ObjectModel.KeyedCollection<System.String,Mono.Options.Command> + + System.String + Mono.Options.Command + + + + + + A suite of commands, global program options, and associated documentation. + + + + A common requirement of some programs are discrete commands. + A CommandSet represents a suite of commands, intermixed + with suite documentation. Commands are managed by + instances, which have a required + name and optional help text, and CommandSet will use the + intermixed documentation, options, and commands to produce help + command output. + + + To create a CommandSet instance, use the + + constructor. Only the suite name is required; all other parameters are + optional. + + + Once a CommandSet instance has been constructed, use the + methods to add and + intermix suite documentation, global options, and commands. + Documentation is any string constant, global options are handled + by instances and associated + Add() method overloads which implicitly create Option + instances, and commands are through Command instances. + + + Once the CommandSet instance has been initialized, call the + + method to process the arguments provided to Main(). + The appropriate Command instance will be determined, it's + options parsed, and + will be executed. The return value of Command.Invoke() + is returned from CommandSet.Run(), and should be treated + as the process exit value. + + + + + The following commands example demonstrates some simple usage + of . + + // Sub-commands with Mono.Options.CommandSet +// +// Compile as: +// mcs -r:Mono.Options.dll commands.cs + +using System; +using System.Collections.Generic; + +using Mono.Options; + +class CommandDemo { + public static int Main (string[] args) + { + var commands = new CommandSet ("commands") { + "usage: commands COMMAND [OPTIONS]", + "", + "Mono.Options.CommandSet sample app.", + "", + "Global options:", + { "v:", + "Output verbosity.", + (int? n) => Verbosity = n.HasValue ? n.Value : Verbosity + 1 }, + "", + "Available commands:", + new Command ("echo", "Echo arguments to the screen") { + Run = ca => Console.WriteLine ("{0}", string.Join (" ", ca)), + }, + new RequiresArgs (), + }; + return commands.Run (args); + } + + public static int Verbosity; +} + +class RequiresArgs : Command { + + public RequiresArgs () + : base ("requires-args", "Class-based Command subclass") + { + Options = new OptionSet () { + "usage: commands requires-args [OPTIONS]", + "", + "Class-based Command subclass example.", + { "name|n=", + "{name} of person to greet.", + v => Name = v }, + { "help|h|?", + "Show this message and exit.", + v => ShowHelp = v != null }, + }; + } + + public bool ShowHelp {get; private set;} + public new string Name {get; private set;} + + public override int Invoke (IEnumerable<string> args) + { + try { + var extra = Options.Parse (args); + if (ShowHelp) { + Options.WriteOptionDescriptions (CommandSet.Out); + return 0; + } + if (string.IsNullOrEmpty (Name)) { + Console.Error.WriteLine ("commands: Missing required argument `--name=NAME`."); + Console.Error.WriteLine ("commands: Use `commands help requires-args` for details."); + return 1; + } + Console.WriteLine ($"Hello, {Name}!"); + return 0; + } + catch (Exception e) { + Console.Error.WriteLine ("commands: {0}", CommandDemo.Verbosity >= 1 ? e.ToString () : e.Message); + return 1; + } + } +} + + + The output, under the influence of different command-line arguments, is: + + $ mono commands.exe +Use `commands help` for usage. + +$ mono commands.exe --help +usage: commands COMMAND [OPTIONS] + +Mono.Options.CommandSet sample app. + +Global options: + -v[=VALUE] Output verbosity. + +Available commands: + echo Echo arguments to the screen + requires-args Class-based Command subclass + +$ mono commands.exe help +usage: commands COMMAND [OPTIONS] + +Mono.Options.CommandSet sample app. + +Global options: + -v[=VALUE] Output verbosity. + +Available commands: + echo Echo arguments to the screen + requires-args Class-based Command subclass + +$ mono commands.exe help --help +Usage: commands COMMAND [OPTIONS] +Use `commands help COMMAND` for help on a specific command. + +Available commands: + + echo Echo arguments to the screen + requires-args Class-based Command subclass + help Show this message and exit + +$ mono commands.exe help echo +--help + +$ mono commands.exe echo --help +--help + +$ mono commands.exe echo hello, world +hello, world + +$ mono commands.exe requires-args +commands: Missing required argument `--name=NAME`. +commands: Use `commands help requires-args` for details. + +$ mono commands.exe help requires-args +usage: commands requires-args [OPTIONS] + +Class-based Command subclass example. + --name, -n=name name of person to greet. + --help, -h, -? Show this message and exit. + +$ mono commands.exe requires-args --help +usage: commands requires-args [OPTIONS] + +Class-based Command subclass example. + --name, -n=name name of person to greet. + --help, -h, -? Show this message and exit. + +$ mono commands.exe requires-args -n World +Hello, World! + +$ mono commands.exe invalid-command +commands: Unknown command: invalid-command +commands: Use `commands help` for usage. + +$ mono commands.exe help invalid-command +commands: Unknown command: invalid-command +commands: Use `commands help` for usage. + + + The commands.exe output is short, informing the user that + commands are required for use. + + + The commands.exe --help is identical to the commands help + output, and shows the provided suite documentation. + + + commands.exe COMMAND --help and commands.exe help COMMAND + output is likewise identical, and will attempt to generate documentation + for the specified command, if available. This is performed by invoking + + with the value { "--help" }. This can be seen in the + commands.exe help echo and commands.exe echo --help + output, which simply prints --help. If a command wants + to partake in help COMMAND support, it needs to explicitly + handle the --help option in its associated + instance, referenced by the + property. + + + Finally, if an invalid command is specified, then an error is written + to , prefixed with the + value. + + + + + + + + Constructor + + 0.2.3.0 + + + + + + + + + + A containing the name of the suite. + This value is used in default help text output. + + + A + instance that will be used to translate strings. + If , then no localization is performed. + + + A where output messages will be + written to. If , then the + property will be used. + + + A where error messages will be + written to. If , then the + property will be used. + + + Creates and initializes a new CommandSet instance. + + + + This constructor initializes + the property + of the new instance using , + the property + of the new instance using , + the property + of the new instance using , and + the property + of the new instance using . + + + + is . + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + A to register for + argument processing. + + + Registers so that it may be consulted + during argument processing within + . + + + The current instance. + This is to permit method chaining. + + + + + is . + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + A to add to the suite. + + + Add a Command to the suite. + + + The current instance. + This is to permit method chaining. + + + + + + A Command with the same value for + .Name + has already been added to the CommandSet. + + -or- + + has been Add()ed to a different + CommandSet instance. + + + + is . + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + The to register. + + + Adds as a global suite option. + + + The current instance. + This is to permit method chaining. + + + + Registers each option name returned by + , ensuring that any + option with a matching name will be handled by the + instance. + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + is . + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + A containing the header to display + during help processing. + + + Declare a header to be printed during for help output. + + + The current instance. + This is to permit method chaining. + + + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + Calls + + with a value of + . + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + Calls + + with a value of + . + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A to be used to initialize + the property. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + Use this method when should accept + two values, generally a key and a value. + + + If specifies a + option, + then it's possible that both the key and the value will be + in the callback function. + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A containing to used to initialize + the property. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A containing to used to initialize + the property. + + + A + to invoke when an option is parsed. + + + A specifying whether or not the + Option should be displayed in + help output. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A containing to used to initialize + the property. + + + A + to invoke when an option is parsed. + + + A specifying whether or not the + Option should be displayed in + help output. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + + The type of the option to parse and provide to the + callback. + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + Calls + + with a value of + . + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + + + The type of the option to parse and provide to the + callback. + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A containing to used to initialize + the property. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + Use this typed overload when you want strongly typed option values + that correspond to a managed type. + + is used to lookup the + to use when + performing the string-to-type conversion. + + + Special support is provided for + types; + doesn't currently support their use, but if + is a nullable type, then this method + will instead use the + for the + type. This allows straightforward use + of nullable types, identical to using any other strongly typed + value. + + + + If specifies an + value + and the value is not provided, then default(T) is + provided as the value to . + + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + + + The type of the first argument to parse and provide to the + callback. + + + The type of the second argument to parse and provide to the + callback. + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + Calls + + with a value of + . + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Method + + 0.2.3.0 + + + Mono.Options.CommandSet + + + + + + + + + + + + + The type of the first argument to parse and provide to the + callback. + + + The type of the second argument to parse and provide to the + callback. + + + A containing all option aliases to + register, an (optional) type specifier, and an (optional) value + separator list; see + + for details. + + + A to be used to initialize + the property. + + + A + to invoke when an option is parsed. + + + Registers each alias within so that any + options matching the aliases in will be + handled by during any subsequent + + calls. + + + The current instance. + This is to permit method chaining. + + + + Use this method when should accept + two typed values, generally a key and a value. + + + + If specifies an + value + and the value is not provided, then default(TKey) and + default(TValue) may be provided as the values to + . + + + + + + has an alias (as returned from + ) that conflicts with + a previously registered . + + + + is + -or- + + is + + + + + + + Property + + 0.2.3.0 + + + System.IO.TextWriter + + + + Where CommandSet should write error messages. + + + A where error messages will + be written to. + + + + This value may be set by providing the + constructor parameter. If not specified, then + will be used. + + + Error messages by CommandSet are written to the Error + property. Command instances may also choose to write error + messages to the CommandSet.Error property. This is suggested + to help with unit testing. + + + + + + + + Method + + 0.2.3.0 + + + System.String + + + + + + + An to return the key of. + + + Returns .Name. + + + A containing the command name. + + + + This is to support the + + infrastructure. + + + + + + + + Property + + 0.2.3.0 + + + System.Converter<System.String,System.String> + + + + Permits access to the message localization facility. + + + A + that can be used to localize messages. + + + + + + + + + Property + + 0.2.3.0 + + + System.IO.TextWriter + + + + Where CommandSet should write output messages. + + + A where output messages will + be written to. + + + + This value may be set by providing the + constructor parameter. If not specified, then + will be used. + + + Output messages by CommandSet are written to the Out + property. Command instances may also choose to write output + messages to the CommandSet.Out property. This is suggested + to help with unit testing. + + + + + + + + Method + + 0.2.3.0 + + + System.Int32 + + + + + + + A + containing the command-line arguments to process. + + + Processes command-line arguments and invokes the specified command. + + + A containing the command's exit value. + Normal Unix process exit values should be used: 0 for success, + non-zero values for failures. + + + + Processes , parsing global options in the first pass. + The first unprocessed argument is treated as a command name, and the + instance with a + value matching the command name + has its method invoked with the + unprocessed arguments. + + + + If no command is specified within or an invalid command name is specified, then 1 is returned. + Otherwise, the value returned from + is returned. + + + + If the help command is provided with no arguments, then the + suite help text will be written to + + consisting of the descriptive text ("headers"), options, and + Command values provided to + . + + + If the help command is provided with --help as an + argument, then all registered Command instances are written to + . + + + If the help command is provided with any other string, the + following value is treated as a command name, and the output of + command --help is written to + . If the specified + command name has not been registered, then an error message is written to + . + + + + + + + + Property + + 0.2.3.0 + + + System.String + + + + The name of the suite. + + + A containing the name of the command suite. + + + + The Suite value is used when no command is specified, or + when an unregistered command name is provided. + + + + + + \ No newline at end of file diff --git a/mcs/class/Mono.Options/Documentation/en/Mono.Options/HelpCommand.xml b/mcs/class/Mono.Options/Documentation/en/Mono.Options/HelpCommand.xml new file mode 100644 index 00000000000..74af8564321 --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/Mono.Options/HelpCommand.xml @@ -0,0 +1,51 @@ + + + + + Mono.Options + 0.2.3.0 + + + Mono.Options.Command + + + + To be added. + To be added. + + + + + + Constructor + + 0.2.3.0 + + + + To be added. + To be added. + + + + + + Method + + 0.2.3.0 + + + System.Int32 + + + + + + To be added. + To be added. + To be added. + To be added. + + + + diff --git a/mcs/class/Mono.Options/Documentation/en/Mono.Options/OptionSet.xml b/mcs/class/Mono.Options/Documentation/en/Mono.Options/OptionSet.xml index 67225ad7768..56ba9fdfdba 100644 --- a/mcs/class/Mono.Options/Documentation/en/Mono.Options/OptionSet.xml +++ b/mcs/class/Mono.Options/Documentation/en/Mono.Options/OptionSet.xml @@ -1059,7 +1059,7 @@ localization: hello:Could not convert string `not-an-int' to type Int32 for opti - is . + is . @@ -1137,8 +1137,8 @@ localization: hello:Could not convert string `not-an-int' to type Int32 for opti - The Add(string) method can be used to provide option groupin - in the output generatedy by + The Add(string) method can be used to provide option grouping + in the output generated by . diff --git a/mcs/class/Mono.Options/Documentation/en/examples/commands.cs b/mcs/class/Mono.Options/Documentation/en/examples/commands.cs new file mode 100644 index 00000000000..d8c48e4838e --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/examples/commands.cs @@ -0,0 +1,78 @@ +// Sub-commands with Mono.Options.CommandSet +// +// Compile as: +// mcs -r:Mono.Options.dll commands.cs + +using System; +using System.Collections.Generic; + +using Mono.Options; + +class CommandDemo { + public static int Main (string[] args) + { + var commands = new CommandSet ("commands") { + "usage: commands COMMAND [OPTIONS]", + "", + "Mono.Options.CommandSet sample app.", + "", + "Global options:", + { "v:", + "Output verbosity.", + (int? n) => Verbosity = n.HasValue ? n.Value : Verbosity + 1 }, + "", + "Available commands:", + new Command ("echo", "Echo arguments to the screen") { + Run = ca => Console.WriteLine ("{0}", string.Join (" ", ca)), + }, + new RequiresArgs (), + }; + return commands.Run (args); + } + + public static int Verbosity; +} + +class RequiresArgs : Command { + + public RequiresArgs () + : base ("requires-args", "Class-based Command subclass") + { + Options = new OptionSet () { + "usage: commands requires-args [OPTIONS]", + "", + "Class-based Command subclass example.", + { "name|n=", + "{name} of person to greet.", + v => Name = v }, + { "help|h|?", + "Show this message and exit.", + v => ShowHelp = v != null }, + }; + } + + public bool ShowHelp {get; private set;} + public new string Name {get; private set;} + + public override int Invoke (IEnumerable args) + { + try { + var extra = Options.Parse (args); + if (ShowHelp) { + Options.WriteOptionDescriptions (CommandSet.Out); + return 0; + } + if (string.IsNullOrEmpty (Name)) { + Console.Error.WriteLine ("commands: Missing required argument `--name=NAME`."); + Console.Error.WriteLine ("commands: Use `commands help requires-args` for details."); + return 1; + } + Console.WriteLine ($"Hello, {Name}!"); + return 0; + } + catch (Exception e) { + Console.Error.WriteLine ("commands: {0}", CommandDemo.Verbosity >= 1 ? e.ToString () : e.Message); + return 1; + } + } +} diff --git a/mcs/class/Mono.Options/Documentation/en/examples/commands.in b/mcs/class/Mono.Options/Documentation/en/examples/commands.in new file mode 100644 index 00000000000..96855a870ee --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/examples/commands.in @@ -0,0 +1,25 @@ +mono Documentation/en/examples/commands.exe + +mono Documentation/en/examples/commands.exe --help + +mono Documentation/en/examples/commands.exe help + +mono Documentation/en/examples/commands.exe help --help + +mono Documentation/en/examples/commands.exe help echo + +mono Documentation/en/examples/commands.exe echo --help + +mono Documentation/en/examples/commands.exe echo hello, world + +mono Documentation/en/examples/commands.exe requires-args + +mono Documentation/en/examples/commands.exe help requires-args + +mono Documentation/en/examples/commands.exe requires-args --help + +mono Documentation/en/examples/commands.exe requires-args -n World + +mono Documentation/en/examples/commands.exe invalid-command + +mono Documentation/en/examples/commands.exe help invalid-command diff --git a/mcs/class/Mono.Options/Documentation/en/examples/commands.txt b/mcs/class/Mono.Options/Documentation/en/examples/commands.txt new file mode 100644 index 00000000000..19306801140 --- /dev/null +++ b/mcs/class/Mono.Options/Documentation/en/examples/commands.txt @@ -0,0 +1,74 @@ +$ mono commands.exe +Use `commands help` for usage. + +$ mono commands.exe --help +usage: commands COMMAND [OPTIONS] + +Mono.Options.CommandSet sample app. + +Global options: + -v[=VALUE] Output verbosity. + +Available commands: + echo Echo arguments to the screen + requires-args Class-based Command subclass + +$ mono commands.exe help +usage: commands COMMAND [OPTIONS] + +Mono.Options.CommandSet sample app. + +Global options: + -v[=VALUE] Output verbosity. + +Available commands: + echo Echo arguments to the screen + requires-args Class-based Command subclass + +$ mono commands.exe help --help +Usage: commands COMMAND [OPTIONS] +Use `commands help COMMAND` for help on a specific command. + +Available commands: + + echo Echo arguments to the screen + requires-args Class-based Command subclass + help Show this message and exit + +$ mono commands.exe help echo +--help + +$ mono commands.exe echo --help +--help + +$ mono commands.exe echo hello, world +hello, world + +$ mono commands.exe requires-args +commands: Missing required argument `--name=NAME`. +commands: Use `commands help requires-args` for details. + +$ mono commands.exe help requires-args +usage: commands requires-args [OPTIONS] + +Class-based Command subclass example. + --name, -n=name name of person to greet. + --help, -h, -? Show this message and exit. + +$ mono commands.exe requires-args --help +usage: commands requires-args [OPTIONS] + +Class-based Command subclass example. + --name, -n=name name of person to greet. + --help, -h, -? Show this message and exit. + +$ mono commands.exe requires-args -n World +Hello, World! + +$ mono commands.exe invalid-command +commands: Unknown command: invalid-command +commands: Use `commands help` for usage. + +$ mono commands.exe help invalid-command +commands: Unknown command: invalid-command +commands: Use `commands help` for usage. diff --git a/mcs/class/Mono.Options/Documentation/en/index.xml b/mcs/class/Mono.Options/Documentation/en/index.xml index 4cdca7c2a7d..f8fdaddee42 100644 --- a/mcs/class/Mono.Options/Documentation/en/index.xml +++ b/mcs/class/Mono.Options/Documentation/en/index.xml @@ -18,6 +18,9 @@ System.Reflection.AssemblyTitle("Mono.Options.dll") + + System.Runtime.CompilerServices.CompilationRelaxations(8) + System.Runtime.CompilerServices.RuntimeCompatibility(WrapNonExceptionThrows=true) @@ -30,6 +33,9 @@ + + + diff --git a/mcs/class/Mono.Options/Makefile b/mcs/class/Mono.Options/Makefile index 2fb4724b281..d8cad7eeab1 100644 --- a/mcs/class/Mono.Options/Makefile +++ b/mcs/class/Mono.Options/Makefile @@ -17,6 +17,16 @@ mono_options_DATA = Mono.Options/Options.cs include ../../build/library.make +test-local: Mono.Options-PCL.dll + +clean-local: clean-pcl + +Mono.Options-PCL.dll: Mono.Options.dll.sources $(shell cat Mono.Options.dll.sources) + $(CSCOMPILE) -target:library -out:$@ -debug+ -d:PCL -r:../lib/$(PROFILE)/System.dll @$< + +clean-pcl: + -rm Mono.Options-PCL.dll + install-local: install-source uninstall-local: uninstall-source @@ -35,6 +45,7 @@ fixup-docs: DOC_EXAMPLES_OUTPUT = \ Documentation/en/examples/bundling.txt \ + Documentation/en/examples/commands.txt \ Documentation/en/examples/context.txt \ Documentation/en/examples/greet.txt \ Documentation/en/examples/localization.txt \ @@ -47,7 +58,7 @@ Documentation/en/examples/Mono.Options.dll: $(the_lib) -cp $^.mdb $@.mdb %.exe: %.cs Documentation/en/examples/Mono.Options.dll - $(CSCOMPILE) -debug+ -r:Mono.Posix.dll -r:System.Core.dll -lib:Documentation/en/examples -r:Mono.Options.dll -out:$@ $< + $(CSCOMPILE) -debug+ -r:../lib/$(PROFILE)/System.dll -r:../lib/$(PROFILE)/Mono.Posix.dll -r:../lib/$(PROFILE)/System.Core.dll -lib:Documentation/en/examples -r:../lib/$(PROFILE)/Mono.Options.dll -out:$@ $< Documentation/en/examples/locale/es/LC_MESSAGES/localization.mo: Documentation/en/examples/localization-es.po msgfmt $< -o $@ diff --git a/mcs/class/Mono.Options/Mono.Options/Options.cs b/mcs/class/Mono.Options/Mono.Options/Options.cs index 05810c39481..029bcb87dde 100644 --- a/mcs/class/Mono.Options/Mono.Options/Options.cs +++ b/mcs/class/Mono.Options/Mono.Options/Options.cs @@ -2,13 +2,14 @@ // Options.cs // // Authors: -// Jonathan Pryor +// Jonathan Pryor , // Federico Di Gregorio // Rolf Bjarne Kvinge // // Copyright (C) 2008 Novell (http://www.novell.com) // Copyright (C) 2009 Federico Di Gregorio. // Copyright (C) 2012 Xamarin Inc (http://www.xamarin.com) +// Copyright (C) 2017 Microsoft Corporation (http://www.microsoft.com) // // Permission is hereby granted, free of charge, to any person obtaining // a copy of this software and associated documentation files (the @@ -31,8 +32,8 @@ // // Compile With: -// gmcs -debug+ -r:System.Core Options.cs -o:NDesk.Options.dll -// gmcs -debug+ -d:LINQ -r:System.Core Options.cs -o:NDesk.Options.dll +// mcs -debug+ -r:System.Core Options.cs -o:Mono.Options.dll +// mcs -debug+ -d:LINQ -r:System.Core Options.cs -o:Mono.Options.dll // // The LINQ version just changes the implementation of // OptionSet.Parse(IEnumerable), and confers no semantic changes. @@ -40,7 +41,7 @@ // // A Getopt::Long-inspired option parsing library for C#. // -// NDesk.Options.OptionSet is built upon a key/value table, where the +// Mono.Options.OptionSet is built upon a key/value table, where the // key is a option format string and the value is a delegate that is // invoked when the format string is matched. // @@ -127,6 +128,33 @@ // p.Parse (new string[]{"-a-"}); // sets v == null // +// +// Mono.Options.CommandSet allows easily having separate commands and +// associated command options, allowing creation of a *suite* along the +// lines of **git**(1), **svn**(1), etc. +// +// CommandSet allows intermixing plain text strings for `--help` output, +// Option values -- as supported by OptionSet -- and Command instances, +// which have a name, optional help text, and an optional OptionSet. +// +// var suite = new CommandSet ("suite-name") { +// // Use strings and option values, as with OptionSet +// "usage: suite-name COMMAND [OPTIONS]+", +// { "v:", "verbosity", (int? v) => Verbosity = v.HasValue ? v.Value : Verbosity+1 }, +// // Commands may also be specified +// new Command ("command-name", "command help") { +// Options = new OptionSet {/*...*/}, +// Run = args => { /*...*/}, +// }, +// new MyCommandSubclass (), +// }; +// return suite.Run (new string[]{...}); +// +// CommandSet provides a `help` command, and forwards `help COMMAND` +// to the registered Command instance by invoking Command.Invoke() +// with `--help` as an option. +// + using System; using System.Collections; using System.Collections.Generic; @@ -414,7 +442,7 @@ namespace Mono.Options ? new[]{prototype + this.GetHashCode ()} : prototype.Split ('|'); - if (this is OptionSet.Category) + if (this is OptionSet.Category || this is CommandOption) return; this.type = ParsePrototype (); @@ -585,6 +613,11 @@ namespace Mono.Options protected abstract void OnParseComplete (OptionContext c); + internal void InvokeOnParseComplete (OptionContext c) + { + OnParseComplete (c); + } + public override string ToString () { return Prototype; @@ -732,20 +765,26 @@ namespace Mono.Options public class OptionSet : KeyedCollection { public OptionSet () - : this (delegate (string f) {return f;}) + : this (null) { } public OptionSet (MessageLocalizerConverter localizer) { + this.roSources = new ReadOnlyCollection (sources); this.localizer = localizer; - this.roSources = new ReadOnlyCollection(sources); + if (this.localizer == null) { + this.localizer = delegate (string f) { + return f; + }; + } } MessageLocalizerConverter localizer; public MessageLocalizerConverter MessageLocalizer { get {return localizer;} + internal set {localizer = value;} } List sources = new List (); @@ -1210,6 +1249,9 @@ namespace Mono.Options private const int Description_FirstWidth = 80 - OptionWidth; private const int Description_RemWidth = 80 - OptionWidth - 2; + static readonly string CommandHelpIndentStart = new string (' ', OptionWidth); + static readonly string CommandHelpIndentRemaining = new string (' ', OptionWidth + 2); + public void WriteOptionDescriptions (TextWriter o) { foreach (Option p in this) { @@ -1223,6 +1265,11 @@ namespace Mono.Options WriteDescription (o, p.Description, "", 80, 80); continue; } + CommandOption co = p as CommandOption; + if (co != null) { + WriteCommandDescription (o, co.Command); + continue; + } if (!WriteOptionPrototype (o, p, ref written)) continue; @@ -1264,6 +1311,17 @@ namespace Mono.Options } } + internal void WriteCommandDescription (TextWriter o, Command c) + { + var name = new string (' ', 8) + c.Name; + if (name.Length < OptionWidth - 1) { + WriteDescription (o, name + new string (' ', OptionWidth - name.Length) + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); + } else { + WriteDescription (o, name, "", 80, 80); + WriteDescription (o, CommandHelpIndentStart + c.Help, CommandHelpIndentRemaining, 80, Description_RemWidth); + } + } + void WriteDescription (TextWriter o, string value, string prefix, int firstWidth, int remWidth) { bool indent = false; @@ -1403,5 +1461,341 @@ namespace Mono.Options return StringCoda.WrappedLines (description, firstWidth, remWidth); } } + + public class Command + { + public string Name {get;} + public string Help {get;} + + public OptionSet Options {get; set;} + public Action> Run {get; set;} + + public CommandSet CommandSet {get; internal set;} + + public Command (string name, string help = null) + { + if (string.IsNullOrEmpty (name)) + throw new ArgumentNullException (nameof (name)); + + Name = name; + Help = help; + } + + public virtual int Invoke (IEnumerable arguments) + { + var rest = Options?.Parse (arguments) ?? arguments; + Run?.Invoke (rest); + return 0; + } + } + + class CommandOption : Option + { + public Command Command {get;} + + // Prototype starts with '=' because this is an invalid prototype + // (see Option.ParsePrototype(), and thus it'll prevent Category + // instances from being accidentally used as normal options. + public CommandOption (Command command, bool hidden = false) + : base ("=:Command:= " + command?.Name, command?.Name, maxValueCount: 0, hidden: hidden) + { + if (command == null) + throw new ArgumentNullException (nameof (command)); + Command = command; + } + + protected override void OnParseComplete (OptionContext c) + { + throw new NotSupportedException ("CommandOption.OnParseComplete should not be invoked."); + } + } + + class HelpOption : Option + { + Option option; + CommandSet commands; + + public HelpOption (CommandSet commands, Option d) + : base (d.Prototype, d.Description, d.MaxValueCount, d.Hidden) + { + this.commands = commands; + this.option = d; + } + + protected override void OnParseComplete (OptionContext c) + { + commands.showHelp = true; + + option?.InvokeOnParseComplete (c); + } + } + + class CommandOptionSet : OptionSet + { + CommandSet commands; + + public CommandOptionSet (CommandSet commands, MessageLocalizerConverter localizer) + : base (localizer) + { + this.commands = commands; + } + + protected override void SetItem (int index, Option item) + { + if (ShouldWrapOption (item)) { + base.SetItem (index, new HelpOption (commands, item)); + return; + } + base.SetItem (index, item); + } + + bool ShouldWrapOption (Option item) + { + if (item == null) + return false; + var help = item as HelpOption; + if (help != null) + return false; + foreach (var n in item.Names) { + if (n == "help") + return true; + } + return false; + } + + protected override void InsertItem (int index, Option item) + { + if (ShouldWrapOption (item)) { + base.InsertItem (index, new HelpOption (commands, item)); + return; + } + base.InsertItem (index, item); + } + } + + public class CommandSet : KeyedCollection + { + readonly OptionSet options; + readonly TextWriter outWriter; + readonly TextWriter errorWriter; + readonly string suite; + + HelpCommand help; + + internal bool showHelp; + + internal OptionSet Options => options; + + public CommandSet (string suite, MessageLocalizerConverter localizer = null, TextWriter output = null, TextWriter error = null) + { + if (suite == null) + throw new ArgumentNullException (nameof (suite)); + this.suite = suite; + options = new CommandOptionSet (this, localizer); + outWriter = output ?? Console.Out; + errorWriter = error ?? Console.Error; + } + + public string Suite => suite; + public TextWriter Out => outWriter; + public TextWriter Error => errorWriter; + public MessageLocalizerConverter MessageLocalizer => options.MessageLocalizer; + + protected override string GetKeyForItem (Command item) + { + return item?.Name; + } + + public new CommandSet Add (Command value) + { + if (value == null) + throw new ArgumentNullException (nameof (value)); + AddCommand (value); + options.Add (new CommandOption (value)); + return this; + } + + void AddCommand (Command value) + { + if (value.CommandSet != null && value.CommandSet != this) { + throw new ArgumentException ("Command instances can only be added to a single CommandSet.", nameof (value)); + } + value.CommandSet = this; + if (value.Options != null) { + value.Options.MessageLocalizer = options.MessageLocalizer; + } + + base.Add (value); + + help = help ?? value as HelpCommand; + } + + public CommandSet Add (string header) + { + options.Add (header); + return this; + } + + public CommandSet Add (Option option) + { + options.Add (option); + return this; + } + + public CommandSet Add (string prototype, Action action) + { + options.Add (prototype, action); + return this; + } + + public CommandSet Add (string prototype, string description, Action action) + { + options.Add (prototype, description, action); + return this; + } + + public CommandSet Add (string prototype, string description, Action action, bool hidden) + { + options.Add (prototype, description, action, hidden); + return this; + } + + public CommandSet Add (string prototype, OptionAction action) + { + options.Add (prototype, action); + return this; + } + + public CommandSet Add (string prototype, string description, OptionAction action) + { + options.Add (prototype, description, action); + return this; + } + + public CommandSet Add (string prototype, string description, OptionAction action, bool hidden) + { + options.Add (prototype, description, action, hidden); + return this; + } + + public CommandSet Add (string prototype, Action action) + { + options.Add (prototype, null, action); + return this; + } + + public CommandSet Add (string prototype, string description, Action action) + { + options.Add (prototype, description, action); + return this; + } + + public CommandSet Add (string prototype, OptionAction action) + { + options.Add (prototype, action); + return this; + } + + public CommandSet Add (string prototype, string description, OptionAction action) + { + options.Add (prototype, description, action); + return this; + } + + public CommandSet Add (ArgumentSource source) + { + options.Add (source); + return this; + } + + public int Run (IEnumerable arguments) + { + if (arguments == null) + throw new ArgumentNullException (nameof (arguments)); + + this.showHelp = false; + if (help == null) { + help = new HelpCommand (); + AddCommand (help); + } + Action setHelp = v => showHelp = v != null; + if (!options.Contains ("help")) { + options.Add ("help", "", setHelp, hidden: true); + } + if (!options.Contains ("?")) { + options.Add ("?", "", setHelp, hidden: true); + } + var extra = options.Parse (arguments); + if (extra.Count == 0) { + if (showHelp) { + return help.Invoke (extra); + } + Out.WriteLine (options.MessageLocalizer ($"Use `{Suite} help` for usage.")); + return 1; + } + var command = Contains (extra [0]) ? this [extra [0]] : null; + if (command == null) { + help.WriteUnknownCommand (extra [0]); + return 1; + } + extra.RemoveAt (0); + if (showHelp) { + if (command.Options?.Contains ("help") ?? true) { + extra.Add ("--help"); + return command.Invoke (extra); + } + command.Options.WriteOptionDescriptions (Out); + return 0; + } + return command.Invoke (extra); + } + } + + public class HelpCommand : Command + { + public HelpCommand () + : base ("help", help: "Show this message and exit") + { + } + + public override int Invoke (IEnumerable arguments) + { + var extra = new List (arguments ?? new string [0]); + var _ = CommandSet.Options.MessageLocalizer; + if (extra.Count == 0) { + CommandSet.Options.WriteOptionDescriptions (CommandSet.Out); + return 0; + } + var command = CommandSet.Contains (extra [0]) + ? CommandSet [extra [0]] + : null; + if (command == this || extra [0] == "--help") { + CommandSet.Out.WriteLine (_ ($"Usage: {CommandSet.Suite} COMMAND [OPTIONS]")); + CommandSet.Out.WriteLine (_ ($"Use `{CommandSet.Suite} help COMMAND` for help on a specific command.")); + CommandSet.Out.WriteLine (); + CommandSet.Out.WriteLine (_ ($"Available commands:")); + CommandSet.Out.WriteLine (); + foreach (var c in CommandSet) { + CommandSet.Options.WriteCommandDescription (CommandSet.Out, c); + } + return 0; + } + if (command == null) { + WriteUnknownCommand (extra [0]); + return 1; + } + if (command.Options != null) { + command.Options.WriteOptionDescriptions (CommandSet.Out); + return 0; + } + return command.Invoke (new [] { "--help" }); + } + + internal void WriteUnknownCommand (string unknownCommand) + { + CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Unknown command: {unknownCommand}")); + CommandSet.Error.WriteLine (CommandSet.Options.MessageLocalizer ($"{CommandSet.Suite}: Use `{CommandSet.Suite} help` for usage.")); + } + } } diff --git a/mcs/class/Mono.Options/Mono.Options_test.dll.sources b/mcs/class/Mono.Options/Mono.Options_test.dll.sources index 686fa1244ef..d63b1f2c83b 100644 --- a/mcs/class/Mono.Options/Mono.Options_test.dll.sources +++ b/mcs/class/Mono.Options/Mono.Options_test.dll.sources @@ -1,5 +1,6 @@ Mono.Options/BaseRocksFixture.cs Mono.Options/CollectionContract.cs +Mono.Options/CommandSetTest.cs Mono.Options/ListContract.cs Mono.Options/OptionContextTest.cs Mono.Options/OptionSetTest.cs diff --git a/mcs/class/Mono.Options/Test/Mono.Options-Test-net_4_x.csproj b/mcs/class/Mono.Options/Test/Mono.Options-Test-net_4_x.csproj new file mode 100644 index 00000000000..83ffda62b11 --- /dev/null +++ b/mcs/class/Mono.Options/Test/Mono.Options-Test-net_4_x.csproj @@ -0,0 +1,52 @@ + + + + Debug + AnyCPU + {472A5C45-8331-4849-B89F-A872CF884DA3} + Library + Mono.Options_Test_net_4_x + Mono.Options_Test_net_4_x + v4.5 + + + true + full + false + bin\Debug + DEBUG; + prompt + 4 + + + true + bin\Release + prompt + 4 + + + + + ..\lib\net_4_x\nunit.framework.dll + + + + + + + + + + + + + + + + + {115711B0-D1F2-4E50-83F9-63128E70CE05} + Mono.Options-net_4_x + + + + \ No newline at end of file diff --git a/mcs/class/Mono.Options/Test/Mono.Options/CommandSetTest.cs b/mcs/class/Mono.Options/Test/Mono.Options/CommandSetTest.cs new file mode 100644 index 00000000000..a195e43a80f --- /dev/null +++ b/mcs/class/Mono.Options/Test/Mono.Options/CommandSetTest.cs @@ -0,0 +1,249 @@ +// +// CommandSetTest.cs +// +// Authors: +// Jonathan Pryor +// +// Copyright (C) 2017 Microsoft (http://www.microsoft.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; + +#if NDESK_OPTIONS +using NDesk.Options; +#else +using Mono.Options; +#endif + +using Cadenza.Collections.Tests; + +using NUnit.Framework; + +#if NDESK_OPTIONS +namespace Tests.NDesk.Options +#else +namespace MonoTests.Mono.Options +#endif +{ + [TestFixture] + public class CommandSetTest : ListContract + { + protected override ICollection CreateCollection (IEnumerable values) + { + var set = new CommandSet ("test"); + foreach (var value in values) + set.Add (value); + return set; + } + + protected override Command CreateValueA () + { + return new Command ( + "foo", + "foo help"); + } + + protected override Command CreateValueB () + { + return new Command ( + "bar", + "bar help"); + } + + protected override Command CreateValueC () + { + return new Command ( + "baz", + "baz help"); + } + + static IEnumerable _ (params string [] a) + { + return a; + } + + [Test] + public void Constructor_SuiteRequired () + { + Assert.Throws (() => new CommandSet (null)); + } + + [Test] + public void Add_NullCommand () + { + var c = new CommandSet ("cs"); + Assert.Throws (() => c.Add ((Command)null)); + } + + [Test] + public void Add_CommandCanBeAddedToOnlyOneSet () + { + var cs1 = new CommandSet ("cs1"); + var cs2 = new CommandSet ("cs2"); + var c = new Command ("command", "help"); + cs1.Add (c); + Assert.Throws (() => cs2.Add (c)); + } + + [Test] + public void Add_SetsCommandSet () + { + var cs = new CommandSet ("cs"); + var c = new Command ("command"); + Assert.IsNull (c.CommandSet); + cs.Add (c); + Assert.AreSame (cs, c.CommandSet); + } + + [Test] + public void Add_DuplicateCommand () + { + var s = new CommandSet ("set"); + s.Add (new Command ("value")); + Assert.Throws (() => s.Add (new Command ("value"))); + } + + [Test] + public void Run_Help () + { + var o = new StringWriter (); + var e = new StringWriter (); + + var showVersion = false; + var showHelp = false; + + var git = new CommandSet ("git", output: o, error: e) { + "usage: git [--version] ... []", + "", + "Common Options:", + { "version", + "show version info", + v => showVersion = v != null }, + { "help", + "show this message and exit", + v => showHelp = v != null }, + "", + "These are common Git commands used in various situations:", + "", + "start a working area (see also: git help tutorial)", + new Command ("clone", "Clone a repository into a new directory"), + new Command ("init", "Create an empty Git repository or reinitialize an existing one"), + new Command ("thisIsAVeryLongCommandNameInOrderToInduceWrapping", "Create an empty Git repository or reinitialize an existing one. Let's make this really long to cause a line wrap, shall we?"), + }; + + var expectedHelp = new StringWriter (); + + expectedHelp.WriteLine ("usage: git [--version] ... []"); + expectedHelp.WriteLine (""); + expectedHelp.WriteLine ("Common Options:"); + expectedHelp.WriteLine (" --version show version info"); + expectedHelp.WriteLine (" --help show this message and exit"); + expectedHelp.WriteLine (""); + expectedHelp.WriteLine ("These are common Git commands used in various situations:"); + expectedHelp.WriteLine (""); + expectedHelp.WriteLine ("start a working area (see also: git help tutorial)"); + expectedHelp.WriteLine (" clone Clone a repository into a new directory"); + expectedHelp.WriteLine (" init Create an empty Git repository or reinitialize an"); + expectedHelp.WriteLine (" existing one"); + expectedHelp.WriteLine (" thisIsAVeryLongCommandNameInOrderToInduceWrapping"); + expectedHelp.WriteLine (" Create an empty Git repository or reinitialize an"); + expectedHelp.WriteLine (" existing one. Let's make this really long to"); + expectedHelp.WriteLine (" cause a line wrap, shall we?"); + + Assert.AreEqual (0, git.Run (new [] { "help" })); + Assert.AreEqual (expectedHelp.ToString (), o.ToString ()); + + var expectedHelpHelp = new StringWriter (); + expectedHelpHelp.WriteLine ("Usage: git COMMAND [OPTIONS]"); + expectedHelpHelp.WriteLine ("Use `git help COMMAND` for help on a specific command."); + expectedHelpHelp.WriteLine (); + expectedHelpHelp.WriteLine ("Available commands:"); + expectedHelpHelp.WriteLine (); + expectedHelpHelp.WriteLine (" clone Clone a repository into a new directory"); + expectedHelpHelp.WriteLine (" init Create an empty Git repository or reinitialize an"); + expectedHelpHelp.WriteLine (" existing one"); + expectedHelpHelp.WriteLine (" thisIsAVeryLongCommandNameInOrderToInduceWrapping"); + expectedHelpHelp.WriteLine (" Create an empty Git repository or reinitialize an"); + expectedHelpHelp.WriteLine (" existing one. Let's make this really long to"); + expectedHelpHelp.WriteLine (" cause a line wrap, shall we?"); + expectedHelpHelp.WriteLine (" help Show this message and exit"); + + o.GetStringBuilder ().Clear (); + Assert.AreEqual (0, git.Run (new [] { "help", "--help" })); + Assert.AreEqual (expectedHelpHelp.ToString (), o.ToString ()); + } + + [Test] + public void Run_Command () + { + var a = 0; + var b = 0; + var c = new CommandSet ("set") { + new Command ("a") { Run = v => a = v.Count () }, + new Command ("b") { Run = v => b = v.Count () }, + }; + Assert.AreEqual (0, c.Run (new [] { "a", "extra" })); + Assert.AreEqual (1, a); + Assert.AreEqual (0, b); + + a = b = 0; + Assert.AreEqual (0, c.Run (new [] { "b" })); + Assert.AreEqual (0, a); + Assert.AreEqual (0, b); + + Assert.AreEqual (0, c.Run (new [] { "b", "one", "two" })); + Assert.AreEqual (0, a); + Assert.AreEqual (2, b); + } + + [Test] + public void Run_HelpCommandSendsHelpOption () + { + var e = new Command ("echo"); + e.Run = (args) => e.CommandSet.Out.WriteLine (string.Join (" ", args)); + + var o = new StringWriter (); + var c = new CommandSet ("set", output:o) { + e, + }; + Assert.AreEqual (0, c.Run (new [] { "help", "echo" })); + + var expected = $"--help{Environment.NewLine}"; + var actual = o.ToString (); + Assert.AreEqual (expected, actual); + } + + [Test] + public void Run_NullArgument () + { + var c = new CommandSet ("c"); + Assert.Throws (() => c.Run (null)); + } + } +} + diff --git a/mcs/class/Mono.Options/Test/Mono.Options/CommandTest.cs b/mcs/class/Mono.Options/Test/Mono.Options/CommandTest.cs new file mode 100644 index 00000000000..5805f280c33 --- /dev/null +++ b/mcs/class/Mono.Options/Test/Mono.Options/CommandTest.cs @@ -0,0 +1,102 @@ +// +// CommandSetTest.cs +// +// Authors: +// Jonathan Pryor +// +// Copyright (C) 2017 Microsoft (http://www.microsoft.com) +// +// Permission is hereby granted, free of charge, to any person obtaining +// a copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to +// permit persons to whom the Software is furnished to do so, subject to +// the following conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +// + +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Globalization; +using System.IO; +using System.Linq; + +#if NDESK_OPTIONS +using NDesk.Options; +#else +using Mono.Options; +#endif + +using Cadenza.Collections.Tests; + +using NUnit.Framework; + +#if NDESK_OPTIONS +namespace Tests.NDesk.Options +#else +namespace MonoTests.Mono.Options +#endif +{ + [TestFixture] + public class CommandTest + { + [Test] + public void Constructor_NameRequired () + { + Assert.Throws (() => new Command (name: null, help: null)); + } + + [Test] + public void Constructor () + { + var c = new Command ("command", "help"); + Assert.AreEqual ("command", c.Name); + Assert.AreEqual ("help", c.Help); + } + + [Test] + public void Invoke_CallsRun () + { + bool runInvoked = false; + var c = new Command ("command") { + Run = v => runInvoked = true, + }; + Assert.AreEqual (0, c.Invoke (null)); + Assert.IsTrue (runInvoked); + } + + [Test] + public void Invoke_RequiresNothing () + { + var c = new Command ("c"); + Assert.AreEqual (0, c.Invoke (null)); + } + + [Test] + public void Invoke_UsesOptions () + { + bool showHelp = false; + var c = new Command ("c") { + Options = new OptionSet { + { "help", v => showHelp = v != null }, + }, + }; + Assert.AreEqual (0, c.Invoke (new [] { "--help" })); + Assert.IsTrue (showHelp); + } + } +} +