As of v0.20.0 JCLI is using a fully rewritten code base, which has major breaking changes, and as of v0.30.0 a largely rewritten codebase.
JCLI is a library to aid in the creation of command line tooling, with an aim of being easy to use, while also allowing the individual parts of the library to be used on their own, aiding more dedicated users in creation of their own CLI core.
As a firm believer of good documentation, JCLI is completely documented with in-depth explanations where needed. In-browser documentation can be found here.
Tested on Windows and Ubuntu 18.04.
- Overview
- Features
- Quick Start
- Creating a default command
- Positional Arguments
- Registering commands
- Running the program
- Named arguments
- Optional Arguments
- Arguments with multiple names
- Named Commands
- User Defined argument binding
- User Defined argument validation
- Per-argument binding
- Unparsed Raw Arg List
- Inheritance
- Argument groups
- Bash Completion
- Command Introspection
- Light-weight command parsing
- Light-weight command help text
- Argument configuration
- Using JCLI without Dub
- Contributing
-
Building:
-
This library was primarily built using Meson as the build system, so should be fully integratable into other Meson projects.
-
All individual parts of this library are intended to be reusable. Allowing you to build your own CLI core using these already-made components, if desired.
-
All individual parts of this library are split into sub packages, so you can only include what you need if you're not using the main
jcli
package.
-
-
Argument parsing:
-
Named and positional arguments.
-
Boolean flags.
-
Optional arguments using the standard
Nullable
type, or by giving that field a default value. -
User-Defined argument binding (string -> any_type_you_want) - blanket and per-argument.
-
User-Defined argument validation (via UDAs that follow a convention).
-
Pass through unparsed arguments (
./mytool parsed args -- these are unparsed args
). -
Capture overflowed arguments (
./mytool arg1 arg2 overflow1 overflow2
) -
Automatic error messages for missing and malformed arguments.
-
-
Commands:
-
Standard command line format (
./mytool command args --named=value ...
). -
Automatic command dispatch.
-
Defined using UDAs, and are automatically discovered.
-
Supports a default command.
-
Supports named commands that allow for multiple words and per-command argument parsing.
-
Support for command inheritance(currently broken). -
Only
structs
are allowed (I don't see why you'd want to use classes for this anyway).
-
-
Help text:
-
Automatically generated with slight ability for customisation.
-
Works for the default command.
-
Works for exact matches for named commands.
-
Works for partial matches for named commands.
-
Arguments can be displayed in organised groups.
-
-
Utilities:
-
Bash completion support. -
Decent support for writing and parsing ANSI text via jcli.
-
An ANSI-enabled text buffer, for easier and efficient control over coloured, non-uniform text output.
-
This is a brief overview, for slightly more in-depth examples please look at the fully-documented examples folder.
The default command is the command that is ran when you don't specify any named command. e.g. mytool 60 20 --some=args
would call the default command if it exists:
// inside of app.d
module app;
import jcli
@CommandDefault("The default command.")
struct DefaultCommand
{
int onExecute()
{
return 0;
}
}
The @CommandDefault
is a UDA (User Defined Attribute) where the first parameter is the command's description.
All commands must define an onExecute
function, which either returns void
, or an int
that will be used as the program's exit/status code.
As a side note, an initial dub project does not include the module app;
statement shown in the example above. I've added it as we'll need to directly reference the module in a later section.
To start off, let's make our default command take a number as a positional arg. If this number is even then return 1
, otherwise return 0
.
Positional arguments are expected to exist in a specific position within the arguments passed to your program.
For example the command mytool 60 yoyo true
would have 60
in the 0th position, yoyo
in the 1st position, and true
in the 2nd position:
@CommandDefault("The default command.")
struct DefaultCommand
{
@ArgPositional("The number to check.")
int number;
int onExecute()
{
return number % 2 == 0 ? 1 : 0;
}
}
We create the field member int number;
and decorate it with the @ArgPositional
UDA to specify it as a positional argument.
The first parameter is an optional name we can give the parameter, which is shown in the command's help text, but serves no other function.
The last parameter is simply a description.
An example of the help text is shown in the Running your program section, which demonstrates why you should provide a name to positional arguments.
The position of a positional argument is defined by the order it appears in your command, relative to other positional arguments.
For example:
@CommandDefault
struct Command
{
@ArgPositional // I'm at position 0
int one;
@ArgPositional // I'm at position 1
int two;
@ArgPositional // I'm at position 2
int three;
}
// myTool.exe one two three
To use our new command, we just need to register it first:
The CommandLineInterface has been removed, so this is wrong. There
arewill be simpler alternatives.
module app;
import jcli;
import std.stdio;
// This is still in app.d
int main(string[] args)
{
auto executor = new CommandLineInterface!(app);
const statusCode = executor.parseAndExecute(args);
writefln("Program exited with status code %s", statusCode);
return statusCode;
}
// Imagine our previous command code is here.
Our main function is defined to return an int
(status code) while also taking in any arguments passed to us via the args
parameter.
First, we create executor
which is a CommandLineInterface
instance. To discover commands, it must know which modules to look in. Remember at the start I told you to write module app;
at the start of the file? So what we're doing here is passing our module called app
into CommandLineInterface
, so that it can find all our commands there.
For future reference, you can pass any amount of modules into CommandLineInterface
, not just a single one.
Second, we call executor.parseAndExecute(args)
, which returns a status code that we store into the variable statusCode
. This parseAndExecute
function will parse the arguments given to it; figure out which command to call; create an instance of that command; fill out the command's argument members, and then finally call the command's onExecute
function. The rest is pretty self explanatory.
Your app.d file should look something like this.
First, let's have a look at the help text for our default command:
$> ./mytool --help
mytool DEFAULT number
Description:
The default command.
Positional Arguments:
number The number to check.
So we can see that the help text matches the structure of our DefaultCommand
struct.
Next, let's try out our command!
# Even number
$> ./mytool 60
Program exited with status code 1
# Odd number
$> ./mytool 59
Program exited with status code 0
# No number
$> ./mytool
temp.exe: Expected 1 positional arguments but got 0 instead. Missing the following required positional arguments: number
Program exited with status code -1
# Too many numbers
$> ./mytool 1 2
temp.exe: Too many positional arguments near '2'. Expected 1 positional arguments.
Program exited with status code -1
Excellent, we can see that with little to no work, our command performs as expected while rejecting invalid use cases.
Now let's add a mode that will enable reversed output (return 1
for odd number and 0
for even). To do this we should add a named argument called --mode
that maps directly to an enum
:
enum Mode
{
normal, // Even returns 1. Odd returns 0.
reversed // Even returns 0. Odd returns 1.
}
@CommandDefault("The default command.")
struct DefaultCommand
{
@ArgPositional("The number to check.")
int number;
@ArgNamed("mode", "Which mode to use.")
Mode mode;
int onExecute()
{
if(this.mode == Mode.normal)
return number % 2 == 0 ? 1 : 0;
else
return number % 2;
}
}
Inside DefaultCommand
we create a member field called mode
that is decorated with the @ArgNamed
UDA and has enum type. JCLI knows how to convert an argument value into an enum value.
The first parameter is the name of the argument, which is actually important this time as this determines what name the user needs to use.
The second parameter is just the description.
Then inside of onExecute
we just check what mode
was set to and do stuff based off of its value.
Let's have a quick look at the help text first, to see the changes being reflected:
$> ./mytool --help
temp.exe DEFAULT number --mode
Description:
The default command.
Positional Arguments:
number The number to check.
Named Arguments:
--mode Which mode to use.
And now let's test our functionality:
# JCLI supports most common argument styles.
# Even (Normal)
$> mytool 60 --mode normal
Program exited with status code 1
# Even (Reversed)
$> mytool 60 --mode=reversed
Program exited with status code 0
# Bad value for mode
$> mytool 60 --mode non_existing_mode
temp.exe: Mode does not have a member named 'non_existing_mode'
Program exited with status code -1
# Can safely assume Odd behaves properly.
# Now, we haven't marked --mode as optional, so...
$> mytool 60
temp.exe: The following required named arguments were not found: mode
Program exited with status code -1
We can see that --mode
is working as expected, however notice that in the last case, the user isn't allowed to leave out --mode
since it's not marked as optional.
So to make our mode
argument optional, we add ArgConfig.optional
UDA:
@CommandDefault("The default command.")
struct DefaultCommand
{
@ArgPositional("The number to check.")
int number;
@ArgNamed("mode", "Which mode to use.")
@(ArgConfig.optional)
Mode mode;
int onExecute()
{
if(mode == Mode.normal)
return number % 2 == 0 ? 1 : 0;
else
return number % 2;
}
}
You could also give it a default value. If the default value is other than the initial value (Mode.init
, which would be Mode.normal
in this case), it would infer that it's optional on its own.
Another way is to make it Nullable
, which may be useful sometimes, but in this case it makes it more complicated than anything.
@CommandDefault("The default command.")
struct DefaultCommand
{
@ArgPositional("The number to check.")
int number;
@ArgNamed("mode", "Which mode to use.")
Nullable!Mode mode;
int onExecute()
{
if(this.mode.get(Mode.normal) == Mode.normal)
return number % 2 == 0 ? 1 : 0;
else
return number % 2;
}
}
The other change we've made is that onExecute
now uses mode.get(Mode.normal)
which returns Mode.normal
if the --mode
option is not provided.
First, let's look at the help text, as it very slightly changes for nullable arguments:
$> ./mytool --help
temp.exe DEFAULT number [--mode]
Description:
The default command.
Positional Arguments:
number The number to check.
Named Arguments:
--mode Which mode to use.
Notice the "Usage" line. --mode
has now become [--mode]
to indicate it is optional.
So now let's test that the argument is now optional:
# Even (implicitly Normal)
$> ./mytool 60
Program exited with status code 1
# Even (Reversed)
$> ./mytool 60 --mode reversed
Program exited with status code 0
While --mode
is nice and descriptive, it'd be nice if we could also refer to it via -m
wouldn't it?
Here is where the very simple concept of "patterns" comes into play. Patterns are just arrays of possible names:
@CommandDefault("The default command.")
struct DefaultCommand
{
@ArgNamed(["mode", "m"], "Which mode to use.")
Nullable!Mode mode;
// omitted as it's unchanged...
}
You can make it (subjectively) nicer by separating the names with a pipe (|
) within a single string:
@ArgNamed("mode|m", "Which mode to use.")
All we've done is changed @ArgNamed
's name from "mode"
to "mode|m"
, which basically means that we can use either --mode
or -m
to set the mode.
-mode
and--m
also work.
The option names must be ASCII.
You can have as many values within a pattern as you want. Named Arguments cannot have whitespace within their patterns though.
Let's do a quick test as usual:
$> ./mytool 60 -m normal
Program exited with status code 1
# And here's the help text
$> ./mytool --help
temp.exe DEFAULT number [--mode|-m]
Description:
The default command.
Positional Arguments:
number The number to check.
Named Arguments:
--mode|-m Which mode to use.
Named commands are commands that... have a name. For example git commit
; git remote add
; dub init
, etc. are all named commands.
It's really easy to make a named command. Let's change our default command into a named command:
// Renamed from DefaultCommand
@Command("assert|a|is-even", "Asserts that a number is even.")
struct AssertCommand
{
// ...
}
Basically, we change @CommandDefault
to @Command
, then we just pass a pattern (yes, commands can have multiple names!) as the first parameter for the @Command
UDA, and move the description into the second parameter.
Command patterns cannot have spaces in them, but you can group commands, creating command hierachies (described later not described yet).
As a bit of a difference, let's test the code first:
# We have to specify a name now. JCLI will offer suggestions!
$> ./mytool 60
temp.exe: Unknown command
Did you mean:
assert Asserts that a number is even.
# Passing cases (all producing the same output)
$> ./mytool assert 60
$> ./mytool a 60
$> ./mytool is-even 60
Program exited with status code 1
JCLI has "smart" help text when it comes to displaying named commands. Observe here that JCLI is careful to only display one of the possible names for commands that may have multiple names:
# JCLI will always display the first name of each commands' pattern.
$> ./mytool --help
Available commands:
assert Asserts that a number is even.
The other feature of this help text is that JCLI has support for partial command matches:
# So let's first start with a tool that has two commands.
$> ./mytool --help
Available commands:
assert - Asserts that a number is even.
do-a - Does A
# If we have a partial match to a command, then JCLI will filter the results down.
$> ./mytool do
mytool.exe: Unknown command 'do'.
Did you mean:
do-a - Does A
# If the command has multiple names, then JCLI is careful to use the correct name for the partial match.
# Remember that "assert" is also "is even".
$> ./mytool is --help
Available commands:
assert - Asserts that a number is even.
JCLI has support for users specifying their own functions for converting an argument's string value into the final value passed into the command instance.
By default, all arguments are converted to the right types using std.conv.to
.
While I won't go over them directly, here's the documentation for lookup rules regarding binders, for those of you who are interested.
Let's recreate the cat
command, which takes a filepath and then outputs the contents of that file.
Instead of asking JCLI for just a string though, let's create an arg binder that will construct a File
(from std.stdio) from the string, so our command doesn't have to do any file loading by itself.
First, we need to create the arg binder:
// app.d still
import std.stdio : File;
import jcli : Result;
// You can optionally attach an exit code to a result.
enum FileErrorCodes
{
notFound = 200;
}
@Binder
ResultOf!File fileBinder(string arg)
{
import std.file : exists;
return (arg.exists)
? ok(File(arg, "r"))
: fail!File("File does not exist: "~arg, FileErrorCodes.notFound); // Second arg is optional
}
First of all we import File
from the std.stdio
module and Result
from jcli
.
Second, we create a function, decorated with @Binder
, that follow a specific convention for its signature:
@Binder
ResultOf!<OutputType> <anyNameItDoesntMatter>(string arg);
The return type is a ResultOf
, whose <OutputType>
is the type of the value that the binder sets the argument to, which is a File
in our case.
The arg
parameter is the raw string provided by the user, for whichever argument we're binding from.
Finally, we check if the file exists, and if it does we return a ok!File
with a File
opened in read-only mode. If it doesn't exist then we
return a fail!File
alongside a user-friendly error message.
Arg binders need to be marked with the @Binder
UDA so that the CommandLineInterface
class can discover them. Talking about CommandLineInterface
, it'll automatically discover any arg binder from the modules you tell it about, just like it does with commands.
Let's now create our new command:
@Command("cat", "Displays the contents of a file.")
struct CatCommand
{
@ArgPositional("filePath", "The path to the file to display.")
File file;
void onExecute()
{
import std.stdio : writeln;
foreach(lineInFile; this.file.byLine())
writeln(lineInFile);
}
}
The most important thing of note here is, notice how the file
variable has the type File
, and recall that our arg binder's return type also has the type ResultOf!File
? This allows the arg binder to know that it has a function to convert the user's provided string into a File
for us.
Our onExecute
function is nothing overly special, it just displays the file line by line.
Test time. Let's make it show the contents of our dub.json
file, which is within the root of our project:
$> ./mytool cat ./dub.json
{
"authors": [
"Sealab"
],
"copyright": "Copyright ┬® 2020, Sealab",
"dependencies": {
"jcli": "~>0.10.0"
},
"description": "A minimal D application.",
"license": "proprietary",
"name": "mytool"
}
Program exited with status code 0
# And just for good measure, let's see what happens if the file doesn't exist
$> ./mytool cat non-existing-file
temp.exe: File does not exist: non-existing-file
Very simple. Very useful.
Simple example:
@PreValidate
takes a sequence of functions that will be called on the raw input of the user, before the value gats converted and bound to the argument field.
@PostValidate
also takes a sequence of functions that will be called on the argument value after the conversion.
Functions passed to these UDA's must return ResultOf!void
, which would include the message displayed to the user when the validation fails.
Of course, you can pass either lambdas, or normal function aliases.
This is what the above example would look like using these two validators:
@Command("cat", "Displays the contents of a file.")
struct CatCommand
{
@ArgPositional("filePath", "The path to the file to display.")
@PreValidate!(
str => !str.endsWith(".json")
? fail("Expected file to end with .json.")
: ok())
@PostValidate!(
file => file.size() <= 2
? ok()
: fail!void())
File file;
// omitted...
}
The interface is still kind of ugly, IMO it should be simplified significantly.
A complicated example with opCall
It's cool and all being able to very easily create arg binders, but sometimes commands will need validation logic involved.
For example, some commands might only want files with a .json
extention, while others may not care about extentions. So putting this logic into the arg binder itself isn't overly wise.
Some arguments may need validation on the pre-arg-binded string, whereas others may need validation on the post-arg-binded value. Some may need both!
JCLI handles all of this via argument validators.
Let's start off with the first example, making sure the user only passes in files with a .json
extention, and apply it to our cat
command. Code first, explanation after:
struct HasExtention
{
string wantedExtention;
ResultOf!void opCall(string arg)
{
import std.algorithm : endsWith;
return arg.endsWith(this.wantedExtention)
? ok()
: fail!void("Expected file to have extention of "~this.wantedExtention);
}
}
@Command("cat", "Displays the contents of a file.")
struct CatCommand
{
@ArgPositional("filePath", "The path to the file to display.")
@PreValidate!(HasExtention(".json"))
File file;
// omitted...
}
Before I continue, I want to explicitly state that this validator wants to perform validation on the raw string that the user provides (pre-arg-binded) and not on the final value (post-arg-binded). This is referred to as "Pre Validation". So on that note...
Most importantly, we define the function that will be called when that follows the following convention:
ResultOf!void opCall(string arg);
This is the function that performs the actual validation (in this case, "Pre" validation).
It returns ok()
if there are no validation errors, otherwise it returns fail!void()
and optionally provides an error string as a user-friendly error (one is automatically generated otherwise).
The return type is a ResultOf!void
, so a result that doesn't contain a value, but still states whether there was a fail or a ok.
The first parameter to our function is the raw string that the user has provided us.
So for our HasExtention
validator, all we do is check if the user's file path ends with this.wantedExtention
, which we set the value of later.
Now, inside CatCommand
all we've done is attach our HasExtention
struct as a UDA (and if you're not familiar with D, congrats, you just made your first UDA!). JCLI will automatically detect that @HasExtention
is a pre-bind validator because it is decorated with @PreValidator
.
Because D is wonderful, it will automatically generate a constructor for us where the first parameter sets the wantedExtention
member. So @HasExtention(".json")
will set the extention we want to ".json"
.
And that's literally all there is to it, let's test:
# Passing
$> ./mytool cat ./dub.json
[contents of dub.json since validation was a ok]
Program exited with status code 0
# Failing
$> ./mytool cat ./.gitignore
temp.exe: Expected file to have extention of .json
Program exited with status code -1
The other type of validation is post-arg-binded validation, which performs validation on the final value provided by an arg binder.
Let's make a validator that ensures that the file is under a certain size:
struct MaxSize
{
ulong maxSize;
ResultOf!void opCall(File file)
{
return file.size() <= this.maxSize
? ok()
: fail!void("File is too large.");
}
}
@Command("cat", "Displays the contents of a file.")
struct CatCommand
{
@ArgPositional("filePath", "The path to the file to display.")
@PreValidate!(HasExtention(".json"))
@PostValidate!(MaxSize(2))
File file;
// omitted...
}
The convention for post-arg-binded validation is almost exactly the same as pre-arg-binded validation, it also functions in exactly the same way:
ResultOf!void opCall(<TYPE_OF_VALUE_TO_VALIDATE> value);
The only difference is that the first parameter isn't a string
, but instead the type of value that this validator will work with.
You must also mark the struct with @PostValidator
instead of @PreValidator
.
Validators can have different overloads of this function if required. You can even make it a template. JCLI is fine with any of that.
We've set the max size to something really small, so we can easily test that it works:
$> ./mytool cat ./dub.json
temp.exe: File is too large.
Program exited with status code -1
There is seemingly a fatal flaw with the arg binding system.
Imagine we had a copy
command that copies the contents of a file into another file:
@Command("copy", "Copies a file")
struct CopyCommand
{
@ArgPositional("The source file.")
File source;
@ArgPositional("The destination file.")
File destination;
void onExecute()
{
foreach(line; source.byLine)
destination.writeln(line);
}
}
The issue here is that source
needs to be opened in read-only mode(r
), however destination
needs be written in truncate/write mode(w
).
If we were to create a normal @Binder
, we wouldn't be able to tell it the difference between the two files since we're limited in the amount
of information that is passed to an arg binder.
What we need is a way to specify the binding behavior on a per-argument basis.
While you could do a hackish thing such as creating two separate file types (ReadOnlyFile
and WriteFile
) then making arg binders for them, there's actually
a much easier solution - @UseConverter
:
import std.stdio : File;
ResultOf!File openReadOnly(string arg)
{
import std.file : exists;
return (arg.exists)
? ok!File(File(arg, "r"))
: fail!File("The file doesn't exist: "~arg);
}
@Command("copy", "Copies a file")
struct CopyCommand
{
@ArgPositional("The source file.")
@UseConverter!openReadOnly
File source;
@ArgPositional("The destination file.")
@UseConverter!(arg => ok!File(File(arg, "w")))
File destination;
void onExecute()
{
foreach(line; source.byLine)
destination.writeln(line);
}
}
To start off, we create the fairly self-explanatory openReadOnly
function which looks exactly like an @Binder
, except it doesn't have the UDA attached to it.
Next, we attach @UseConverter!openReadOnly
onto our source
argument. This tells JCLI to use our openReadOnly
function as this argument's binder.
Finally, we attach @UseConverter!(/*lambda*/)
onto our destination
argument, for the same reasons as above. A lambda is used here for demonstration purposes.
And just like that we have now solved overcome our initial issue of "how do I customise binding for arguments of the same type?" in a simple, sane manner.
I'd like to mention that this feature works alongside the usual arg binding behavior. In other words, you can define an @Binder
for a type which will
serve as the default method for binding, but then for those awkward, one-off cases you can use @UseConverter
to specify a different binding behavior on a per-argument
basis.
You don't have to validate everything before the command is started. You can just take strings and open your files in
onExecute
, and then do additional validation there, if it's too complicated or messy to express it with UDA's.
In some cases you might want to stop parsing arguments and just get them as raw strings. JCLI supports this use case by allowing raw arguments to appear after a long double-dash (--
) parameter in the command line: ./mytool args to parse -- args to pass as is
.
Commands can access the raw arg list like so:
@Command("echo", "Echos the raw arg list.")
struct EchoCommand
{
@ArgRaw
string[] rawArgs;
void onExecute()
{
import std.stdio;
writeln(rawArgs);
}
}
Simply make a field of type string[]
, then mark it with @ArgRaw
, and then voila:
$> ./mytool echo -- Hello world, please be kind.
["Hello", "world", "please", "be", "kind."]
Program exited with status code 0
As of v0.12.0 inheritance is currently in a broken state, please see issue #44 for a description of the issue, as well as a mitigation suggestion.
First question yourself, why you'd want to use inheritance and/or classes with commands in the first place, what problem does it solve. Then solve your problem in a simpler way.
JCLI supports command inheritance.
The only rules with inheritance are:
-
Only concrete classes can be marked with
@Command
. -
Concrete classes must have
onExecute
defined, either by a base class or directly.
Other than that, go wild. Every argument marked with @ArgNamed
and @ArgPositional
will be discovered within the inheritance tree for a command,
and they will all be populated as expected:
abstract class CommandBase
{
@ArgNamed("verbose|v", "Show verbose information.")
Nullable!bool verbose;
// This isn't recognised my JCLI, it's just a function all our
// child classes should call as an arbitrary design choice.
final void onPreExecute()
{
import std.stdio;
if(this.verbose.get(false))
writeln("Running in verbose mode!");
}
// Force our child classes to implement the function JCLI recognises.
abstract void onExecute();
}
@Command("verbose say hello", "Says hello!... but only when you define the verbose flag.")
final class MyCommand : CommandBase
{
override void onExecute()
{
import std.stdio;
super.onPreExecute();
if(super.verbose.get(false))
writeln("Hello!");
}
}
Nothing here is overly new, and it should make sense to you if you've gotten this far down:
# Without flag
$> ./mytool verbose say hello
Program exited with status code 0
# With flag
$> ./mytool verbose say hello --verbose
Running in verbose mode!
Hello!
Program exited with status code 0
To summarize, JCLI supports inheritance within commands, and it should for the most part function as you expect. The rest is down to your own design.
Some applications will find it useful to group their arguments together inside of their help text, for example:
$> ./mytool command -h
temp.exe command arg1 arg2 output [--config|-c] [--log|-l] [--test-flag] [--verbose|-v]
Description:
This is a command that is totally super complicated.
Positional Arguments:
arg1 This is a generic argument that isn't grouped anywhere
arg2 This is a generic argument that isn't grouped anywhere
output Where to place the output.
Named Arguments:
--test-flag Test flag, please ignore.
I/O
Arguments related to I/O.
--config|-c Specifies the config file to use.
Debug
Arguments related to debugging.
--log|-l Specifies a log file to direct output to.
--verbose|-v Enables verbose logging.
This can be achieved by using the @ArgGroup
UDA - this is how to produce the above help text:
@Command("command", "This is a command that is totally super complicated.")
struct ComplexCommand
{
@ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere")
int a;
@ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere")
int b;
@ArgNamed("test-flag", "Test flag, please ignore.")
bool flag;
@ArgGroup("Debug", "Arguments related to debugging.")
{
@ArgNamed("verbose|v", "Enables verbose logging.")
Nullable!bool verbose;
@ArgNamed("log|l", "Specifies a log file to direct output to.")
Nullable!string log;
}
@ArgGroup("I/O", "Arguments related to I/O.")
{
// Notice that positional args DON'T get moved. This is to avoid unneeded confusion
// since positional args are always required, and thus should be next to eachother in help text.
@ArgPositional("output", "Where to place the output.")
string output;
@ArgNamed("config|c", "Specifies the config file to use.")
Nullable!string config;
}
void onExecute(){}
}
JCLI provides will provide automatic autocomplete for commands (currently only for command arguments, not commands themselves) via the jcli.autocomplete
package.
In certain cases there may be a need for being able to gather and inspect the data of a command and its arguments, ideally in the same way JCLI is able to.
JCLI exposes this via the jcli.introspect
package, which gathers all the JCLI-relevant details about a command and all of its recognised arguments.
This information is available at compile-time, allowing for the usual meta-programming shenanigans that D allows. This is useful for those that want to build their own functionality on top of the several parts JCLI provides.
Our example will simply be an empty command with a few arguments we'd like to get information of:
TODO Update this for v0.20.0 since this is outdated
It's way simpler than in the example, actually, and the info is all compile-time.
import std, jcli;
@Command("name", "description")
struct MyCommand
{
@ArgNamed("v|verbose", "Toggle verbose output.")
Nullable!bool verbose;
@ArgNamed("l", "Verbose level counter.")
@(ArgAction.count)
uint lCount;
@ArgPositional("arg1", "The first argument to do stuff with.")
string arg1;
// No definition of 'onExecute' is required for this use-case.
}
// Via the `getCommandInfoFor` template, we can gather all the JCLI-relevant information we want.
// We do also have to pass in an instantiation of `ArgBinder`, but it's an unfortunate yet minor design limitation.
enum Info = getCommandInfoFor!(MyCommand, ArgBinder!());
void main()
{
writeln("[Command Info]");
writeln("Pattern = ", Info.pattern);
writeln("Description = ", Info.description);
writeln();
void displayArg(ArgInfoT)(ArgInfoT argInfo)
{
writefln("[Argument Info - %s]", ArgInfoT.stringof);
writeln("Identifier = ", argInfo.identifier);
writeln("UDA = ", argInfo.uda);
writeln("Action = ", argInfo.action);
writeln("Group = ", argInfo.group);
writeln("Existence = ", argInfo.existence);
writeln("ParseScheme = ", argInfo.parseScheme);
writeln();
}
foreach(arg; Info.namedArgs) displayArg(arg);
foreach(arg; Info.positionalArgs) displayArg(arg);
if(Info.rawListArg.isNull)
writeln("[No Raw Arg List]\n");
else
displayArg(Info.rawListArg.get);
// If needed, you can still get access to the argument's symbol.
alias Symbol = __traits(getMember, MyCommand, Info.namedArgs[0].identifier);
writeln("Arg0Nullable = ", isInstanceOf!(Nullable, typeof(Symbol)));
}
With the output of:
[Command Info]
Pattern = Pattern("name")
Description = description
[Argument Info - ArgumentInfo!(ArgNamed, MyCommand)]
Identifier = verbose
UDA = ArgNamed(Pattern("v|verbose"), "Toggle verbose output.")
Action = default_
Group = ArgGroup("", "")
Existence = optional
ParseScheme = bool_
[Argument Info - ArgumentInfo!(ArgNamed, MyCommand)]
Identifier = lCount
UDA = ArgNamed(Pattern("l"), "Verbose level counter.")
Action = count
Group = ArgGroup("", "")
Existence = cast(CommandArgExistence)3 # NOTE: 3 = multiple | optional, result of the `count` action
ParseScheme = allowRepeatedName # Result of the `count` action
[Argument Info - ArgumentInfo!(ArgPositional, MyCommand)]
Identifier = arg1
UDA = ArgPositional("arg1", "The first argument to do stuff with.")
Action = default_
Group = ArgGroup("", "")
Existence = default_
ParseScheme = default_
[No Raw Arg List]
Arg0Nullable = true
I'll also note that every ArgumentInfo
also contains an actionFunc
variable which will be one of the functions inside of
jcli.introspect.actions
. This function will perform the binding action (e.g. default_ goes through the ArgBinder
, count increments, etc.).
Some users may find CommandLineInterface
too forceful and heavy in how it works. Some users may prefer that JCLI only handle
argument parsing and value binding, and then these users will handle the execution/logic themselves.
To do this, you can use the CommandParser
struct, which is responsible for only parsing data into a command instance.
Here's an example:
It's an over-complicated example, it's in fact way simpler than this.
import std, jcli;
enum CalculateOperation
{
add,
sub
}
struct CalculateCommand
{
@ArgPositional("The first value.")
int a;
@ArgPositional("The second value.")
int b;
@ArgNamed("o|op", "The operation to perform.")
CalculateOperation op;
}
int main(string[] args)
{
// If you don't specify an `ArgBinder`, then `CommandParser` will use the default one.
CommandParser!(CalculateCommand, ArgBinder!()) parser; // Same as: CommandParser!CalculateCommand
ResultOf!CalculateCommand result = parser.parse(args[1..$]); // args[0] is the program name, so we need to skip it.
// Normally CommandLineInterface handles everything for us, but now we have to do this ourselves.
if(!result.isOk)
{
writeln("calculate: ", result.error);
return -1;
}
auto instance = result.value;
// We also have to call/handle command logic ourself.
final switch(instance.op) with(CalculateOperation)
{
case add: writeln(instance.a + instance.b); break;
case sub: writeln(instance.a - instance.b); break;
}
return 0;
}
If you're this far down you won't need any example output of the above, so I've not bothered with it.
This usage of JCLI supports all forms of argument parsing and value binding (validators, custom binders, etc.) but does not support:
* Help text generation (see: Light-weight command help text)
* Automatic support for multiple commands (you'll have to build that yourself on top of CommandParser
)
* Bash Completion (planned to become an independent component though)
* Basically anything other than parsing arguments.
In situations where you'd rather use light-weight command parsing instead of CommandLineInterface
, chances are that you'd also like easy access
to JCLI's per-command help text generation.
This can be achieved using the CommandHelpText
struct which can be used to either generate a HelpTextBuilderSimple
, or just a plain string
in the exact same format that you'd normally get by using CommandLineInterface
:
module app;
import std, jcli;
@Command("command", "This is a command that is totally super complicated.")
struct ComplexCommand
{
@ArgPositional("arg1", "This is a generic argument that isn't grouped anywhere")
int a;
@ArgPositional("arg2", "This is a generic argument that isn't grouped anywhere")
int b;
@ArgNamed("test-flag", "Test flag, please ignore.")
bool flag;
@ArgGroup("Debug", "Arguments related to debugging.")
{
@ArgNamed("verbose|v", "Enables verbose logging.")
Nullable!bool verbose;
@ArgNamed("log|l", "Specifies a log file to direct output to.")
Nullable!string log;
}
@ArgGroup("I/O", "Arguments related to I/O.")
{
@ArgPositional("output", "Where to place the output.")
string output;
@ArgNamed("config|c", "Specifies the config file to use.")
Nullable!string config;
}
void onExecute(){}
}
void main(string[] args)
{
CommandHelpText!ComplexCommand helpText;
writeln(helpText.generate());
}
This is almost exactly the same as the argument groups example, except that instead of going through CommandLineInterface
we use CommandHelpText
to directly access the help text for our ComplexCommand
.
The output is exactly the same as shown in the argument groups example, so I won't be duplicating it here.
You can attach any values from the ArgConfig
enum directly onto an argument, to configure certain behaviour about it.
As a reminder, to attach enum values onto something as a UDA, you must use the form @(ArgConfig.xxx)
.
The information below is useful for seeing which combinations of features are supported.
Supported orthogonal higher level flag combinatons (encouraged to use). "implied" written in a cell means the flag combination from the header implies the flag combination from the left:
canRedefine | optional | caseInsensitive | accumulate | aggregate | repeatableName | parseAsFlag | |
---|---|---|---|---|---|---|---|
canRedefine | o | + | + | - | - | - (not yet) | + |
optional | + implied | o | + | + | + | + | + implied |
caseInsensitive | + | + | o | + | + | + | + |
accumulate | - | + | + | o | - | + implied | - |
aggregate | - | + | + | - | o | - | - |
repeatableName | - (not yet) | + | + | + | - | o | - |
parseAsFlag | - | + | + | - | - | - | o |
By default, named arguments are case-sensitive, meaning abc
is not the same as abC
.
By attaching @(ArgConfig.caseInsensitive)
onto a named argument, it will allow things like abc
to match abc
, aBc
, ABC
, etc.
By default, named arguments can only be defined once, meaning --abc 2 --abc 1
produces an error.
By attaching @(ArgConfig.canRedefine)
onto a named argument, the right-most definition will be used (so, --abc 1
in this case).
By default, all arguments mandatory unless marked they're a Nullable
type, or have a default value other than the init
value.
By attaching @(ArgConfig.optional)
onto an argument, it is no longer an error to not specify the argument.
You can force the arguments with default values other than init
to be required using this flag.
Only applicable to numeric named arguments. Everytime the argument is defined, it will increment the argument's value.
So -a
produces 1
, -a -a -a
produces 3
, etc.
This is a special case for array named arguments. Everytime the argument is defined, it will append a value onto the array.
So -a a -a b
would produce ["a", "b"]
.
Allows the argument's name to be repeated multiple times.
So -v
would define v
once, -vvvv
would define -v
multiple times.
Enables flag semantics for the named argument. For now, this can only work on bool
and Nullable!bool
.
If the argument is defined by itself with no value, then it is set to true: --verbose
If the argument is defined with a value, and the value is either true
or false
, then the
argument is set to that given value: --verbose true
If the argument is defined with a value, and the value isn't true
or false
, and there's
a space between the argument name and its value, then the value is treated as a positional argument
instead of a named argument value, and the named argument is set to true
: --verbose foo
If the argument is defined with a value, and the value isn't true
or false
, and there's
an equal sign between the argument name and its value, then this is treated as an error: --verbose=foo
Custom converters are supported for this.
It's possible to use JCLI without dub, especially because it has no external dependencies (other than JANSI, which is actually bundled instead of added as a proper dub dependency, why btw?).
In fact, this library is developed under Meson as the build tool, which means that you can easily integrate this library into your own Meson projects.
I'm perfectly accepting of anyone wanting to contribute to this library, just note that it might take me a while to respond.
And please, if you have an issue, create a Github issue for me. I can't fix or prioritise issues that I don't know exist. I tend to not care about issues when I run across them, but when someone else runs into them, then it becomes a much higher priority for me to address it.
Finally, if you use JCLI in anyway feel free to request for me to add your project into the Examples
section. I'd really love to see how others are using my code :)