rust
cli
RustDay 1

Write a Good CLI Program

Write a Good CLI Program

A command line interface(CLI) program runs on the terminal, which means there is no graphic interface, aka none-GUI.

Actually, we use CLI every day, such as ls, ps, top, etc. There is also a awesome-cli-apps collects many good CLI program. You can take a look. I recommend exa, A modern version of ‘ls’ written in Rust.

A CLI program

A CLI program would look like:

$ ./program_name [arguments] [flags] [options]

Usually we could add -h or --help to see the information.

Take the cargo program for example:

$ cargo -h
Rust's package manager

USAGE:
    cargo [OPTIONS] [SUBCOMMAND]

OPTIONS:
    -V, --version           Print version info and exit
        --list              List installed commands
        --explain <CODE>    Run `rustc --explain CODE`
    -v, --verbose           Use verbose output (-vv very verbose/build.rs output)
    -q, --quiet             No output printed to stdout
        --color <WHEN>      Coloring: auto, always, never
        --frozen            Require Cargo.lock and cache are up to date
        --locked            Require Cargo.lock is up to date
    -Z <FLAG>...            Unstable (nightly-only) flags to Cargo, see 'cargo -Z help' for details
    -h, --help              Prints help information

Some common cargo commands are (see all commands with --list):
    build       Compile the current project
    check       Analyze the current project and report errors, but don't build object files
    clean       Remove the target directory
    doc         Build this project's and its dependencies' documentation
    new         Create a new cargo project
    init        Create a new cargo project in an existing directory
    run         Build and execute src/main.rs
    test        Run the tests
    bench       Run the benchmarks
    update      Update dependencies listed in Cargo.lock
    search      Search registry for crates
    publish     Package and upload this project to the registry
    install     Install a Rust binary
    uninstall   Uninstall a Rust binary

See 'cargo help <command>' for more information on a specific command.

Now you will know how to use cargo.

Create project

Let's start to build a new CLI program!

I name the project meow here.

$ cargo new meow
$ cd meow

cargo will help you create a new project.

Arguments

As we have seen how a CLI would be, there are usually arguments for a CLI.

A simple and naive solution is:

// main.rs
use std::env;

fn main() {
    let args: Vec<String> = env::args().collect();
    println!("{:?}", args);
}
$./meow a1 a2 a3
["meow", "a1", "a2", "a3"]

Then we get the arguments.

However, in the real word, the CLI is more complicated, such as:

$ ./foo -g -e a1 a3 a4
$ ./foo a1 -e -l --path=~/test/123

So the naive way is inconvenient to use.
- arguments might have default value
- flags would exchange positions
- options would exchange positions
- arg1 might binds arg2

So, we need a crate to help us do this job easily.

Clap

Clap is a full featured, fast Command Line Argument Parser for Rust.

How to use it?

First, create a cli.yml file, which is the setting for arguments. It looks like:

<!--  cli.yml -->
name: myapp
version: "1.0"
author: Kevin K. <kbknapp@gmail.com>
about: Does awesome things
args:
    - config:
        short: c
        long: config
        value_name: FILE
        help: Sets a custom config file
        takes_value: true
    - INPUT:
        help: Sets the input file to use
        required: true
        index: 1
    - verbose:
        short: v
        multiple: true
        help: Sets the level of verbosity
subcommands:
    - test:
        about: controls testing features
        version: "1.3"
        author: Someone E. <someone_else@other.com>
        args:
            - debug:
                short: d
                help: print debug information

Then we add the following code in main.rs

#[macro_use]
extern crate clap;
use clap::App;

fn main() {
    // The YAML file is found relative to the current file, similar to how modules are found
    let yaml = load_yaml!("cli.yml");
    let m = App::from_yaml(yaml).get_matches();

     match m.value_of("argument1") {
         // ...
     }

     // ...
}

The clap crate will load and parse the yml, and we can use the arguments in the program.

The running result of the program with the cli.yml above with -h is:

$ meow -h
My Super Program 1.0
Kevin K. <kbknapp@gmail.com>
Does awesome things

USAGE:
    MyApp [FLAGS] [OPTIONS] <INPUT> [SUBCOMMAND]

FLAGS:
    -h, --help       Prints help information
    -v               Sets the level of verbosity
    -V, --version    Prints version information

OPTIONS:
    -c, --config <FILE>    Sets a custom config file

ARGS:
    INPUT    The input file to use

SUBCOMMANDS:
    help    Prints this message or the help of the given subcommand(s)
    test    Controls testing features

Very convenient. Isn't it?

Configuration

A CLI will also need configuration. Some parameters should be determined before run time and they are record in the configuration file, which usally is .env, .config, .setting.

For example, a .env file:

PORT = 8000
PATH = "home/foo/bar"
MODE = "happy mode"
ZONE = 8
AREA = "Taipei"

you could write by hands

  • read file .env
  • split \n
  • split = and add data to HashMap

or use a crate.

dotenv_codegen

dotenv_codegen is a simple .env configuration parser with macro.

The crate reads .env. It is esay to use.

fn main() {
  println!("{}", dotenv!("PORT"));
}

Environment Variables

You might also want to call the environment variable in the system, such as JAVA_HOME.

use std::env;

let key = "HOME";
match env::var_os(key) {
    Some(val) => println!("{}: {:?}", key, val),
    None => println!("{} is not defined in the environment.", key)
}

Error handling

Erro handling is also important. We don't want the program always panic! and shut down the program when it encounter an error. Sometimes, the errors don't matter too much, so we can handle them without break the program, such as running againg or adopting another rule when there is an error.

panic

panic!("this is panic");

Simple but powerless.

  • break the program
  • exit without error code
  • better use in script

Result

Result pass the error without crash. If the function breaks, it will return Error with error type. Then we can decide what to do next according to the type, such as "retry" or "give up".

enum MyErr {
    Reason1,
    Reason2,
}
fn foo() -> Result<(), MyErr> {
    match bar {
        Some(_)=>{}
        Nono => Err(MyErr::Reason1)
    }
}
fn hoo() {
    match foo() {
        Ok(_) => reply(),
        Err(e) => println!(e) 
        // `e` not work yet
        // we need `fmt` to tranlate to the message
    }
}

Error Message

You might want to print or use the error message of the error type. You need to impl the fmt for MyErr, so that it will have its own error message.

enum MyErr {
    Reason1(String),
    Reason2(String, u32),
}
impl fmt::Display for MyErrError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match *self {
            MyErr::Reason1(ref s) => 
                write!(f, "`{}` is the error", s),
            MyErr::Reason2(ref s, ref num) => 
                write!(f, "`{}` and `{}` are error", s, num),
        }
    }
}
Err(e) => println!("{}", e)
// `XXX` is the error

Standard error

There are standard output and standard error in the system.

println!() is the standard output, and eprintln!() is the standard error.

For example:

$ cargo run > output.txt

The standard output stream will be redirected to the file output.txt only.

So, if we don't want to write error messages in log files, we can use eprintln!() to show error as the stand error.

Exit Code

none-zero code lets other programs know it failed.

use std::process;

fn main() {
    process::exit(1);
}

Conclusion

A CLI program could do any kind of works, and a good CLI need a well design. It parses arguments and configuration. It reads environment variables. It handles errors well. It output message in standard output and error. It exits with code when it failed.


About Me

Liu, An-Chi (劉安齊). A software engineer, who loves writing code and promoting CS to people. Welcome to follow me at Facebook Page. More information on Personal Site and Github.