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.
- Parsing arg input
- Validate command flag
- 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
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"
}
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)
}
}
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)
}
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(())
}
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(())
}
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
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)