Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

vecli banner

What Is Vecli?

Warning

vecli is a relatively new crate and is still in development. Expect breaking changes and unstable behavior.

vecli (pronounced vek-lii) is a zero-dependency CLI (command-line interface) framework that’s genuinely readable. Minimal but powerful, vecli uses a system where CLI commands are the same as in-program functions that take the given command context.

Looking for a minimal CLI framework that compiles almost instantly with zero bloat? Vecli is for you. No? If your app is that important, use clap or something.

That being said, you navigated to the user guide because you wanted something, so let’s start!

Installation

cargo add vecli

vecli is available on crates.io. To add it to your project, run:

cargo add vecli

Or add it manually to your Cargo.toml:

[dependencies]
vecli = "0.3"

That’s all. vecli has zero dependencies, so it compiles instantly.


Next, let’s build your first app.

First App

‘hello, vecli!’

Important

This guide documentation is a work in progress. Pages are missing. Expect only the bare minimum to know about using vecli.

Let’s build our first app using vecli. The full program can be found in this repo’s main.rs file. This is the official, up-to-date source code with full examples that use this library’s full features.

To see it for yourself, clone the repo and run cargo run.

A minimal working example:

use vecli::*;

fn main() {
    App::new("my-app")
        .run();
}

When you run the app, you should see something like:

error: No command provided. Try 'my-app --help'.

And when you run --help (or cargo run -- --help), you should see a usage message like this:

Usage: my-app <command> [options]

No commands available. Add some using .add_command()!

vecli comes with built-in support for generating help messages and usage information.

Congratulations! You’ve built your first app with vecli. But this isn’t customized yet, so let’s configure it.


Next up, personalize your app by Configuring Your App.

Configuring Your App

Configuring your app is awfully simple, there’s a handful of options you can configure for your app.

Here are the available options:

  • .name("My App"): The human readable name for your app
  • .description("the most awesome app ever"): The description for your app
  • .version("0.1.0"): The version of your app
  • .print_help_if_no_args(true): Prints the help screen when no command is specified.
  • .print_help_on_fail(true): Prints help when no command found.

(Added on newer versions, 0.2.0 or above)

  • .flag(Flag): Adds a flag to your app, we’ll get to this later.
  • .strict_flags(true): Enforces that only flags specified in .flag(Flag) are allowed.
  • .main(fallback /* fn(PassedFlags) */): Sets a fallback function to run when no command is specified. Conventional names: entrypoint, entry, fallback.

Example

use vecli::*;

fn main() {
    App::new()
        .name("My App")
        .description("the most awesome app ever")
        .version("0.3.0")
        .print_help_if_no_args(true)
        .print_help_on_fail(true)
        .run();
}

Now that the app is configured how you want it, let’s add some functionality to it, by Adding Commands.

Commands

‘don’t tell me what to do’

Let’s give your app some commands to run. Here’s how you can define them:

#![allow(unused)]
fn main() {
// use vecli::*;

let hello_command = Command::new("hello", function_that_runs_on_hello /* fn(&CommandContext) */);

// Adding this to the app:
// App::new() ...
    .add_command(hello_command)
// ... .run();
}

The above is a simple command that runs when the user types the command hello, as in my-app hello.

To actually make the command run, you need to define the function function_that_runs_on_hello that will be called when the command is executed.

#![allow(unused)]
fn main() {
fn function_that_runs_on_hello(_ctx: &CommandContext) {
    println!("Hello!");
}
}

Put two together, and you have a working app!

cargo run hello

That should print Hello! to the console.

A shorter way to do this is to just directly define Command inside .add_command():

#![allow(unused)]
fn main() {
App::new()
    .add_command(Command::new("hello", function_that_runs_on_hello))
// ... .run();
}

Configuration

Just like with the app itself, you can configure how the command is displayed on the help screen and usage output.

For example, you can set the command’s description and usage text:

#![allow(unused)]
fn main() {
Command::new("hello", function_that_runs_on_hello)
    .description("Prints a friendly greeting")
    .usage("<none>"); // a suffix to `my-app hello`
}

This will display the command’s description and usage text when the user runs my-app help or my-app hello --help.

The full configuration options for commands:

  • .description("Prints hello and exit."): The description of the command, what it does.
  • .usage("[none]"): The usage for the command, will print alongside my-app hello.
  • .strict_flags(true): If toggled, unknown flags will abort the program. We’ll get to flags in a moment.

(Added in 0.3.0)

  • .print_help_if_no_args(true): If toggled, the help screen will be printed if no arguments (subcommands) are provided.

Okay, that was a lot to take in. Take a breather and let’s continue. Say we want a goodbye subcommand that prints a farewell message. That’s what we’ll cover next on Subcommands.

Subcommands

Full subcommand chaining support was added on version 0.3.0.

Say we want a goodbye subcommand for our hello command that prints a farewell message. We can achieve this flawlessly by adding a new subcommand to our hello command.

