A way to build command line interfaces, inspired by immediate mode guis.
conso::args(|ctx| {
ctx.command("greet")
.run(|| {
println!("Hello world!");
});
ctx.command("order")
.run(|| {
println!("I would like a boiled crab, please");
});
});In the above example our program can now run with three possible arguments;
greet: This will printHello world!order: This will printI would like a boiled crab, pleasehelp: This will print help information about the usage of the command.
Notice how the help command is completely auto-generated! We will also get nice error output if mistakes are found in the input.
The names of commands may not be enough to describe what they do. Call description
to add extra help information to a command.
conso::args(|ctx| {
ctx.command("greet")
.description("Give the world a wonderful greeting")
.run(|| {
println!("Hello world!");
});
ctx.command("order")
.description("Order something delicious")
.run(|| {
println!("I would like a boiled crab, please");
});
});Subcommands can be added by calling sub_commands. This provides a new ctx that
can be used to add subcommands in the same way as normal commands.
conso::args(|ctx| {
ctx.command("greet")
.sub_commands(|ctx| {
ctx.command("world")
.run(|| {
println!("Hello world!");
});
ctx.command("you")
.run(|| {
println!("Hello, you!");
});
});
});sub_commands and run can be combined. In this case,
the run will happen if no valid sub commands were found (and as long as there are no more
arguments given, if there were an error will be emitted instead).
conso::args(|ctx| {
ctx.command("greet")
.sub_commands(|ctx| {
ctx.command("crudely")
.run(|| {
println!("Heyo world!");
});
})
.run(|| {
println!("Hello world!");
});
});Another way of acheiving the same thing is with otherwise.
conso::args(|ctx| {
ctx.command("greet")
.sub_commands(|ctx| {
ctx.command("crudely")
.run(|| {
println!("Heyo world!");
});
ctx.otherwise()
.run(|| {
println!("Hello world!");
});
});
});Sometimes we might want a command to be able to recieve data. This can be acheived
by calling the arg function, specifying the type of data we want to recieve.
After doing so, our run method closure will gain a parameter containing the argument that was passed.
conso::args(|ctx| {
ctx.command("echo")
.arg::<String>()
.run(|message| {
println!("{}", message);
});
});If you want several arguments, you can request a tuple of (almost) any size from the arg function.
conso::args(|ctx| {
ctx.command("echo1")
.arg::<(String, String)>()
.run(|(message1, message2)| {
println!("{}, then {}", message1, message2);
});
});You can also call the arg function several times in succession, but it's more confusing so I will leave that out.
For some arguments you may want to make sure they are within a certain bound. For that there is the constrained_arg function!
It takes in arguments describing the constraints, in this case saying that we want two numbers between 0 and 100.
conso::args(|ctx| {
ctx.command("multiply")
.constrained_arg((0..100, 0..100))
.run(|(a, b)| {
println!("{} * {} = {}", a, b, a * b);
});
});One funny, or maybe scary thing about the command function we have been using up until now, is that it actually takes in a constraint
exactly like constrained_arg! If the constraint given is fulfilled, then the command is ran. This means we can
make crazy commands like this too;
conso::args(|ctx| {
ctx.command(0..10)
.run(|| {
println!("The number you entered was between 0 and 10");
});
ctx.command(100..110)
.run(|| {
println!("The number you entered was between 100 and 110");
});
// `otherwise` is actually just a wrapper over `ctx.command(())`, since
// `()` is a constraint that always passes.
ctx.otherwise()
.run(|| {
println!("You didn't enter a number");
});
});We can also get the actual value of the entered numbers by using data_command instead.
conso::args(|ctx| {
ctx.data_command(0..10)
.run(|number| {
println!("The number you entered was {}", number);
});
});If there are a lot of commands and organization starts becoming necessary, we may have to bring out the big guns; good old functions!
fn greetings(ctx: &mut conso::Ctx) {
ctx.command("crudely")
// The help command is of course still automatically generated!
.description("Crudely greet the world")
.run(|| {
println!("Heyo world!");
});
ctx.otherwise()
.run(|| {
println!("Hello world!");
});
}
conso::args(|ctx| {
ctx.command("greet")
.sub_commands(greetings);
// This command also gets the same subcommands, but we also add an extra
// one called `dont`.
ctx.command("maybegreet")
.sub_commands(|ctx| {
ctx.command("dont")
.run(|| {});
greetings(ctx);
});
});Sometimes just command line arguments aren't enough. We might want to allow the user to input
commands in a loop. As it happens user_loop exists just for this purpose!
conso::user_loop(|ctx, control_flow| {
ctx.command("greet")
.run(|| {
println!("Hello world!");
});
ctx.command("quit")
.run(|| {
control_flow.quit(());
});
});As opposed to args, the closure here takes an extra argument called control_flow, that
lets you tell conso when the loop should be finished using quit. This also allows data to be
passed to the caller. Other than that, it works exactly the same.
Some commands are so common that you might want a shorter name for them. Since command names are really
just constraints, we can use the either function to combine two constraints!
conso::user_loop(|ctx, control_flow| {
ctx.command(conso::either("q", "quit"))
.run(|| {
control_flow.quit(());
});
});The way the help auto-generation works is a bit cheeky; and a hint can be found in the signature
of the args function:
pub fn args(handler: impl FnMut(&mut conso::Ctx<'_, '_>)) {
todo!();
}Instead of taking an FnOnce closure like you might expect, it takes an FnMut. This lets
conso call it several times for different purposes. If the help command is called, or crono
wants to try and find suggested usages after an error has occured, conso will call this
function again, but in a special mode where nothing is really parsed and all run calls are
completely skipped.
What does this mean in practice? Nothing much, mostly you get a really simple way to define
commands, while also getting nice help information for free! The main thing to keep in mind is
not to run complex logic without being inside of a run call, since that logic probably should
not run when help is called.
The idea of the control_flow parameter inside of user_loop has a few big advantages;
one is that it allows Ctx to remain generic-less, which is a life-saver when grouping
commands together. It also allows nested user loops affect the control flows of parent user
loops easily. Originally the idea was to put a generic parameter on Ctx describing whether
it was a loop or not, and if it was a loop what return it had, but that was a pain so I'm much
happier with this approach. Originally Command and DataCommand were also going to be the same
type but with generics describing whether or not they had data attached, but that was scrapped
in favor of two types, with Command just being a thin wrapper over DataCommand instead.