DEV Community

Cover image for Create your First Command-line Application in Rust
Ahmad Rosid
Ahmad Rosid

Posted on

Create your First Command-line Application in Rust

Becoming a Programmer to be sure we will working with terminal a lot. Most of the time we will use command line application to do our work in terminal.

But sometime there are some workflow that we want to achieve when working on terminal but there is no application that you like to use.

So for that case you actually can write your own command line application. As for me I write a lot of command line application to automate some repeated work on terminal.

I have been trying to create command line application in various programming language, and I find that Rust is the one that make me happy. It's easy to use and the there is quite a lot library that we can use to do some operation on the terminal.

There are multiple way how we can structure our project when writing command line application, the one that I like the most is following cargo.

Project structure

When you are writing command line application, there are three boring task you need to handle.

  1. Parsing arg input
  2. Validate command flag
  3. Handle the functionality

So to fix this issue let's construct our project structure like this.

└── src
    ├── cli
    │   ├── mod.rs
    │   └── parser.rs
    ├── commands
    │   ├── mod.rs
    │   └── new.rs
    └── main.rs
Enter fullscreen mode Exit fullscreen mode

With this project structure we will use two crates clap and anyhow, this two library will handle argument parser and error handling.

Let's Code

First let's create a parser to our command line application, in this code we will register command and subcommand of our application.

use crate::commands;
use crate::cli::*;

pub fn parse() -> App {
    let usage = "rust-clap-cli [OPTIONS] [SUBCOMMAND]";
    App::new("rust-clap-cli")
        .allow_external_subcommands(true)
        .setting(AppSettings::DeriveDisplayOrder)
        .disable_colored_help(false)
        .override_usage(usage)
        .help_template(get_template())
        .arg(flag("version", "Print version info and exit").short('V'))
        .arg(flag("help", "List command"))
        .subcommands(commands::builtin())
}

pub fn print_help() {
    println!("{}", get_template()
        .replace("{usage}", "rust-clap-cli [OPTIONS] [SUBCOMMAND]")
        .replace("{options}", "\t--help")
    );
}

fn get_template() -> &'static str {
    "\
rust-clap-cli v0.1
USAGE:
    {usage}
OPTIONS:
{options}
Some common rust-clap-cli commands are (see all commands with --list):
    help           Show help
See 'rust-clap-cli help <command>' for more information on a specific command.\n"
}
Enter fullscreen mode Exit fullscreen mode

Next: create cli module to handle some basic error handling and clap app setup.

mod parser;

pub use clap::{value_parser, AppSettings, Arg, ArgAction, ArgMatches};
pub type App = clap::Command<'static>;

pub use parser::parse;
pub use parser::print_help;

#[derive(Debug)]
pub struct Config {
}

pub fn subcommand(name: &'static str) -> App {
    App::new(name)
        .dont_collapse_args_in_usage(true)
        .setting(AppSettings::DeriveDisplayOrder)
}

pub trait AppExt: Sized {
    fn _arg(self, arg: Arg<'static>) -> Self;

    fn arg_new_opts(self) -> Self {
        self
    }
    fn arg_quiet(self) -> Self {
        self._arg(flag("quiet", "Do not print log messages").short('q'))
    }
}

impl AppExt for App {
    fn _arg(self, arg: Arg<'static>) -> Self {
        self.arg(arg)
    }
}

pub fn flag(name: &'static str, help: &'static str) -> Arg<'static> {
    Arg::new(name)
        .long(name)
        .help(help)
        .action(ArgAction::SetTrue)
}

pub fn opt(name: &'static str, help: &'static str) -> Arg<'static> {
    Arg::new(name).long(name).help(help)
}

pub type CliResult = Result<(), CliError>;

#[derive(Debug)]
pub struct CliError {
    pub error: Option<anyhow::Error>,
    pub exit_code: i32,
}

impl CliError {
    pub fn new(error: anyhow::Error, code: i32) -> CliError {
        CliError {
            error: Some(error),
            exit_code: code,
        }
    }
}

impl From<anyhow::Error> for CliError {
    fn from(err: anyhow::Error) -> CliError {
        CliError::new(err, 101)
    }
}

impl From<clap::Error> for CliError {
    fn from(err: clap::Error) -> CliError {
        let code = if err.use_stderr() { 1 } else { 0 };
        CliError::new(err.into(), code)
    }
}

impl From<std::io::Error> for CliError {
    fn from(err: std::io::Error) -> CliError {
        CliError::new(err.into(), 1)
    }
}
Enter fullscreen mode Exit fullscreen mode

Sub-commands

Now let's organize our sub-command of our app, you can think of this like route to our application.

use super::cli::*;
pub mod new;

pub fn builtin() -> Vec<App> {
    vec![
        new::cli()
    ]
}

pub fn builtin_exec(cmd: &str) -> Option<fn(&mut Config, &ArgMatches) -> CliResult> {
    let f = match cmd {
        "new" => new::exec,
        _ => return None,
    };
    Some(f)
}
Enter fullscreen mode Exit fullscreen mode

Thank we can easily just add new subcommand like this.

use crate::cli::*;

pub fn cli() -> App {
    subcommand("new")
        .about("Create a new rust-clap-cli  project at <path>")
        .arg_quiet()
        .arg(Arg::new("path").required(true))
        .arg(opt("registry", "Registry to use").value_name("REGISTRY"))
        .arg_new_opts()
        .after_help("Run `rust-clap-cli help new` for more detailed information.\n")
}

pub fn exec(_: &mut Config, _: &ArgMatches) -> CliResult {
    println!("Hei thanks to create new project");
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Combine All

Now let's combine all of our code and call it from main entry point of our command line application.

mod cli;
mod commands;

use cli::*;

fn main() -> CliResult {
    let mut config = Config{};
    let args = match cli::parse().try_get_matches() {
        Ok(args) => args,
        Err(e) => {
            return Err(e.into());
        }
    };

    if let Some((cmd, args)) = args.subcommand() {
        if let Some(cm) = commands::builtin_exec(cmd) {
            let _ = cm(&mut config, args);
        }
    } else {
        cli::print_help();
    }

    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

Now we can test our application like this.

cargo run -- new work

# Result
...
    Finished dev [unoptimized + debuginfo] target(s) in 0.03s
     Running `target/debug/rust-clap-cli new work`
Hei thanks to create new project
Enter fullscreen mode Exit fullscreen mode

And if you got that message all ready to setup and you can start to create your command line app.

Full source code of this tutorial is available on github here.

Top comments (0)