First, make the handler for it, we’ll need it later:

#![allow(unused)]
fn main() {
// use vecli::*;

fn goodbye(_ctx: &CommandContext) {
    println!("Hello!");
    println!("Goodbye!");
}
}

To add this as a subcommand for our hello command, we can do it like so by using the .subcommand() method:

#![allow(unused)]
fn main() {
// let hello_command = Command::new(...)
// ...

hello_command.subcommand(
    Command::new("goodbye", goodbye)
);
}

Now, when the user runs my-app hello goodbye (or cargo run hello goodbye), the goodbye subcommand will be executed and print a farewell message!

$ my-app hello goodbye
Hello!
Goodbye!

Since subcommands are just regular commands, they can have their own flags, and even subcommands of their own. Configuring them is the same as configuring any other command.

Extra

If you want a command that does nothing and is only a container for subcommands, you can use a separate Command constructor using Command::parent("name"). This is useful for organizing related subcommands together. Running a parent command without a subcommand will print an error by default, so it’s best to pass .print_help_if_no_args(true) to make it print the help message instead.

#![allow(unused)]
fn main() {
let parent_command = Command::parent("parent").print_help_if_no_args(true);
}

Next up, let’s implement the silent flag for our hello command. We can accomplish this by adding Flags

Flags

‘oh, you mean a nation's flag?’

Flags are named options passed by the user at the command line, like --silent or --output file.txt. vecli handles parsing, alias resolution, and delivery to your handler automatically.

Defining a Flag

Flags are built with Flag::new() and attached to a command with .flag():

#![allow(unused)]
fn main() {
Command::new("hello", function_that_handles_hello)
    .flag(Flag::new("silent").alias("s").description("Suppress output."))
}

The name is the canonical long form, without the -- prefix. An alias is the short form, without -.

Reading Flags in a Handler

Flags are available in ctx.flags, a HashMap<String, String>. Boolean flags have the value "true".

#![allow(unused)]
fn main() {
fn greet(ctx: &CommandContext) {
    if ctx.flags.contains_key("silent") {
        return;
    }
    println!("Hello!");
}
}

Value-carrying flags (e.g. --output file.txt) store the value as a string:

#![allow(unused)]
fn main() {
let path = ctx.flags.get("output").map(String::as_str).unwrap_or("out.txt");
}

Aliases

An alias lets users pass -s instead of --silent. vecli resolves it to the canonical name before your handler is called, so you always check for "silent", never "s".

#![allow(unused)]
fn main() {
Flag::new("silent").alias("s")
}

Note: Aliases are always boolean. -s value will not capture value as the flag’s value.

Global Flags

A global flag is available to every command without any extra setup. Define it on the app with Flag::global():

#![allow(unused)]
fn main() {
App::new("mytool")
    .flag(Flag::global("verbose").alias("v").description("Enable verbose output."))
}

It shows up in every handler’s ctx.flags automatically. Making a regular flag inside App::flag() means it’s only available to the main entry (.main(entry)), if defined.

Strict Mode

By default, unknown flags produce a warning and execution continues. Enable strict mode on a command to treat unknown flags as a hard error instead:

#![allow(unused)]
fn main() {
Command::new("add", add)
    .strict_flags(true)
}

Strict mode can also be set at the app level via App::strict_flags(true), which applies to app-level flag parsing before any command is dispatched.


While making commands, you may have noticed that the function takes this peculiar argument: &CommandContext. What is that? Know more about the CommandContext as our next stop.

CommandContext

‘no context’

Every command handler receives a &CommandContext as its only argument. It contains everything vecli parsed from the command line for that invocation.

#![allow(unused)]
fn main() {
fn my_command(ctx: &CommandContext) {
    // ctx is yours to use
}
}

Fields

ctx.subcommand

The name of the command as typed by the user, e.g. "add" or "list".

#![allow(unused)]
fn main() {
println!("Running: {}", ctx.subcommand);
}

ctx.positionals

A Vec<String> of all non-flag tokens that followed the subcommand, in order.

mytool add "buy milk" groceries
# ctx.positionals == ["buy milk", "groceries"]
#![allow(unused)]
fn main() {
let task = ctx.positionals.first().map(String::as_str).unwrap_or("unnamed");
}

ctx.flags

A HashMap<String, String> of all flags passed by the user, keyed by canonical name after alias resolution. Boolean flags (no explicit value) have the value "true".

mytool add "buy milk" --priority high -v
# ctx.flags == { "priority": "high", "verbose": "true" }
#![allow(unused)]
fn main() {
if ctx.flags.contains_key("verbose") {
    println!("verbose mode");
}

let priority = ctx.flags.get("priority").map(String::as_str).unwrap_or("medium");
}

Global flags registered on the app are merged in automatically — no extra setup needed in the handler.

Full Example

