Posted at
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


-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/ 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/
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.


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

A simple and naive solution is:


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 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. <>
about: Does awesome things
- config:
short: c
long: config
value_name: FILE
help: Sets a custom config file
takes_value: true
help: Sets the input file to use
required: true
index: 1
- verbose:
short: v
multiple: true
help: Sets the level of verbosity
- test:
about: controls testing features
version: "1.3"
author: Someone E. <>
- debug:
short: d
help: print debug information

Then we add the following code in


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. <>
Does awesome things


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

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

INPUT The input file to use

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

Very convenient. Isn't it?


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 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!("this is panic");

Simple but powerless.

  • break the program

  • exit without error code

  • better use in script


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 {

fn foo() -> Result<(), MyErr> {
match bar {
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 {

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() {


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.