origen/framework/
mod.rs

1pub mod app_cmds;
2pub mod aux_cmds;
3pub mod cmd_gen_helpers;
4pub mod core_cmds;
5pub mod extensions;
6pub mod helps;
7pub mod plugins;
8
9use origen_metal::indexmap::IndexMap;
10use std::collections::HashMap;
11
12pub use app_cmds::AppCmds;
13pub use aux_cmds::AuxCmds;
14pub use extensions::{Extension, ExtensionTOML, Extensions};
15pub use helps::{CmdHelps, CmdSrc};
16pub use plugins::Plugins;
17use std::env;
18
19use crate::commands::_prelude::clap_arg_actions::*;
20use clap::Arg as ClapArg;
21use clap::Command as App;
22use clap::Command as ClapCommand;
23use origen::{in_app_invocation, Result};
24
25#[macro_export]
26macro_rules! uses_reserved_prefix {
27    ($q:expr) => {{
28        $q.starts_with(crate::framework::extensions::EXT_BASE_PREFIX)
29    }};
30}
31
32#[macro_export]
33macro_rules! err_processing_cmd_preface {
34    ($func:ident, $cmd_path:expr, $msg:expr, $($arg:expr),* $(,)?) => {{
35        $func!(concat!("When processing command '{}': ", $msg), $cmd_path, $($arg),*)
36    }}
37}
38
39#[macro_export]
40macro_rules! log_err_processing_cmd {
41    ($cmd_path:expr, $msg:expr, $($arg:expr),* $(,)?) => {{
42        crate::err_processing_cmd_preface!(log_error, $cmd_path, $msg, $($arg),*)
43    }};
44}
45
46#[macro_export]
47macro_rules! from_toml_args {
48    ($toml_args: expr, $cmd_path: expr) => {{
49        let mut current_names: Vec<Option<&str>> = vec!();
50        // Tracks a preceding multi-value positional. Clap cannot parse a
51        // following optional positional (the greedy arg consumes everything),
52        // and asserts rather than accepting the definition.
53        let mut variadic_arg: Option<&str> = None;
54        $toml_args.as_ref()
55            .map(|args| args.iter()
56                .filter_map( |a| {
57                    if let Some(i) = current_names.iter().position( |n| *n == Some(&a.name)) {
58                        crate::log_err_processing_cmd!(
59                            $cmd_path,
60                            "Argument '{}' is already present. Subsequent occurrences will be skipped (first occurrence at index {})",
61                            &a.name,
62                            i
63                        );
64                        current_names.push(None);
65                        None
66                    } else if crate::uses_reserved_prefix!(a.name) { // a.name.starts_with(crate::framework::extensions::EXT_BASE_NAME) {
67                        crate::log_err_processing_cmd!(
68                            $cmd_path,
69                            "Argument '{}' uses reserved prefix '{}'. This option will not be available.",
70                            &a.name,
71                            crate::framework::extensions::EXT_BASE_NAME
72                        );
73                        current_names.push(None);
74                        None
75                    } else if variadic_arg.is_some() && a.required != Some(true) {
76                        crate::log_err_processing_cmd!(
77                            $cmd_path,
78                            "Argument '{}' follows multi-value argument '{}' and is not required, so it could never receive a value. This argument will not be available; mark it as required, or declare it before '{}'.",
79                            &a.name,
80                            variadic_arg.unwrap(),
81                            variadic_arg.unwrap()
82                        );
83                        current_names.push(None);
84                        None
85                    } else {
86                        current_names.push(Some(&a.name));
87                        if a.multiple == Some(true) || a.use_delimiter == Some(true) {
88                            variadic_arg = Some(&a.name);
89                        }
90                        Some(crate::framework::Arg::from_toml(a))
91                    }
92                })
93                .collect::<Vec<crate::framework::Arg>>())
94            .map(|mut args| {
95                crate::framework::promote_leading_positionals(&mut args, $cmd_path);
96                args
97            })
98    }}
99}
100
101/// Clap rejects a definition in which an optional positional precedes a
102/// required one, because filling positionals in order makes it impossible to
103/// tell which argument a lone value belongs to. Clap 3 silently accepted the
104/// definition and then always filled the earlier positional first, so it was
105/// effectively required regardless of how it was declared. Promote those
106/// arguments so the declaration matches the behavior users already had, and say
107/// so, rather than aborting on a parser assertion.
108pub(crate) fn promote_leading_positionals(args: &mut Vec<Arg>, cmd_path: &str) {
109    let last_required = args.iter().rposition(|a| a.required == Some(true));
110    if let Some(last_required) = last_required {
111        for arg in args[..last_required].iter_mut() {
112            if arg.required != Some(true) {
113                crate::log_err_processing_cmd!(
114                    cmd_path,
115                    "Argument '{}' is optional but precedes a required argument, which cannot be parsed unambiguously. It will be treated as required; declare it after the required arguments to keep it optional.",
116                    &arg.name
117                );
118                arg.required = Some(true);
119            }
120        }
121    }
122}
123
124#[macro_export]
125macro_rules! from_toml_opts {
126    ($toml_opts: expr, $cmd_path: expr) => {
127        crate::from_toml_opts!($toml_opts, $cmd_path, None::<&str>)
128    };
129    ($toml_opts: expr, $cmd_path: expr, $ext_from: expr) => {{
130        let mut current_names: Vec<Option<&str>> = vec!();
131        $toml_opts.as_ref()
132            .map(|opts| opts.iter()
133                .filter_map( |o| {
134                    if let Some(i) = current_names.iter().position( |n| *n == Some(&o.name)) {
135                        if let Some(ext) = $ext_from {
136                            crate::log_err_processing_cmd!(
137                                $cmd_path,
138                                "Option '{}' extended from {} is already present. Subsequent occurrences will be skipped (first occurrence at index {})",
139                                &o.name,
140                                ext,
141                                i
142                            );
143                        } else {
144                            crate::log_err_processing_cmd!(
145                                $cmd_path,
146                                "Option '{}' is already present. Subsequent occurrences will be skipped (first occurrence at index {})",
147                                &o.name,
148                                i
149                            );
150                        }
151                        current_names.push(None);
152                        None
153                    } else if crate::uses_reserved_prefix!(o.name) {
154                        if let Some(ext) = $ext_from {
155                            crate::log_err_processing_cmd!(
156                                $cmd_path,
157                                "Option '{}' extended from {} uses reserved prefix '{}'. This option will not be available",
158                                &o.name,
159                                ext,
160                                crate::framework::extensions::EXT_BASE_NAME
161                            );
162                        } else {
163                            crate::log_err_processing_cmd!(
164                                $cmd_path,
165                                "Option '{}' uses reserved prefix '{}'. This option will not be available",
166                                &o.name,
167                                crate::framework::extensions::EXT_BASE_NAME
168                            );
169                        }
170                        current_names.push(None);
171                        None
172                    } else {
173                        current_names.push(Some(&o.name));
174                        Some(crate::framework::Opt::from_toml(o, $cmd_path, $ext_from))
175                    }
176                })
177                .collect::<Vec<crate::framework::Opt>>())
178    }}
179}
180
181pub trait Applies {
182    fn in_app_context(&self) -> Option<bool>;
183    fn in_global_context(&self) -> Option<bool>;
184    fn on_env(&self) -> Option<&Vec<String>>;
185    fn on_env_error_msg(&self, e: &String) -> String;
186
187    fn applies(&self) -> Result<bool> {
188        Ok(self.applies_with_env()? && self.applies_in_context()?)
189    }
190
191    fn applies_in_app_context(&self) -> Result<bool> {
192        Ok(self.in_app_context().unwrap_or(true))
193    }
194
195    fn applies_in_global_context(&self) -> Result<bool> {
196        Ok(self.in_global_context().unwrap_or(true))
197    }
198
199    fn applies_in_context(&self) -> Result<bool> {
200        if in_app_invocation() {
201            self.applies_in_app_context()
202        } else {
203            self.applies_in_global_context()
204        }
205    }
206
207    fn applies_with_env(&self) -> Result<bool> {
208        if let Some(envs) = self.on_env() {
209            for e in envs {
210                let mut s = e.splitn(1, '=');
211                let e_name = s.next().ok_or_else(|| self.on_env_error_msg(e))?.trim();
212                let e_val = s.next();
213                match env::var(e_name) {
214                    Ok(val) => {
215                        if let Some(v) = e_val {
216                            if v == val {
217                                return Ok(true);
218                            }
219                        } else {
220                            return Ok(true);
221                        }
222                    }
223                    Err(err) => match err {
224                        env::VarError::NotPresent => {}
225                        _ => {
226                            return Err(err.into());
227                        }
228                    },
229                }
230            }
231            Ok(false)
232        } else {
233            Ok(true)
234        }
235    }
236}
237
238#[derive(Debug, Deserialize)]
239pub(crate) struct CommandsToml {
240    pub command: Option<Vec<CommandTOML>>,
241    pub extension: Option<Vec<ExtensionTOML>>,
242}
243
244#[derive(Debug, Deserialize, Clone)]
245pub struct CommandTOML {
246    pub name: String,
247    pub help: String,
248    pub alias: Option<String>,
249    pub arg: Option<Vec<ArgTOML>>,
250    pub opt: Option<Vec<OptTOML>>,
251    pub subcommand: Option<Vec<CommandTOML>>,
252    pub add_target_opt: Option<bool>,
253    pub add_mode_opt: Option<bool>,
254    pub in_global_context: Option<bool>,
255    pub in_app_context: Option<bool>,
256    pub on_env: Option<Vec<String>>,
257}
258
259#[derive(Debug)]
260pub struct Command {
261    pub name: String,
262    pub help: String,
263    pub alias: Option<String>,
264    pub args: Option<Vec<Arg>>,
265    pub opts: Option<Vec<Opt>>,
266    pub subcommands: Option<Vec<String>>,
267    pub add_mode_opt: Option<bool>,
268    pub add_target_opt: Option<bool>,
269    pub cmd_path: CmdSrc,
270    pub in_global_context: Option<bool>,
271    pub in_app_context: Option<bool>,
272    pub on_env: Option<Vec<String>>,
273}
274
275impl Command {
276    pub fn from_toml_cmd(
277        cmd: &CommandTOML,
278        cmd_path: CmdSrc,
279        parent_cmd: Option<&Self>,
280    ) -> Result<Option<Self>> {
281        let mut slf = Self {
282            name: cmd.name.to_owned(),
283            help: cmd.help.to_owned(),
284            alias: cmd.alias.to_owned(),
285            args: None,
286            opts: None,
287            subcommands: None,
288            add_mode_opt: cmd.add_mode_opt.or_else(|| {
289                if let Some(p) = parent_cmd {
290                    p.add_mode_opt.to_owned()
291                } else {
292                    None
293                }
294            }),
295            add_target_opt: cmd.add_target_opt.or_else(|| {
296                if let Some(p) = parent_cmd {
297                    p.add_target_opt.to_owned()
298                } else {
299                    None
300                }
301            }),
302            cmd_path: cmd_path,
303            in_global_context: cmd.in_global_context,
304            in_app_context: cmd.in_app_context,
305            on_env: cmd.on_env.to_owned(),
306        };
307        if !slf.applies()? {
308            return Ok(None);
309        }
310        let fp = slf.cmd_path.to_string();
311        slf.args = from_toml_args!(cmd.arg, &fp);
312        slf.opts = from_toml_opts!(cmd.opt, &fp);
313        if let Some(args) = slf.args.as_ref() {
314            if let Some(opts) = slf.opts.as_mut() {
315                opts.retain(|o| {
316                    if let Some(idx) = args.iter().position(|a| a.name == o.name) {
317                        crate::log_err_processing_cmd!(
318                            &fp,
319                            "Option '{}' conflicts with Arg of the same name (Arg #{})",
320                            o.name,
321                            idx,
322                        );
323                        false
324                    } else {
325                        true
326                    }
327                });
328            }
329        }
330        slf.subcommands = cmd.subcommand.as_ref().map(|sub_cmds| {
331            sub_cmds
332                .iter()
333                .map(|c| format!("{}.{}", &slf.offset_path(), &c.name.to_string()))
334                .collect::<Vec<String>>()
335        });
336        Ok(Some(slf))
337    }
338
339    pub fn add_mode_opt(&self) -> bool {
340        self.add_mode_opt.unwrap_or(true)
341    }
342
343    pub fn add_target_opt(&self) -> bool {
344        self.add_target_opt.unwrap_or(true)
345    }
346
347    pub fn offset_path(&self) -> &str {
348        self.cmd_path.offset_path()
349    }
350}
351
352impl Applies for Command {
353    fn in_global_context(&self) -> Option<bool> {
354        self.in_global_context
355    }
356
357    fn in_app_context(&self) -> Option<bool> {
358        self.in_app_context
359    }
360
361    fn on_env(&self) -> Option<&Vec<String>> {
362        self.on_env.as_ref()
363    }
364
365    fn on_env_error_msg(&self, e: &String) -> String {
366        format!(
367            "Failed to parse 'on_env' '{}', for command {}",
368            e,
369            self.offset_path()
370        )
371    }
372}
373
374#[derive(Debug)]
375pub struct CmdOptCache {
376    opt_names: Vec<String>,
377    lns: HashMap<String, (bool, usize)>,
378    ilns: HashMap<String, (bool, usize)>,
379    ln_aliases: HashMap<String, (bool, usize)>,
380    sns: HashMap<char, (bool, usize)>,
381    sn_aliases: HashMap<char, (bool, usize)>,
382    ext_opt_names: IndexMap<String, usize>,
383    exts: Vec<String>,
384    cmd_path: String,
385    last_needs_visible_full_name: bool,
386    current: String,
387}
388
389macro_rules! processing_exts {
390    ($slf:expr) => {{
391        $slf.exts.len() > 0
392    }};
393}
394
395macro_rules! conflict_err_msg {
396    ($self:expr, $conflict:expr, $name:expr, $conflict_type:expr, $with_type:expr) => {{
397        if processing_exts!($self) {
398            if $conflict.0 {
399                // Conflict with the command itself
400                log_err_processing_cmd!(
401                    $self.cmd_path,
402                    concat!(
403                        $conflict_type,
404                        " '{}' for extension option '{}', from {}, conflicts with ",
405                        $with_type,
406                        " from command option '{}'"
407                    ),
408                    $name,
409                    $self.current,
410                    $self.exts.last().unwrap(),
411                    $self.opt_names[$conflict.1]
412                );
413            } else {
414                // Conflict with an extension
415                let e = $self.ext_opt_names.get_index($conflict.1).unwrap();
416                log_err_processing_cmd!(
417                    $self.cmd_path,
418                    concat!(
419                        $conflict_type,
420                        " '{}' for extension option '{}', from {}, conflicts with ",
421                        $with_type,
422                        " for extension '{}' provided by {}"
423                    ),
424                    $name,
425                    $self.current,
426                    $self.exts.last().unwrap(),
427                    e.0,
428                    $self.exts[*e.1]
429                );
430            }
431        } else {
432            log_err_processing_cmd!(
433                $self.cmd_path,
434                concat!(
435                    $conflict_type,
436                    " '{}' for command option '{}' conflicts with ",
437                    $with_type,
438                    " from option '{}'"
439                ),
440                $name,
441                $self.current,
442                $self.opt_names[$conflict.1]
443            );
444        }
445    }};
446}
447
448macro_rules! cache {
449    ($slf:expr, $cache:ident, $to_cache:expr) => {{
450        $slf.$cache.insert($to_cache, {
451            if processing_exts!($slf) {
452                (false, $slf.ext_opt_names.len() - 1)
453            } else {
454                (true, $slf.opt_names.len() - 1)
455            }
456        });
457    }};
458}
459
460impl CmdOptCache {
461    pub fn new(cmd_path: String) -> Self {
462        let slf = Self {
463            opt_names: Vec::new(),
464            lns: HashMap::new(),
465            ilns: HashMap::new(),
466            ln_aliases: HashMap::new(),
467            sns: HashMap::new(),
468            sn_aliases: HashMap::new(),
469            ext_opt_names: IndexMap::new(),
470            exts: Vec::new(),
471            cmd_path: cmd_path,
472            last_needs_visible_full_name: true,
473            current: "".to_string(),
474        };
475        slf
476    }
477
478    pub fn unchecked_populated(cmd: &App, cmd_path: String) -> Self {
479        let mut slf = Self::new(cmd_path);
480        for (i, arg) in cmd.get_arguments().enumerate() {
481            let mut push_arg = false;
482            if let Some(ln) = arg.get_long() {
483                slf.lns.insert(ln.to_string(), (true, i));
484                push_arg = true;
485            }
486            if let Some(lns) = arg.get_all_aliases() {
487                slf.ln_aliases.extend(
488                    lns.iter()
489                        .map(|ln| (ln.to_string(), (true, i)))
490                        .collect::<Vec<(String, (bool, usize))>>(),
491                );
492                push_arg = true;
493            }
494            if let Some(sn) = arg.get_short() {
495                slf.sns.insert(sn, (true, i));
496                push_arg = true;
497            }
498            if let Some(sns) = arg.get_all_short_aliases() {
499                slf.sn_aliases.extend(
500                    sns.iter()
501                        .map(|sn| (*sn, (true, i)))
502                        .collect::<Vec<(char, (bool, usize))>>(),
503                );
504                push_arg = true;
505            }
506
507            if push_arg {
508                slf.opt_names.push(arg.get_id().to_string());
509            }
510        }
511        slf
512    }
513
514    pub fn register(&mut self, name: &String, ext: Option<&Extension>) -> bool {
515        // TODO name conflict. Probably a better way to deal with this but just skip for now
516        self.current = name.to_string();
517        if let Some(e) = ext {
518            self.exts.push(e.source.to_string());
519            if !self.ext_opt_names.contains_key(name) {
520                self.ext_opt_names
521                    .insert(name.to_string(), self.exts.len() - 1);
522            }
523        } else {
524            self.opt_names.push(name.to_string());
525        }
526        true
527    }
528
529    pub fn iln_conflicts(&mut self, iln: &String) -> bool {
530        if let Some(conflict) = self.ln_aliases.get(iln) {
531            conflict_err_msg!(self, conflict, iln, "Inferred long name", "long name alias");
532            true
533        } else if let Some(conflict) = self.lns.get(iln) {
534            conflict_err_msg!(self, conflict, iln, "Inferred long name", "long name");
535            true
536        } else if let Some(conflict) = self.ilns.get(iln) {
537            conflict_err_msg!(
538                self,
539                conflict,
540                iln,
541                "Inferred long name",
542                "inferred long name"
543            );
544            true
545        } else {
546            cache!(self, ilns, iln.to_owned());
547            self.last_needs_visible_full_name = false;
548            false
549        }
550    }
551
552    pub fn ln_conflicts(&mut self, ln: &String) -> bool {
553        if let Some(conflict) = self.ln_aliases.get(ln) {
554            conflict_err_msg!(self, conflict, ln, "Long name", "long name alias");
555            true
556        } else if let Some(conflict) = self.lns.get(ln) {
557            conflict_err_msg!(self, conflict, ln, "Long name", "long name");
558            true
559        } else if let Some(conflict) = self.ilns.get(ln) {
560            conflict_err_msg!(self, conflict, ln, "Long name", "inferred long name");
561            true
562        } else {
563            cache!(self, lns, ln.to_owned());
564            self.last_needs_visible_full_name = false;
565            false
566        }
567    }
568
569    pub fn sn_conflicts(&mut self, sn: char) -> bool {
570        if let Some(conflict) = self.sn_aliases.get(&sn) {
571            conflict_err_msg!(self, conflict, sn, "Short name", "short name alias");
572            true
573        } else if let Some(conflict) = self.sns.get(&sn) {
574            conflict_err_msg!(self, conflict, sn, "Short name", "short name");
575            true
576        } else {
577            cache!(self, sns, sn);
578            self.last_needs_visible_full_name = false;
579            false
580        }
581    }
582
583    pub fn non_conflicting_snas(&mut self, snas: &Vec<char>) -> Vec<char> {
584        snas.iter()
585            .filter_map(|sna| {
586                if let Some(conflict) = self.sn_aliases.get(sna) {
587                    conflict_err_msg!(self, conflict, sna, "Short name alias", "short name alias");
588                    None
589                } else if let Some(conflict) = self.sns.get(sna) {
590                    conflict_err_msg!(self, conflict, sna, "Short name alias", "short name");
591                    None
592                } else {
593                    cache!(self, sn_aliases, *sna);
594                    Some(*sna)
595                }
596            })
597            .collect::<Vec<char>>()
598    }
599
600    pub fn non_conflicting_lnas(&mut self, lnas: &Vec<String>) -> Vec<String> {
601        lnas.iter()
602            .filter_map(|lna| {
603                if let Some(conflict) = self.ln_aliases.get(lna) {
604                    conflict_err_msg!(self, conflict, lna, "Long name alias", "long name alias");
605                    None
606                } else if let Some(conflict) = self.lns.get(lna) {
607                    conflict_err_msg!(self, conflict, lna, "Long name alias", "long name");
608                    None
609                } else if let Some(conflict) = self.ilns.get(lna) {
610                    conflict_err_msg!(self, conflict, lna, "Long name alias", "inferred long name");
611                    None
612                } else {
613                    cache!(self, ln_aliases, lna.to_owned());
614                    Some(lna.clone())
615                }
616            })
617            .collect::<Vec<String>>()
618    }
619
620    pub fn needs_visible_full_name(&mut self) -> bool {
621        let retn = self.last_needs_visible_full_name;
622        self.last_needs_visible_full_name = true;
623        retn
624    }
625}
626
627#[derive(Debug, Deserialize, Clone)]
628pub struct ArgTOML {
629    pub name: String,
630    pub help: String,
631    pub multiple: Option<bool>,
632    pub required: Option<bool>,
633    pub value_name: Option<String>,
634    pub use_delimiter: Option<bool>,
635}
636
637#[derive(Debug)]
638pub struct Arg {
639    pub name: String,
640    pub help: String,
641    pub multiple: Option<bool>,
642    pub required: Option<bool>,
643    pub value_name: Option<String>,
644    pub use_delimiter: Option<bool>,
645    pub upcased_name: Option<String>,
646}
647
648impl Arg {
649    fn from_toml(arg: &ArgTOML) -> Self {
650        Self {
651            name: arg.name.to_owned(),
652            help: arg.help.to_owned(),
653            multiple: arg.multiple,
654            required: arg.required,
655            value_name: arg.value_name.to_owned(),
656            use_delimiter: arg.use_delimiter,
657            upcased_name: {
658                if arg.value_name.is_some() {
659                    None
660                } else {
661                    Some(arg.name.to_uppercase())
662                }
663            },
664        }
665    }
666}
667
668#[derive(Debug, Deserialize, Clone)]
669pub struct OptTOML {
670    pub name: String,
671    pub help: String,
672    pub short: Option<char>,
673    pub long: Option<String>,
674    pub takes_value: Option<bool>,
675    pub multiple: Option<bool>,
676    pub required: Option<bool>,
677    pub value_name: Option<String>,
678    pub use_delimiter: Option<bool>,
679    pub short_aliases: Option<Vec<char>>,
680    pub long_aliases: Option<Vec<String>>,
681    pub hidden: Option<bool>,
682}
683
684#[derive(Debug)]
685pub struct Opt {
686    pub name: String,
687    pub help: String,
688    pub short: Option<char>,
689    pub long: Option<String>,
690    pub takes_value: Option<bool>,
691    pub multiple: Option<bool>,
692    pub required: Option<bool>,
693    pub value_name: Option<String>,
694    pub use_delimiter: Option<bool>,
695    pub short_aliases: Option<Vec<char>>,
696    pub long_aliases: Option<Vec<String>>,
697    pub hidden: Option<bool>,
698    pub upcased_name: Option<String>,
699    pub full_name: Option<String>,
700}
701
702impl Opt {
703    fn from_toml(opt: &OptTOML, cmd_path: &str, ext_from: Option<&str>) -> Self {
704        macro_rules! gen_err {
705            ($msg:tt $(,)? $($arg:expr),*) => {{
706                if let Some(ext) = ext_from {
707                    log_err_processing_cmd!(cmd_path, concat!("Option '{}' extended from {} ", $msg), opt.name, ext, $($arg),*);
708                } else {
709                    log_err_processing_cmd!(cmd_path, concat!("Option '{}' ", $msg), opt.name, $($arg),*);
710                }
711            }}
712        }
713
714        macro_rules! res_opt_ln_msg {
715            ($conflict:expr, $name:expr) => {
716                gen_err!(
717                    "tried to use reserved option {} '{}' and will not be available as '--{}'",
718                    $conflict,
719                    $name,
720                    $name
721                );
722            };
723        }
724        macro_rules! res_opt_sn_msg {
725            ($conflict:expr, $name:expr) => {
726                gen_err!(
727                    "tried to use reserved option {} '{}' and will not be available as '-{}'",
728                    $conflict,
729                    $name,
730                    $name
731                )
732            };
733        }
734        macro_rules! res_prefix_msg {
735            ($conflict:expr, $name:expr) => {
736                gen_err!(
737                    "uses reserved prefix '{}' in {} '{}' and will not be available as '--{}'",
738                    crate::framework::extensions::EXT_BASE_NAME,
739                    $conflict,
740                    $name,
741                    $name
742                )
743            };
744        }
745
746        let ln = opt.long.as_ref().and_then(|ln| {
747            if RESERVED_OPT_NAMES.contains(&ln.as_str()) {
748                res_opt_ln_msg!("long name", ln);
749                None
750            } else if uses_reserved_prefix!(ln) {
751                res_prefix_msg!("long name", ln);
752                None
753            } else {
754                Some(ln.to_owned())
755            }
756        });
757
758        let sn = opt.short.as_ref().and_then(|sn| {
759            if RESERVED_OPT_SHORT_NAMES.contains(sn) {
760                res_opt_sn_msg!("short name", sn);
761                None
762            } else {
763                Some(*sn)
764            }
765        });
766
767        Self {
768            name: opt.name.to_owned(),
769            help: opt.help.to_owned(),
770            takes_value: opt.takes_value,
771            multiple: opt.multiple,
772            required: opt.required,
773            value_name: opt.value_name.to_owned(),
774            use_delimiter: opt.use_delimiter,
775            short_aliases: {
776                let mut snas: HashMap<&char, usize> = HashMap::new();
777                opt.short_aliases.as_ref().map( |sns| sns.iter().enumerate().filter_map( |(i, sna)| {
778                    if RESERVED_OPT_SHORT_NAMES.contains(sna) {
779                        res_opt_sn_msg!("short name alias", sna);
780                        return None;
781                    } else if sn.is_some() && (sn.as_ref().unwrap() == sna) {
782                        gen_err!("specifies short name alias '{}' but it conflicts with the option's short name", sna);
783                        return None;
784                    } else if let Some(idx) = snas.get(sna) {
785                        gen_err!("repeats short name alias '{}' (first occurrence at index {})", sna, idx);
786                        return None;
787                    }
788                    snas.insert(sna, i);
789                    Some(*sna)
790                }).collect())
791            },
792            short: sn,
793            long_aliases: {
794                let mut lnas: HashMap<&String, usize> = HashMap::new();
795                opt.long_aliases.as_ref().map( |lns| lns.iter().enumerate().filter_map( |(i, lna)| {
796                    if RESERVED_OPT_NAMES.contains(&lna.as_str()) {
797                        res_opt_ln_msg!("long name alias", lna);
798                        return None
799                    } else if uses_reserved_prefix!(lna) {
800                        res_prefix_msg!("long name alias", lna);
801                        return None
802                    } else if ln.is_some() && (ln.as_ref().unwrap() == lna) {
803                        gen_err!("specifies long name alias '{}' but it conflicts with the option's long name", lna);
804                        return None
805                    } else if (&opt.name == lna) && ln.is_none() {
806                        gen_err!("specifies long name alias '{}' but it conflicts with the option's inferred long name. If this is intentional, please set this as the option's long name", lna);
807                        return None
808                    } else if let Some(idx) = lnas.get(lna) {
809                        gen_err!("repeats long name alias '{}' (first occurrence at index {})", lna, idx);
810                        return None
811                    }
812                    lnas.insert(lna, i);
813                    Some(lna.to_owned())
814                }).collect())
815            },
816            long: ln,
817            hidden: opt.hidden,
818            upcased_name: {
819                if opt.value_name.is_some() {
820                    None
821                } else {
822                    Some(opt.name.to_uppercase())
823                }
824            },
825            full_name: None,
826        }
827    }
828
829    pub fn id(&self) -> &str {
830        if let Some(fname) = self.full_name.as_ref() {
831            fname.as_str()
832        } else {
833            self.name.as_str()
834        }
835    }
836}
837
838pub(crate) fn build_commands<'a, F, G, H>(
839    cmd_def: &'a Command,
840    exts: &G,
841    cmd_container: &F,
842    apply_helps: &H,
843) -> App
844where
845    F: Fn(&str) -> &'a Command,
846    G: Fn(&str, App, &mut CmdOptCache) -> App,
847    H: Fn(&str, App) -> App,
848{
849    let mut cmd = ClapCommand::new(cmd_def.name.clone());
850
851    cmd = add_app_opts(cmd, cmd_def.add_mode_opt(), cmd_def.add_target_opt());
852
853    // TODO need test case for cmd alias
854    if cmd_def.alias.is_some() {
855        cmd = cmd.visible_alias(cmd_def.alias.as_ref().unwrap().clone());
856    }
857
858    if let Some(args) = cmd_def.args.as_ref() {
859        cmd = apply_args(args, cmd);
860    }
861
862    let mut cache = CmdOptCache::new(cmd_def.cmd_path.to_string());
863    if let Some(opts) = cmd_def.opts.as_ref() {
864        cmd = apply_opts(opts, cmd, &mut cache, None);
865    }
866
867    if let Some(subcommands) = &cmd_def.subcommands {
868        for c in subcommands {
869            let subcmd = build_commands(cmd_container(c), exts, cmd_container, apply_helps);
870            cmd = cmd.subcommand(subcmd);
871        }
872        cmd = cmd.subcommand_negates_reqs(true);
873    }
874    cmd = exts(&cmd_def.offset_path(), cmd, &mut cache);
875    cmd = apply_helps(&cmd_def.offset_path(), cmd);
876
877    cmd
878}
879
880pub(crate) fn apply_args<'a>(args: &'a Vec<Arg>, mut cmd: App) -> App {
881    for arg_def in args {
882        let mut arg = clap::Arg::new(arg_def.name.clone())
883            .action(SetArg)
884            .help(arg_def.help.clone());
885
886        if let Some(vn) = arg_def.value_name.as_ref() {
887            arg = arg.value_name(vn.clone());
888        } else {
889            arg = arg.value_name(arg_def.upcased_name.as_ref().unwrap().clone());
890        }
891
892        if let Some(d) = arg_def.use_delimiter {
893            arg = arg.use_value_delimiter(d);
894            arg = arg.num_args(1..).action(AppendArgs);
895        }
896        if let Some(m) = arg_def.multiple {
897            arg = if m {
898                arg.num_args(1..).action(AppendArgs)
899            } else {
900                arg.num_args(1).action(SetArg)
901            };
902        }
903
904        if let Some(r) = arg_def.required {
905            arg = arg.required(r);
906        }
907        cmd = cmd.arg(arg);
908    }
909    cmd
910}
911
912pub(crate) fn apply_opts<'a>(
913    opts: &'a Vec<Opt>,
914    mut cmd: App,
915    cache: &mut CmdOptCache,
916    from_ext: Option<&Extension>,
917) -> App {
918    for opt_def in opts {
919        cache.register(&opt_def.name, from_ext);
920        let mut opt = clap::Arg::new(opt_def.id().to_string())
921            .action(CountArgs)
922            .help(opt_def.help.clone());
923
924        if let Some(val_name) = opt_def.value_name.as_ref() {
925            opt = opt.value_name(val_name.clone()).action(SetArg);
926        }
927        if let Some(tv) = opt_def.takes_value {
928            if tv {
929                opt = opt.action(SetArg);
930            }
931        }
932        if let Some(ud) = opt_def.use_delimiter {
933            if ud {
934                opt = opt.use_value_delimiter(ud);
935            }
936            opt = opt.num_args(1..);
937            opt = opt.action(AppendArgs);
938        }
939        if let Some(m) = opt_def.multiple {
940            if m {
941                opt = opt.num_args(1..).action(AppendArgs);
942            } else {
943                opt = opt.num_args(1).action(SetArg);
944            }
945        }
946
947        if let Some(ln) = opt_def.long.as_ref() {
948            if cache.ln_conflicts(ln) {
949                // long name clashes - try inferred long name
950                if !cache.iln_conflicts(&opt_def.name) {
951                    opt = opt.long(opt_def.name.clone());
952                }
953            } else {
954                opt = opt.long(ln.clone());
955            }
956        } else {
957            if !opt_def.short.is_some() {
958                if !cache.iln_conflicts(&opt_def.name) {
959                    opt = opt.long(opt_def.name.clone());
960                }
961            }
962        }
963        if let Some(sn) = opt_def.short {
964            if !cache.sn_conflicts(sn) {
965                opt = opt.short(sn);
966            } else {
967                if !opt.get_long().is_some() {
968                    if !cache.iln_conflicts(&opt_def.name) {
969                        opt = opt.long(opt_def.name.clone());
970                    }
971                }
972            }
973        }
974
975        if let Some(r) = opt_def.required {
976            opt = opt.required(r);
977        }
978
979        if let Some(h) = opt_def.hidden {
980            opt = opt.hide(h);
981        }
982
983        if opt.get_action().takes_values() && opt_def.value_name.is_none() {
984            opt = opt.value_name(opt_def.upcased_name.as_ref().unwrap().clone());
985        }
986
987        if let Some(lns) = opt_def.long_aliases.as_ref() {
988            let v;
989            v = cache.non_conflicting_lnas(lns);
990            opt = opt.visible_aliases(v);
991        }
992
993        if let Some(sns) = opt_def.short_aliases.as_ref() {
994            let to_add;
995            to_add = cache.non_conflicting_snas(sns);
996            opt = opt.visible_short_aliases(to_add);
997        }
998
999        if from_ext.is_some() {
1000            let full_name = opt_def.full_name.as_ref().unwrap();
1001            if cache.needs_visible_full_name() {
1002                opt = opt.long(full_name.clone());
1003            } else {
1004                opt = opt.alias(full_name.clone());
1005            }
1006        } else {
1007            if cache.needs_visible_full_name() {
1008                log_err_processing_cmd!(
1009                    cache.cmd_path,
1010                    "Unable to place unique long name, short name, or inferred long name for command option '{}'. Please resolve any previous conflicts regarding this option or add/update this option's name, long name, or short name",
1011                    opt_def.name
1012                );
1013                continue;
1014            }
1015        }
1016
1017        cmd = cmd.arg(opt);
1018    }
1019    cmd
1020}
1021
1022pub fn build_path<'a>(mut matches: &'a clap::ArgMatches) -> Result<String> {
1023    let mut path_pieces = vec![];
1024    while matches.subcommand_name().is_some() {
1025        let n = matches.subcommand_name().unwrap();
1026        matches = matches.subcommand_matches(&n).unwrap();
1027        path_pieces.push(n);
1028    }
1029    Ok(path_pieces.join("."))
1030}
1031
1032pub const HELP_OPT_NAME: &str = "help";
1033pub const HELP_OPT_SHORT_NAME: char = 'h';
1034pub const VERBOSITY_KEYWORDS_OPT_NAME: &str = "verbosity_keywords";
1035pub const VERBOSITY_KEYWORDS_OPT_LONG_NAME: &str = "vk";
1036pub const VERBOSITY_OPT_NAME: &str = "verbose";
1037pub const VERBOSITY_OPT_SHORT_NAME: char = 'v';
1038pub const VERBOSITY_OPT_LNA: &str = "verbosity";
1039pub const TARGET_OPT_NAME: &str = "targets";
1040pub const TARGET_OPT_ALIAS: &str = "target";
1041pub const TARGET_OPT_SN: char = 't';
1042pub const NO_TARGET_OPT_NAME: &str = "no_targets";
1043pub const NO_TARGET_OPT_ALIAS: &str = "no_target";
1044pub const MODE_OPT_NAME: &str = "mode";
1045
1046pub const RESERVED_OPT_NAMES: &[&str] = &[
1047    HELP_OPT_NAME,
1048    VERBOSITY_KEYWORDS_OPT_NAME,
1049    VERBOSITY_KEYWORDS_OPT_LONG_NAME,
1050    TARGET_OPT_NAME,
1051    TARGET_OPT_ALIAS,
1052    NO_TARGET_OPT_NAME,
1053    NO_TARGET_OPT_ALIAS,
1054    MODE_OPT_NAME,
1055    VERBOSITY_OPT_NAME,
1056    VERBOSITY_OPT_LNA,
1057];
1058
1059pub const RESERVED_OPT_SHORT_NAMES: &[char] =
1060    &[HELP_OPT_SHORT_NAME, TARGET_OPT_SN, VERBOSITY_OPT_SHORT_NAME];
1061
1062static VERBOSITY_HELP_STR: &str = "Terminal verbosity level e.g. -v, -vv, -vvv";
1063static VERBOSITY_KEYWORD_HELP_STR: &str = "Keywords for verbose listeners";
1064
1065pub const VOV_OPT_NAME: &str = "version_or_verbosity";
1066
1067pub fn add_verbosity_opts<'a>(cmd: ClapCommand, split_v: bool) -> ClapCommand {
1068    if split_v {
1069        cmd.arg(
1070            ClapArg::new(VERBOSITY_OPT_NAME)
1071                .long(VERBOSITY_OPT_NAME)
1072                .visible_alias(VERBOSITY_OPT_LNA)
1073                .action(CountArgs),
1074        )
1075        .arg(
1076            ClapArg::new(VOV_OPT_NAME)
1077                .short(VERBOSITY_OPT_SHORT_NAME)
1078                .action(CountArgs),
1079        )
1080    } else {
1081        cmd.arg(
1082            ClapArg::new(VERBOSITY_OPT_NAME)
1083                .long(VERBOSITY_OPT_NAME)
1084                .visible_alias(VERBOSITY_OPT_LNA)
1085                .short(VERBOSITY_OPT_SHORT_NAME)
1086                .action(CountArgs)
1087                .global(true)
1088                .help(VERBOSITY_HELP_STR),
1089        )
1090    }
1091    .arg(
1092        ClapArg::new(VERBOSITY_KEYWORDS_OPT_NAME)
1093            .long(VERBOSITY_KEYWORDS_OPT_NAME)
1094            .visible_alias(VERBOSITY_KEYWORDS_OPT_LONG_NAME)
1095            .action(AppendArgs)
1096            .num_args(1)
1097            .global(true)
1098            .help(VERBOSITY_KEYWORD_HELP_STR)
1099            .use_value_delimiter(true),
1100    )
1101}
1102
1103macro_rules! add_mode_opt {
1104    ($cmd:expr) => {
1105        $cmd.arg(
1106            clap::Arg::new(crate::framework::MODE_OPT_NAME)
1107                .long("mode")
1108                .value_name("MODE")
1109                .help("Override the default mode currently set by the workspace for this command")
1110                .action(crate::framework::SetArg),
1111        )
1112    };
1113}
1114
1115macro_rules! add_target_opt {
1116    ($cmd:expr) => {
1117        $cmd.arg(
1118            clap::Arg::new(crate::framework::TARGET_OPT_NAME)
1119                .short('t')
1120                .long(crate::framework::TARGET_OPT_NAME)
1121                .visible_alias(TARGET_OPT_ALIAS)
1122                .help("Override the targets currently set by the workspace for this command")
1123                .action(crate::commands::_prelude::AppendArgs)
1124                .use_value_delimiter(true)
1125                .num_args(1..)
1126                .value_name("TARGETS")
1127                .conflicts_with(crate::framework::NO_TARGET_OPT_NAME),
1128        )
1129        .arg(
1130            clap::Arg::new(crate::framework::NO_TARGET_OPT_NAME)
1131                .long(crate::framework::NO_TARGET_OPT_NAME)
1132                .visible_alias(NO_TARGET_OPT_ALIAS)
1133                .help("Clear any targets currently set by the workspace for this command")
1134                .action(crate::commands::_prelude::SetArgTrue),
1135        )
1136    };
1137}
1138
1139pub fn add_app_opts(mut cmd: ClapCommand, add_mode_opt: bool, add_target_opt: bool) -> ClapCommand {
1140    if in_app_invocation() {
1141        if add_target_opt {
1142            cmd = add_target_opt!(cmd);
1143        }
1144        if add_mode_opt {
1145            cmd = add_mode_opt!(cmd);
1146        }
1147    }
1148    cmd
1149}
1150
1151pub fn add_all_app_opts(cmd: ClapCommand) -> ClapCommand {
1152    if in_app_invocation() {
1153        add_mode_opt!(add_target_opt!(cmd))
1154    } else {
1155        cmd
1156    }
1157}
1158
1159#[macro_export]
1160macro_rules! output_dir_opt {
1161    () => {{
1162        Arg::new("output_dir")
1163            .short('o')
1164            .long("output-dir")
1165            .visible_alias("output_dir")
1166            .help("Override the default output directory (<APP ROOT>/output)")
1167            .action(SetArg)
1168            .value_name("OUTPUT_DIR")
1169    }};
1170}
1171
1172pub const REF_DIR_OPT_LNAS: &[&str] = &["reference_dir", "ref_dir", "reference-dir"];
1173
1174#[macro_export]
1175macro_rules! ref_dir_opt {
1176    () => {{
1177        Arg::new("reference_dir")
1178            .short('r')
1179            .long("ref-dir")
1180            .visible_aliases(crate::framework::REF_DIR_OPT_LNAS.iter().copied())
1181            .help("Override the default reference directory (<APP ROOT>/.ref)")
1182            .action(SetArg)
1183            .value_name("REFERENCE_DIR")
1184    }};
1185}