#![allow(unused)]
fn main() {
fn add(ctx: &CommandContext) {
    let task = ctx.positionals.first().map(String::as_str).unwrap_or("unnamed");
    let priority = ctx.flags.get("priority").map(String::as_str).unwrap_or("medium");

    if ctx.flags.contains_key("verbose") {
        println!(
            "[verbose] subcommand='{}', positionals={:?}, flags={:?}",
            ctx.subcommand, ctx.positionals, ctx.flags
        );
    }

    println!("Added '{}' with priority {}.", task, priority);
}
}

Now our app is looking a bit dry, it only accepts commands with no further elaboration. Interact with the user using the Terminal, a collection of interactive terminal prompt utilities.

Terminal & Prompting

Let’s say we want to ask the user for confirmation whenever they run the goodbye command by a sincere please telling us not to. We can achieve this using the Terminal, a collection of interactive terminal utilities.

Confirmation

confirm prompt asking ‘do you want to go out with me [Y/y]’ responded ‘hell no’

We can ask the user for confirmation with the Confirm builder pattern like so:

#![allow(unused)]
fn main() {
// use vecli::*;
// ...
Confirm::new("Are you sure you want to say goodbye?").ask();
// or alternatively: Terminal::confirm("Are you sure you want to say goodbye?")
}

This will show a prompt confirming the user about the given prompt, and returns the user’s answer as a boolean (yes being true and no otherwise). By default, Confirm has the default (when the user doesn’t give a value) value of false. We can change this by configuring the Confirm builder.

Configuring Confirmation

There’s a measly two configuration options for Confirm, which is:

  • .default(b): false by default, changes the default choice to the given boolean.
  • .show_default(b): true by default, and if false, will not show the default option beside the prompt.

And if you chose to do it using the manual Terminal::confirm() command, here’s the order of parameters:

#![allow(unused)]
fn main() {
prompt: &str, 
default: Option<bool>, 
show_default: Option<bool>,
}

Notice Option is being used. You’ll have to manually wrap the boolean in Some(), this is why the builder (Confirm) is much appreciated.

What It Looks Like

Put the code block above on our previous goodbye command, print “Aborted” and return if it evaluates to true. Running the app with cargo run hello goodbye should result in the following:

$ cargo run hello goodbye
Are you sure you want to say goodbye? [y/N]: y
Hello!
Goodbye!

$ cargo run hello goodbye
Are you sure you want to say goodbye? [y/N]: n # or just enter without giving a value
Aborted

Prompting

prompt showing ‘who are you’ and responded ‘chicekn butt’

Okay, let’s get to inputs. We want hello to also ask for the name to be displayed, but Confirm wouldn’t cut it because it’s only yes or no. Well, it would, but that would mean spending a bunch of confirm prompts for a finite amount of names. That’s just… bad.

We can solve the problem by using Terminal::prompt().

Consider the following:

#![allow(unused)]
fn main() {
// ...
Terminal::prompt("Who do you want to greet?");
}

Notice we don’t use spaces at the end, it’s because prompt automatically suffixes them.

The code block above would return a String of whatever the user responds with.

Choices

prompt asking ‘100 million dollars or bone cancer’ responded ‘can…’

For some reason, let’s add a new command that asks the user for their favorite fruit. Add the command like mentioned before, and we want to give a hard-coded choice of what you think are the best fruit nominees.

Note

Subjective opinion alert. If you can’t handle them, then boohoo.

Now for what I think are the nominees for ‘best fruit’ are mangoes, watermelons, grapes, and oranges. Maybe I forgot some, I don’t know. Submit an issue to the repo or something.

Note

Like I’d change a page just because of the issue ‘[insert fruit here] not included in docs’…

We can easily do the task with the Choice builder (alternatively the Terminal::choice() command):

#![allow(unused)]
fn main() {
// ...
Choice::new("What's the best fruit?", ["mango", "watermelon", "grape", "orange"]).ask();
}

The code above will return the String of whatever the user chose.

Configuring Choices

Let’s say you like a fruit on the list so bad, you want to make it the default. Well worry not, there’s actually some configuration options for that and more.

  • .default(&str): Makes the default option the passed &str. Panics if the passed argument isn’t in one of the options.
  • .show_default(bool): true by default. If true, shows the default value by marking it with an asterisk (*). Else, don’t mark it.
  • .show_choices(bool): true by default. If false, not show any choices at all. Not recommended unless you intend to have a choice-showing interface of your own.

The Code

Let’s use the "mango" option as an example. The full block would be:

#![allow(unused)]
fn main() {
// ...
Choice::new("What's the best fruit?", ["mango", "watermelon", "grape", "orange"])
    .default("mango")
    .show_default(true) // default is true anyway,
    .show_choices(true) // but just for the sake of showing.
}

A few possibilities for this feature could be a quiz of some sort. I don’t know, make something awesome with vecli!


If you’ve been this far from the beginning, congrats! This doc is under production and you’ve reached the end… for now. More guides will be added soon! For now, read the API reference at docs.rs.