clap_nested/lib.rs
1//! # Convenient `clap` for CLI apps with multi-level subcommands
2//!
3//! `clap-nested` provides a convenient way for setting up CLI apps
4//! with multi-level subcommands.
5//!
6//! We all know that [`clap`][clap] really shines when it comes to
7//! parsing CLI arguments. It even supports nicely formatted help messages,
8//! subcommands, and shell completion out of the box.
9//!
10//! However, [`clap`][clap] is very much unopinionated in how we should
11//! structure and execute logic. Even when we have tens of subcommands
12//! (and arguments!), we still have to manually match against
13//! all possible options and handle them accordingly. That process quickly
14//! becomes tedious and unorganized.
15//!
16//! So, `clap-nested` add a little sauce of opinion into [`clap`][clap]
17//! to help with that.
18//!
19//! # Use case: Easy subcommands and command execution
20//!
21//! In `clap-nested`, commands are defined together with how to execute them.
22//!
23//! Making it that way instead of going through a separate
24//! matching-and-executing block of code like in [`clap`][clap],
25//! it's very natural to separate commands into different files
26//! in an organized and structured way.
27//!
28//! ```
29//! #[macro_use]
30//! extern crate clap;
31//!
32//! use clap::{Arg, ArgMatches};
33//! use clap_nested::{Command, Commander};
34//!
35//! fn main() {
36//! let foo = Command::new("foo")
37//! .description("Shows foo")
38//! .options(|app| {
39//! app.arg(
40//! Arg::with_name("debug")
41//! .short("d")
42//! .help("Prints debug information verbosely"),
43//! )
44//! })
45//! // Putting argument types here for clarity
46//! .runner(|args: &str, matches: &ArgMatches<'_>| {
47//! let debug = clap::value_t!(matches, "debug", bool).unwrap_or_default();
48//! println!("Running foo, env = {}, debug = {}", args, debug);
49//! Ok(())
50//! });
51//!
52//! let bar = Command::new("bar")
53//! .description("Shows bar")
54//! // Putting argument types here for clarity
55//! .runner(|args: &str, _matches: &ArgMatches<'_>| {
56//! println!("Running bar, env = {}", args);
57//! Ok(())
58//! });
59//!
60//! Commander::new()
61//! .options(|app| {
62//! app.arg(
63//! Arg::with_name("environment")
64//! .short("e")
65//! .long("env")
66//! .global(true)
67//! .takes_value(true)
68//! .value_name("STRING")
69//! .help("Sets an environment value, defaults to \"dev\""),
70//! )
71//! })
72//! // `Commander::args()` derives arguments to pass to subcommands.
73//! // Notice all subcommands (i.e. `foo` and `bar`) will accept `&str` as arguments.
74//! .args(|_args, matches| matches.value_of("environment").unwrap_or("dev"))
75//! // Add all subcommands
76//! .add_cmd(foo)
77//! .add_cmd(bar)
78//! // To handle when no subcommands match
79//! .no_cmd(|_args, _matches| {
80//! println!("No subcommand matched");
81//! Ok(())
82//! })
83//! .run();
84//! }
85//! ```
86//!
87//! # Use case: Straightforward multi-level subcommands
88//!
89//! [`Commander`](struct.Commander.html) acts like a runnable group
90//! of subcommands, calling [`run`](struct.Commander.html#method.run)
91//! on a [`Commander`](struct.Commander.html)
92//! gets the whole execution process started.
93//!
94//! On the other hand, [`Commander`](struct.Commander.html)
95//! could also be converted into a [`MultiCommand`](struct.MultiCommand.html)
96//! to be further included (and executed)
97//! under another [`Commander`](struct.Commander.html).
98//! This makes writing multi-level subcommands way easy.
99//!
100//! ```
101//! use clap_nested::{Commander, MultiCommand};
102//!
103//! let multi_cmd: MultiCommand<(), ()> = Commander::new()
104//! // Add some theoretical subcommands
105//! // .add_cmd(model)
106//! // .add_cmd(controller)
107//! // Specify a name for the newly converted command
108//! .into_cmd("generate")
109//! // Optionally specify a description
110//! .description("Generates resources");
111//! ```
112//!
113//! # Use case: Printing help messages directly on errors
114//!
115//! [`clap`][clap] is also the CLI parsing library which powers [Cargo][cargo].
116//!
117//! Sometimes when you run a [Cargo][cargo] command wrongly,
118//! you may see this:
119//!
120//! ```shell
121//! $ cargo run -x
122//! error: Found argument '-x' which wasn't expected, or isn't valid in this context
123//!
124//! USAGE:
125//! cargo run [OPTIONS] [--] [args]...
126//!
127//! For more information try --help
128//! ```
129//!
130//! While it works and is better for separation of concern
131//! (one command, one job, no suprise effect),
132//! we often wish for more. We want the help message to be printed directly
133//! on errors, so it doesn't take us one more command to show the help message
134//! (and then maybe one more to run the supposedly correct command).
135//!
136//! That's why we take a bit of trade-off to change the default behavior
137//! of [`clap`][clap]. It now works this way:
138//!
139//! ```shell
140//! $ cargo run -x
141//! error: Found argument '-x' which wasn't expected, or isn't valid in this context
142//!
143//! cargo-run
144//! Run a binary or example of the local package
145//!
146//! USAGE:
147//! cargo run [OPTIONS] [--] [args]...
148//!
149//! OPTIONS:
150//! -q, --quiet No output printed to stdout
151//! --bin <NAME>... Name of the bin target to run
152//! --example <NAME>... Name of the example target to run
153//! -p, --package <SPEC> Package with the target to run
154//! -j, --jobs <N> Number of parallel jobs, defaults to # of CPUs
155//! <...omitted for brevity...>
156//!
157//! ARGS:
158//! <args>...
159//!
160//! <...omitted for brevity...>
161//! ```
162//!
163//! [cargo]: https://github.com/rust-lang/cargo
164//! [clap]: https://github.com/clap-rs/clap
165
166use std::collections::HashMap;
167use std::ffi::OsString;
168use std::io::Write;
169use std::result::Result as StdResult;
170
171extern crate clap;
172
173use clap::{
174 App, AppSettings, ArgMatches, Error as ClapError, ErrorKind as ClapErrorKind, SubCommand,
175};
176
177mod macros;
178
179type Result = StdResult<(), ClapError>;
180
181#[doc(hidden)]
182pub trait CommandLike<T: ?Sized> {
183 fn name(&self) -> &str;
184 fn app(&self) -> App;
185 fn run(&self, args: &T, matches: &ArgMatches<'_>, help: &Help) -> Result;
186}
187
188/// Define a single-purpose command to be included
189/// in a [`Commander`](struct.Commander.html)
190pub struct Command<'a, T: ?Sized> {
191 name: &'a str,
192 desc: Option<&'a str>,
193 opts: Option<Box<dyn for<'x, 'y> Fn(App<'x, 'y>) -> App<'x, 'y> + 'a>>,
194 runner: Option<Box<dyn Fn(&T, &ArgMatches<'_>) -> Result + 'a>>,
195}
196
197impl<'a, T: ?Sized> Command<'a, T> {
198 pub fn new(name: impl Into<&'a str>) -> Self {
199 Self {
200 name: name.into(),
201 desc: None,
202 opts: None,
203 runner: None,
204 }
205 }
206
207 pub fn description(mut self, desc: impl Into<&'a str>) -> Self {
208 self.desc = Some(desc.into());
209 self
210 }
211
212 pub fn options(mut self, opts: impl for<'x, 'y> Fn(App<'x, 'y>) -> App<'x, 'y> + 'a) -> Self {
213 self.opts = Some(Box::new(opts));
214 self
215 }
216
217 pub fn runner(mut self, run: impl Fn(&T, &ArgMatches<'_>) -> Result + 'a) -> Self {
218 self.runner = Some(Box::new(run));
219 self
220 }
221}
222
223impl<'a, T: ?Sized> CommandLike<T> for Command<'a, T> {
224 fn name(&self) -> &str {
225 self.name
226 }
227
228 fn app(&self) -> App {
229 let mut app = SubCommand::with_name(self.name);
230
231 if let Some(desc) = self.desc {
232 app = app.about(desc);
233 }
234
235 if let Some(cmd) = &self.opts {
236 app = cmd(app);
237 }
238
239 app
240 }
241
242 fn run(&self, args: &T, matches: &ArgMatches<'_>, _help: &Help) -> Result {
243 if let Some(runner) = &self.runner {
244 runner(args, matches)?;
245 }
246
247 Ok(())
248 }
249}
250
251/// Define a group of subcommands to be run directly,
252/// or converted as a whole into a higher-order command
253pub struct Commander<'a, S: ?Sized, T: ?Sized> {
254 opts: Option<Box<dyn for<'x, 'y> Fn(App<'x, 'y>) -> App<'x, 'y> + 'a>>,
255 args: Box<dyn for<'x> Fn(&'x S, &'x ArgMatches<'_>) -> &'x T + 'a>,
256 cmds: Vec<Box<dyn CommandLike<T> + 'a>>,
257 no_cmd: Option<Box<dyn Fn(&T, &ArgMatches<'_>) -> Result + 'a>>,
258}
259
260impl<'a, S: ?Sized> Commander<'a, S, S> {
261 pub fn new() -> Self {
262 Self {
263 opts: None,
264 args: Box::new(|args, _matches| args),
265 cmds: Vec::new(),
266 no_cmd: None,
267 }
268 }
269}
270
271impl<'a, S: ?Sized, T: ?Sized> Commander<'a, S, T> {
272 pub fn options(mut self, opts: impl for<'x, 'y> Fn(App<'x, 'y>) -> App<'x, 'y> + 'a) -> Self {
273 self.opts = Some(Box::new(opts));
274 self
275 }
276
277 pub fn args<U: ?Sized>(
278 self,
279 args: impl for<'x> Fn(&'x S, &'x ArgMatches<'_>) -> &'x U + 'a,
280 ) -> Commander<'a, S, U> {
281 Commander {
282 opts: self.opts,
283 args: Box::new(args),
284 // All other settings are reset.
285 cmds: Vec::new(),
286 no_cmd: None,
287 }
288 }
289
290 pub fn add_cmd(mut self, cmd: impl CommandLike<T> + 'a) -> Self {
291 self.cmds.push(Box::new(cmd));
292 self
293 }
294
295 pub fn no_cmd(mut self, no_cmd: impl Fn(&T, &ArgMatches<'_>) -> Result + 'a) -> Self {
296 self.no_cmd = Some(Box::new(no_cmd));
297 self
298 }
299
300 fn app(&self) -> App {
301 let mut app = App::new(clap::crate_name!())
302 .version(clap::crate_version!())
303 .about(clap::crate_description!())
304 .author(clap::crate_authors!());
305
306 if let Some(opts) = &self.opts {
307 app = opts(app);
308 }
309
310 self.cmds
311 .iter()
312 .fold(app, |app, cmd| app.subcommand(cmd.app()))
313 }
314
315 fn run_with_data(&self, args: &S, matches: &ArgMatches<'_>, help: &Help) -> Result {
316 let args = (self.args)(args, matches);
317
318 for cmd in &self.cmds {
319 if let Some(matches) = matches.subcommand_matches(cmd.name()) {
320 let help = help.cmds.get(cmd.name()).unwrap();
321 return cmd.run(args, matches, help);
322 }
323 }
324
325 if let Some(no_cmd) = &self.no_cmd {
326 no_cmd(args, matches)
327 } else {
328 let mut buf = Vec::new();
329
330 self.write_help(&help, &[], &mut buf);
331
332 Err(ClapError::with_description(
333 &String::from_utf8(buf).unwrap(),
334 ClapErrorKind::HelpDisplayed,
335 ))
336 }
337 }
338
339 fn write_help(&self, mut help: &Help, path: &[&str], out: &mut impl Write) {
340 for &segment in path {
341 match help.cmds.get(segment) {
342 Some(inner) => help = inner,
343 None => unreachable!("Bad help structure (doesn't match with path)"),
344 }
345 }
346
347 out.write(&help.data).unwrap();
348 }
349
350 pub fn into_cmd(self, name: &'a str) -> MultiCommand<'a, S, T> {
351 MultiCommand {
352 name,
353 desc: None,
354 cmd: self,
355 }
356 }
357}
358
359impl<'a, T: ?Sized> Commander<'a, (), T> {
360 pub fn run(&self) {
361 self.run_result().unwrap_or_else(|error| error.exit())
362 }
363
364 pub fn run_with_args(&self, args: impl IntoIterator<Item = impl Into<OsString> + Clone>) {
365 self.run_with_args_result(args).unwrap_or_else(|error| error.exit())
366 }
367
368 pub fn run_result(&self) -> Result {
369 self.run_with_args_result(std::env::args_os())
370 }
371
372 pub fn run_with_args_result(
373 &self,
374 args: impl IntoIterator<Item = impl Into<OsString> + Clone>,
375 ) -> Result {
376 let mut args = args.into_iter().peekable();
377 let mut app = self.app();
378
379 // Infer binary name
380 if let Some(name) = args.peek() {
381 let name = name.clone().into();
382 let path = std::path::Path::new(&name);
383
384 if let Some(filename) = path.file_name() {
385 if let Some(binary_name) = filename.to_os_string().to_str() {
386 if app.p.meta.bin_name.is_none() {
387 app.p.meta.bin_name = Some(binary_name.to_owned());
388 }
389 }
390 }
391 }
392
393 fn propagate_author<'a>(app: &mut App<'_, 'a>, author: &'a str) {
394 app.p.meta.author = Some(author);
395
396 for subcmd in &mut app.p.subcommands {
397 propagate_author(subcmd, author);
398 }
399 }
400
401 let mut tmp = Vec::new();
402 // This hack is used to propagate all needed information to subcommands.
403 app.p.set(AppSettings::GlobalVersion);
404 app.p.gen_completions_to(clap::Shell::Bash, &mut tmp);
405
406 // Also propagate author to subcommands since `clap` doesn't do it
407 if let Some(author) = app.p.meta.author {
408 propagate_author(&mut app, author);
409 }
410
411 let help = Help::from(&app);
412
413 match app.get_matches_from_safe(args) {
414 Ok(matches) => self.run_with_data(&(), &matches, &help),
415 Err(err) => match err.kind {
416 clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed => Err(err),
417 _ => {
418 let mut msg = err.message;
419 let mut buf = Vec::new();
420 let mut help_captured = false;
421
422 if let Some(index) = msg.find("\nUSAGE") {
423 let usage = msg.split_off(index);
424 let mut lines = usage.lines();
425
426 buf.extend_from_slice(msg.as_bytes());
427 buf.push('\n' as u8);
428
429 lines.next();
430 lines.next();
431
432 if let Some(usage) = lines.next() {
433 let mut usage = usage.to_owned();
434
435 if let Some(index) = usage.find("[") {
436 usage.truncate(index);
437 }
438
439 let mut path: Vec<_> = usage.split_whitespace().collect();
440
441 if path.len() > 0 {
442 path.remove(0);
443 self.write_help(&help, &path, &mut buf);
444 help_captured = true;
445 }
446 }
447 }
448
449 if help_captured {
450 Err(ClapError::with_description(
451 &String::from_utf8(buf).unwrap(),
452 ClapErrorKind::HelpDisplayed,
453 ))
454 } else {
455 unreachable!("The help message from clap is missing a usage section.");
456 }
457 }
458 },
459 }
460 }
461}
462
463/// The result of converting a [`Commander`](struct.Commander.html)
464/// into a higher-order command
465pub struct MultiCommand<'a, S: ?Sized, T: ?Sized> {
466 name: &'a str,
467 desc: Option<&'a str>,
468 cmd: Commander<'a, S, T>,
469}
470
471impl<'a, S: ?Sized, T: ?Sized> MultiCommand<'a, S, T> {
472 pub fn description(mut self, desc: impl Into<&'a str>) -> Self {
473 self.desc = Some(desc.into());
474 self
475 }
476}
477
478impl<'a, S: ?Sized, T: ?Sized> CommandLike<S> for MultiCommand<'a, S, T> {
479 fn name(&self) -> &str {
480 self.name
481 }
482
483 fn app(&self) -> App {
484 let mut app = self.cmd.app().name(self.name);
485
486 if let Some(desc) = self.desc {
487 app = app.about(desc);
488 }
489
490 app
491 }
492
493 fn run(&self, args: &S, matches: &ArgMatches<'_>, help: &Help) -> Result {
494 self.cmd.run_with_data(args, matches, help)
495 }
496}
497
498#[doc(hidden)]
499pub struct Help {
500 data: Vec<u8>,
501 cmds: HashMap<String, Help>,
502}
503
504impl Help {
505 fn from(app: &App) -> Self {
506 let mut data = Vec::new();
507 let mut cmds = HashMap::new();
508
509 app.write_help(&mut data).unwrap();
510
511 for app in &app.p.subcommands {
512 cmds.insert(app.p.meta.name.clone(), Self::from(app));
513 }
514
515 Self { data, cmds }
516 }
517}