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}