5 releases
0.1.2 | Mar 19, 2024 |
---|---|
0.1.1 | Dec 1, 2023 |
0.1.0 | Sep 5, 2023 |
0.0.1-alpha.1 | Aug 27, 2023 |
0.0.0-alpha.2 |
|
#162 in Command-line interface
1,550 downloads per month
Used in netperf
58KB
988 lines
A lightweight framework that simplifies building complex command-line applications with clap.rs.
Overview
cling
's name is a play on CLI-ng (as in next-gen) and "cling" the English word. It enables function handlers to cling to clap user-defined structs 😉.
While writing a command-line applications using the terrific clap crate, developers often write boilerplate functions that finds out which command the user has executed, collects the input types, then runs a handler function that does that job. This quickly gets repetitive for multi-command applications. Cling is designed to simplify that workflow and lets developers declaratively map commands to function.
Key Features
- Map CLI commands to handlers declaratively via
#[cling(run = "my_function")]
- Define handlers as regular Rust unit-testable functions
- Run handlers on any level of the command tree (middlware-style)
- Handler function arguments are extracted automatically from clap input
- Handlers can return a
State<T>
value that can be extracted by downstream handlers - Handlers can be either
sync
orasync
functions - Uniform CLI-friendly error handling with colours
Credit: The handler design of cling is largely inspired by the excellent work done in [Axum](https://github.com/tokio-rs/axum).
For more details, see:
Compiler support: requires rustc
1.74+
Demo
A quick-and-dirty example to show cling in action
# Cargo.toml
[package]
name = "cling-example"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.4.1", features = ["derive", "env"] }
cling = { version = "0.1" }
tokio = { version = "1.13.0", features = ["full"] }
Our main.rs
might look something like this:
use cling::prelude::*;
// -- args --
#[derive(Run, Parser, Debug, Clone)]
#[command(author, version, long_about = None)]
/// A simple multi-command example using cling
struct MyApp {
#[clap(flatten)]
pub opts: CommonOpts,
#[clap(subcommand)]
pub cmd: Command,
}
#[derive(Collect, Parser, Debug, Clone)]
struct CommonOpts {
/// Turn debugging information on
#[arg(short, long, global = true, action = clap::ArgAction::Count)]
pub verbose: u8,
}
// Commands for the app are defined here.
#[derive(Run, Subcommand, Debug, Clone)]
enum Command {
/// Honk the horn!
Honk(HonkOpts),
#[cling(run = "beep")]
/// Beep beep!
Beep,
}
// Options of "honk" command. We define cling(run=...) here to call the
// function when this command is executed.
#[derive(Run, Collect, Parser, Debug, Clone)]
#[cling(run = "honk")]
struct HonkOpts {
/// How many times to honk?
times: u8,
}
// -- Handlers --
// We can access &CommonOpts because it derives [Collect]
fn honk(common_opts: &CommonOpts, HonkOpts { times }: &HonkOpts) {
if common_opts.verbose > 0 {
println!("Honking {} times", times);
}
(0..*times).for_each(|_| {
print!("Honk ");
});
println!("!");
}
// Maybe beeps need to be async!
async fn beep() -> anyhow::Result<()> {
println!("Beep, Beep!");
Ok(())
}
// -- main --
#[tokio::main]
async fn main() -> ClingFinished<MyApp> {
Cling::parse_and_run().await
}
Now, let's run it and verify that it works as expected
$ simple-multi-command -v honk 5
Honking 5 times
Honk Honk Honk Honk Honk !
Concepts
Runnables
Runnables refer to your clap structs that represent a CLI command. In cling,
any struct or enum that encodes the command tree must derive Run
, this
includes the top-level struct of your program (e.g. MyApp
in the above demo).
The Run
trait tells cling that this type should be attached to a handler
function. Essentially, structs that derive Parser
or Subcommand
will need to derive Run
.
For any struct/enum that derive Run
, a #[cling(run = ...)]
attribute can be used to
associate a handler with it. It's possible to design a multi-level clap
program with handlers that run on each level, let's look at a few examples to understand
what's possible.
Single Command, Single Handler
use cling::prelude::*;
#[derive(Run, Collect, Parser, Debug, Clone)]
// standard clap attributes
#[command(author, version, about)]
#[cling(run = "my_only_handler")]
pub struct MyApp {
#[clap(short, long)]
/// User name
pub name: String,
}
fn my_only_handler(app: &MyApp) {
println!("User is {}", app.name);
}
#[tokio::main]
async fn main() -> ClingFinished<MyApp> {
Cling::parse_and_run().await
}
Note: We derive Collect
and Clone
on MyApp
in order to pass it down to
handler my_only_handler
as shared reference app: &MyApp
. Deriving Collect
is not necessary if your handler doesn't need access to &MyApp
.
Our program will now run the code in my_only_handler
$ sample-1 --name=Steve
User is Steve
Sub-commands with handlers
Given a command structure that looks like this:
MyApp [CommonOpts]
- projects
|- create [CreateOpts]
|- list
- whoami
use cling::prelude::*;
#[derive(Run, Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
pub struct MyApp {
#[clap(flatten)]
pub common: CommonOpts,
#[command(subcommand)]
pub cmd: Commands,
}
#[derive(Args, Collect, Debug, Clone)]
pub struct CommonOpts {
/// Access token
#[arg(long, global = true)]
pub access_token: Option<String>,
}
#[derive(Run, Subcommand, Debug, Clone)]
pub enum Commands {
/// Manage projects
#[command(subcommand)]
Projects(ProjectCommands),
/// Self identification
#[cling(run = "handlers::whoami")]
WhoAmI,
}
#[derive(Run, Subcommand, Debug, Clone)]
pub enum ProjectCommands {
/// Create new project
Create(CreateOpts),
/// List all projects
#[cling(run = "handlers::list_projects")]
List,
}
#[derive(Run, Args, Collect, Debug, Clone)]
#[cling(run = "handlers::create_project")]
pub struct CreateOpts {
/// Project name
pub name: String,
}
mod handlers {
pub fn whoami() {}
pub fn list_projects() {}
pub fn create_project() {}
}
CommonOpts
Program-wide options. Any handler should be able to access it if it chooses to.ProjectOpts
Options forprojects [OPTIONS]
CreateOpts
Options for theprojects create
commandListOpts
Options for theprojects list
command
To understand how this works, consider the different set of options that can be passed in projects list
subcommand:
Usage: myapp [COMMON-OPTIONS] projects create <NAME>
Our runnables in this design are: MyApp
, Commands
, ProjectCommands
, and CreateOpts
.
We can attach a handler to any Run
type by using #[cling(run = "...")]
attribute.
This handler will run before any handler of sub-commands (if any). For enums that implement
Subcommand
, we must attach #[cling(run = "...")]
on enum variants that do not
take any arguments (like ProjectCommands::List
). However, for any enum variant that takes
arguments, the argument type itself must derive Run
.
Handlers
Handlers are regular Rust functions that get executed when a clap command is run. Handlers latch
onto any type that derive Run
using the #[cling(run = "function::path")]
attribute.
Every type that has #[cling(run = ...)]
will run its handler function before running the handlers
of the inner runnables.
Example
use cling::prelude::*;
#[derive(Run, Parser, Debug, Clone)]
#[command(author, version, about, long_about = None)]
#[cling(run = "init")]
pub struct MyApp {
#[clap(flatten)]
pub common: CommonOpts,
#[command(subcommand)]
pub cmd: Commands,
}
#[derive(Args, Collect, Debug, Clone)]
pub struct CommonOpts {
/// Access token
#[arg(long, global = true)]
pub access_token: Option<String>,
}
#[derive(Run, Subcommand, Debug, Clone)]
pub enum Commands {
#[cling(run = "run_beep")]
Beep,
/// Self identification
#[cling(run = "run_whoami")]
WhoAmI,
}
fn init() {
println!("init handler!");
}
fn run_whoami() {
println!("I'm groot!");
}
fn run_beep() {
println!("Beep beep!");
}
#[tokio::main]
async fn main() -> ClingFinished<MyApp> {
Cling::parse_and_run().await
}
init
handler will always run before any other handlers in that structure.
$ many-handlers beep
init handler!
Beep beep!
Feature Flags
Feature | Activation | Effect |
---|---|---|
derive |
default | Enables #[derive(Run)] and #[derive(Collect)] |
shlex |
"shlex" feature | Enables parsing from text, useful when building REPLs |
Supported Rust Versions
Cling's minimum supported rust version is 1.74.0
.
License
Dual-licensed under Apache 2.0 or MIT.
Contribution
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you shall be dual licensed as above, without any additional terms or conditions.
Dependencies
~2.1–9.5MB
~91K SLoC