CuiMallet is a .NET library that makes command-line interfaces. It depends on .NET Standard 2.1.
CuiMallet.CSharp is available as the NuGet package.
CuiMallet makes it easy to implement the conventions for command-line options that POSIX recommends [1] and to which GNU adds long options [2].
First, create an OptionSchema
object as follows:
var schema = Options.NewSchema();
Then, add the definition of the command-line options to the OptionSchema
object. For example, to add the --help
option, which has the shortened form
-h
, no argument, and the description "Show help message"
for its help
message, use the Add(string, char?, string)
method against the OptionSchema
object as follows:
schema = schema.Add("help", 'h', "Show help message");
Note that the OptionSchema
object is immutable, so the Add
method returns
a new OptionSchema
object.
After adding all the options, use the Parse(string[])
method against the
schema
, with the specified string
array representing the command-line
arguments, which a static method Main(string[])
typically provides at the
entry point of a program, as follows:
public static void Main(string[] args)
{
var schema = Options.NewSchema()
// Adds options to schema.
⋮
.Add("help", 'h', "Show help message");
try
{
var setting = schema.Parse(args);
var options = setting.Options;
var arguments = setting.Arguments;
// Customizes the program behavior according to the options and
// arguments.
⋮
}
catch (OptionParsingException e)
{
// Handles the exception that the Parse method may throw.
⋮
}
}
The Parse(string[])
method returns the Setting
object,
which contains the options and the arguments. The options
represent the parsed
options in order of appearance. Each option is given as an
Option
object, and the type of the options
is
IEnumerable<Option>
. Meanwhile, the arguments
represent the remaining
non-option arguments. The type of the arguments
is IEnumerable<string>
.
In addition, the Parse(string[])
method must be in the try
block followed by
the catch
clause handling an OptionParsingException
because it may throw the
exception when the specified option is not valid for the schema.
Here, there is a relationship between the options and the non-option arguments illustrated as follows:
command [Options...] [
--
] [Arguments...]
-
A component of Options... is a list separated with whitespace characters, which contains a string starting with a hyphen (
-
) character and consisting of two or more characters (e.g.,--help
,-h
). The string starting with a double hyphen (--
) is called an option, and the string after the double hyphen is called the option name. The option name consists of alphanumeric characters and hyphens. Meanwhile, the string consisting of a single character preceded by a single hyphen (-
) is called a shortened-form option, and the character after the hyphen is called a short name of the option. The short names are single alphanumeric characters. -
A component of Arguments... is a list of strings separated with whitespace characters.
Each component enclosed in square brackets can be omitted. However, a double
hyphen (--
) must be placed between Options... and Arguments... only if the
first element of Arguments... begins with a hyphen character and is not a
hyphen exactly.
Note that the option name in CuiMallet corresponds to the long option name in
GNU getopt_long
[3], and the short name corresponds to the option
name in POSIX getopt
[4].
There are two types of options: the options that can't have an option argument
(e.g., --help
as described above), and that must have a single-option argument.
For example, suppose the --file
option, which has the shortened form -f
,
requires an option argument. Then its argument must be supplied in any one of
the following forms:
--file ARGUMENT
--file=ARGUMENT
-f ARGUMENT
-fARGUMENT
Where you must replace the ARGUMENT
with the actual option argument.
Thus, --file index.html
, --file=infex.html
, -f index.html
, and
-findex.html
are equivalent. However, if the ARGUMENT
must be a zero-length
string (i.e., an empty string), you cannot use the -fARGUMENT
form.
Note that, in general, two types of options have an option argument: the required argument option and the optional argument option. The former cannot omit an option argument, but the latter can. However, CuiMallet has not yet implemented the latter.
Specifying -a -b -c
and -abc
are equivalent. The concatenated options,
except the last one, can't have an option argument. Only the last one can have
an option argument as follows:
-abcf ARGUMENT
(or-abcfARGUMENT
)
Where the ARGUMENT
is of the -f
option, and -a
, -b
, and -c
options cannot have an option argument.
You can abbreviate the option names as long as the abbreviations are unique. For
example, suppose there are only three options in the schema: --help
,
--verbose
, and --version
. Specifying --h
, --he
, --hel
, and --help
are equivalent. Likewise, --verb --vers
is equivalent to
--verbose --version
. However, specifying --ver
causes an error because two
options start with it.
To add the --file
option (as described above) to the schema, use the
Add(string, char?, string, string)
method against the schema object as
follows:
schema = schema.Add("file", 'f', "FILE", "Specify an input file");
To get the value of the actual option argument, use a
RequiredArgumentOption
object as follows:
var options = setting.Options;
foreach (o in options)
{
if (o is RequiredArgumentOption a)
{
var arg = a.ArgumentValue;
⋮
}
⋮
}
Thus, the ArgumentValue
property of a RequiredArgumentOption
object provides
the value of the option argument.
You can specify the same option two or more times. For example, suppose the
option -f
, as noted above, which takes an argument, is specified on the
command line as follows:
-f foo -f bar -f baz
In such cases, the ArgumentValues
property of a RequiredArgumentOption
object is useful. The following code shows how to use the property:
var options = setting.Options;
foreach (o in options)
{
if (o is RequiredArgumentOption a)
{
var arg = a.ArgumentValue;
var all = string.Join(",", a.ArgumentValues);
Console.WriteLine($"{arg} {all}");
}
}
The ArgumentValues
property returns the values of all the option arguments
corresponding to the same option in occurrence order. Note that they do not
contain the option arguments of the options specified after it. Thus, the output
to the console is as follows:
foo foo
bar foo,bar
baz foo,bar,baz
You can also specify a callback function that takes the Option
or
RequiredArgumentOption
object as the argument when adding the definition of
the command-line options to the OptionSchema
object. From invoking the
Parse(string[])
method of the OptionSchema
object until returning, the
function is called to provide the Option
or RequiredArgumentOption
object
each time the object is created.
To add the option, which has no option argument, with a callback function to the
OptionSchema
object, use the Add(string, char?, string, Action<Option>)
method as follows:
// Adds the definition of an Option with the callback function.
schema = schema.Add(
"help",
'h',
"Show help message",
o =>
{
// typeof(o) is Option.
⋮
});
In the same way, to add the option, which has an option argument, with a
callback function to the OptionSchema
object, use the
Add(string, char?, string, string, Action<RequiredArgumentOption>)
method as
follows:
// Adds the definition of a RequiredArgumentOption with the callback
// function.
schema = schema.Add(
"file",
'f',
"FILE",
"Specify an input file",
o =>
{
// typeof(o) is RequiredArgumentOption.
⋮
});
You can generate the help message of command-line options with the
OptionSchema
object. To get the help message, use the GetHelpMessage()
method against the schema
as follows:
public static void Main(string[] args)
{
var schema = Options.NewSchema()
.Add("file", 'f', "FILE", "Specify an input file")
.Add("help", 'h', "Show help message");
PrintUsage(schema, Console.Out);
}
private static void PrintUsage(OptionSchema schema, TextWriter output)
{
var usage = new[]
{
"usage: command [Options...] [--] Arguments...",
"",
"Options are:",
};
var messages = usage.Concat(schema.GetHelpMessage());
foreach (var m in messages)
{
output.WriteLine(m);
}
}
The type of the value the method returns is IEnumerable<string>
. The output to
the console is as follows:
usage: command [Options...] [--] Arguments...
Options are:
-f, --file FILE Specify an input file
-h, --help Show help message
If the description has to be composed of two or more lines, you can split it
into them by inserting a line feed character ('\n'
) as a line separator. See
the following example:
var schema = Options.NewSchema()
.Add(
"file",
'f',
"FILE",
"Specify an input file\n"
+ "The FILE can be - for standard input")
.Add("help", 'h', "Show help message");
⋮
As in the previous example, the output to the console is as follows:
⋮
-f, --file FILE Specify an input file
The FILE can be - for standard input
-h, --help Show help message
Note that the GetHelpMessage()
method sorts options by name.
The Parse(string[])
method, as mentioned earlier, throws an
OptionParsingException
when the specified argument to the schema is invalid.
Specific cases are as follows:
Unknown option
The specified option was not found in the schema.Missing an argument
The specified option requires an argument, but no argument was given.Unable to get an argument
The specified option takes no argument, but the argument was given.Ambiguous option
The name of the specified option is abbreviated, but the abbreviations were not unique.
In most cases, all you need in the catch clause is to write the message of the exception to the standard error output, print the usage, and then exit with a non-zero status code. The following code shows a typical example:
try
{
var setting = schema.Parse(args);
⋮
}
catch (OptionParsingException e)
{
var output = Console.Error;
output.WriteLine(e.Message);
PrintUsage(schema, output);
Environment.Exit(1);
}
When specifying a callback function for a required argument option, the value of
the option argument often has to be validated in the function. In that case, you
can throw an OptionParsingException
when the validation fails so that the
catch clause noted above handles the exception as well as the other
OptionParsingException
s.
For example, suppose the --count
option, which requires the option argument
representing a positive integer. You can parse the command-line options to get
the value of the option argument, as follows:
private const int DefaultCount = 3;
private static int Count { get; set; } = DefaultCount;
private static void ParseCount(RequiredArgumentOption o)
{
var v = o.ArgumentValue;
if (!int.TryParse(v, out var num) || num < 0)
{
var n = o.ArgumentName;
throw new OptionParsingException(
o, $"option '{o}': the value '{v}' is invalid for {n}");
}
Count = num;
}
public static void Main(string[] args)
{
var schema = Options.NewSchema()
.Add(
"count",
'c',
"NUM",
"Specifies the count",
ParseCount)
⋮
try
{
var setting = schema.Parse(args);
⋮
}
catch (OptionParsingException e)
{
var output = Console.Error;
output.WriteLine(e.Message);
PrintUsage(schema, output);
Environment.Exit(1);
}
⋮
The Count
property returns 10 after parsing args
containing --count=10
.
But if replacing it with --count=abc
, you have the output to the console as
follows:
option '--count=abc': the value 'abc' is invalid for NUM
- Maroontress.Cui namespace
- Visual Studio 2022 Version 17.12 or .NET 9.0 SDK (SDK 9.0.101)
git clone URL
cd CuiMallet.CSharp
dotnet build --configuration Release
dotnet tool install -g dotnet-reportgenerator-globaltool
dotnet test --configuration Release --no-build \
--logger "console;verbosity=detailed" \
--collect:"XPlat Code Coverage" \
--results-directory MsTestResults
reportgenerator -reports:MsTestResults/*/coverage.cobertura.xml \
-targetdir:Coverlet-html
[1] POSIX, Utility Conventions
[2] The GNU C Library, Program Argument Syntax Conventions
[3] The GNU C Library, Parsing Long Options with getopt_long
[4] POSIX, getopt
, optarg
, opterr
, optind
, optopt
— command option parsing