clapi/
context.rs

1use crate::command::Command;
2use crate::option::CommandOption;
3use crate::suggestion::SuggestionSource;
4use std::fmt::{Debug, Formatter};
5use crate::utils::debug_option;
6use crate::Argument;
7use crate::help::HelpSource;
8
9/// Provides configuration info for parsing a command.
10///
11/// # Example
12/// ```
13/// use clapi::{Command, Argument, CommandOption, Context, Parser};
14/// use clapi::validator::validate_type;
15///
16/// let command = Command::new("MyApp")
17///     .arg(Argument::one_or_more("values"))
18///     .option(CommandOption::new("enable")
19///         .alias("e")
20///         .requires_assign(true)
21///         .arg(Argument::new().validator(validate_type::<bool>())));
22///
23/// let context = Context::builder(command)
24///     .alias_prefix("/")      // An alias prefix
25///     .assign_operator(':')   // An assign operator for options: `--option:value`
26///     .build();
27///
28/// let result = Parser::new(&context)
29///     .parse(vec!["/e:false", "--", "hello", "hola"])
30///     .unwrap();
31///
32/// assert!(result.options().get_arg("enable").unwrap().contains("false"));
33/// assert!(result.arg().unwrap().contains("hello"));
34/// assert!(result.arg().unwrap().contains("hola"));
35/// ```
36#[derive(Clone)]
37pub struct Context {
38    root: Command,
39    suggestions: Option<SuggestionSource>,
40    help: HelpSource,
41    name_prefixes: Vec<String>,
42    alias_prefixes: Vec<String>,
43    assign_operators: Vec<char>,
44    delimiter: char,
45    help_option: Option<CommandOption>,
46    help_command: Option<Command>,
47    version_option: Option<CommandOption>,
48    version_command: Option<Command>,
49}
50
51impl Context {
52    /// Constructs a default `Context` with the given command.
53    #[inline]
54    pub fn new(root: Command) -> Self {
55        ContextBuilder::new(root).build()
56    }
57
58    /// Constructs a `ContextBuilder` with the given command.
59    #[inline]
60    pub fn builder(root: Command) -> ContextBuilder {
61        ContextBuilder::new(root)
62    }
63
64    /// Returns the `Command` used by this context.
65    pub fn root(&self) -> &Command {
66        &self.root
67    }
68
69    /// Returns an iterator over the option name prefixes of this context.
70    pub fn name_prefixes(&self) -> Prefixes<'_> {
71        Prefixes {
72            iter: self.name_prefixes.iter()
73        }
74    }
75
76    /// Returns an iterator over the option alias prefixes of this context.
77    pub fn alias_prefixes(&self) -> Prefixes<'_> {
78        Prefixes {
79            iter: self.alias_prefixes.iter()
80        }
81    }
82
83    /// Returns an iterator over the assign operator `char`s.
84    pub fn assign_operators(&self) -> impl ExactSizeIterator<Item = &char> {
85        self.assign_operators.iter()
86    }
87
88    /// Returns the delimiter used in this context.
89    pub fn delimiter(&self) -> char {
90        self.delimiter
91    }
92
93    /// Returns the `SuggestionProvider` or `None` if not set.
94    pub fn suggestions(&self) -> Option<&SuggestionSource> {
95        self.suggestions.as_ref()
96    }
97
98    /// Returns the `HelpSource` of this context.
99    pub fn help(&self) -> &HelpSource {
100        &self.help
101    }
102
103    /// Gets the help `CommandOption` of this context.
104    pub fn help_option(&self) -> Option<&CommandOption> {
105        self.help_option.as_ref()
106    }
107
108    /// Gets the help `Command` of this context.
109    pub fn help_command(&self) -> Option<&Command> {
110        self.help_command.as_ref()
111    }
112
113    /// Gets the version `CommandOption` of this context.
114    pub fn version_option(&self) -> Option<&CommandOption> {
115        self.version_option.as_ref()
116    }
117
118    /// Gets the version `Command` of this context.
119    pub fn version_command(&self) -> Option<&Command> {
120        self.version_command.as_ref()
121    }
122
123    /// Sets the `SuggestionSource` of this context.
124    pub fn set_suggestions(&mut self, suggestions: SuggestionSource) {
125        self.suggestions = Some(suggestions);
126    }
127
128    /// Sets the `HelpSource` of this context.
129    pub fn set_help(&mut self, help: HelpSource) {
130        self.help = help;
131    }
132
133    /// Sets the help `CommandOption` of this context.
134    pub fn set_help_option(&mut self, option: CommandOption) {
135        assert!(self.help_option.is_none(), "`Context` already contains a help option");
136        self.help_option = Some(option);
137        add_command_builtin_help_option(self);
138    }
139
140    /// Sets the help `Command` of this context.
141    pub fn set_help_command(&mut self, command: Command) {
142        assert!(self.help_command.is_none(), "`Context` already contains a help command");
143        self.help_command = Some(command);
144        add_command_builtin_help_command(self);
145    }
146
147    /// Sets the version `CommandOption` of this context.
148    pub fn set_version_option(&mut self, option: CommandOption) {
149        assert!(self.version_option.is_none(), "`Context` already contains a version option");
150        self.version_option = Some(option);
151        add_command_builtin_version_option(self);
152    }
153
154    /// Sets the version `Command` of this context.
155    pub fn set_version_command(&mut self, command: Command) {
156        assert!(self.version_command.is_none(), "`Context` already contains a version command");
157        self.version_command = Some(command);
158        add_command_builtin_version_command(self);
159    }
160
161    /// Returns the `CommandOption` with the given name or alias or `None` if not found.
162    pub fn get_option(&self, name_or_alias: &str) -> Option<&CommandOption> {
163        if let Some(opt) = self.root().get_options().get(name_or_alias) {
164            return Some(opt);
165        }
166
167        for child in self.root().get_subcommands() {
168            if let Some(opt) = child.get_options().get(name_or_alias) {
169                return Some(opt);
170            }
171        }
172
173        None
174    }
175
176    /// Returns the `Command` with the given name or `None` if not found.
177    pub fn get_command(&self, name: &str) -> Option<&Command> {
178        self.root().get_subcommands().find(|c| c.get_name() == name)
179    }
180
181    /// Returns `true` if the value is a name prefix.
182    pub fn is_name_prefix(&self, value: &str) -> bool {
183        self.name_prefixes.iter().any(|s| s == value)
184    }
185
186    /// Returns `true` if the value is an alias prefix.
187    pub fn is_alias_prefix(&self, value: &str) -> bool {
188        self.alias_prefixes.iter().any(|s| s == value)
189    }
190
191    /// Removes the prefix from the given option
192    pub fn trim_prefix<'a>(&self, option: &'a str) -> &'a str {
193        self.name_prefixes.iter()
194            .chain(self.alias_prefixes.iter())
195            .find(|prefix| option.starts_with(prefix.as_str()))
196            .map(|prefix| option.strip_prefix(prefix))
197            .flatten()
198            .unwrap_or(option)
199    }
200}
201
202impl Debug for Context {
203    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
204        f.debug_struct("Context")
205            .field("root", &self.root)
206            .field("suggestions", &debug_option(&self.suggestions, "SuggestionSource"))
207            .field("help", &"HelpSource")
208            .field("name_prefixes", &self.name_prefixes)
209            .field("alias_prefixes", &self.alias_prefixes)
210            .field("assign_operators", &self.assign_operators)
211            .field("delimiter", &self.delimiter)
212            .field("help_option", &self.help_option)
213            .field("help_command", &self.help_command)
214            .field("version_option", &self.version_option)
215            .field("version_command", &self.version_command)
216            .finish()
217    }
218}
219
220/// An iterator over option prefixes.
221#[derive(Debug, Clone)]
222pub struct Prefixes<'a> {
223    iter: std::slice::Iter<'a, String>
224}
225
226impl<'a> Iterator for Prefixes<'a> {
227    type Item = &'a String;
228
229    fn next(&mut self) -> Option<Self::Item> {
230        self.iter.next()
231    }
232}
233
234impl<'a> ExactSizeIterator for Prefixes<'a>{
235    fn len(&self) -> usize {
236        self.iter.len()
237    }
238}
239
240/// A builder for `Context`.
241#[derive(Clone)]
242pub struct ContextBuilder {
243    root: Command,
244    suggestions: Option<SuggestionSource>,
245    help: Option<HelpSource>,
246    name_prefixes: Vec<String>,
247    alias_prefixes: Vec<String>,
248    assign_operators: Vec<char>,
249    delimiter: Option<char>,
250    help_option: Option<CommandOption>,
251    help_command: Option<Command>,
252    version_option: Option<CommandOption>,
253    version_command: Option<Command>,
254}
255
256impl ContextBuilder {
257    /// Constructs a default `ContextBuilder` for the given command.
258    pub fn new(root: Command) -> Self {
259        ContextBuilder {
260            root,
261            suggestions: None,
262            help: None,
263            name_prefixes: Default::default(),
264            alias_prefixes: Default::default(),
265            assign_operators: Default::default(),
266            delimiter: None,
267            help_option: None,
268            help_command: None,
269            version_option: None,
270            version_command: None,
271        }
272    }
273
274    /// Adds an option name prefix to the context.
275    pub fn name_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
276        let prefix = prefix.into();
277        assert_valid_symbol("prefixes", prefix.as_str());
278        self.name_prefixes.push(prefix);
279        self
280    }
281
282    /// Adds a option alias prefix to the context.
283    pub fn alias_prefix<S: Into<String>>(mut self, prefix: S) -> Self {
284        let prefix = prefix.into();
285        assert_valid_symbol("prefixes", prefix.as_str());
286        self.alias_prefixes.push(prefix);
287        self
288    }
289
290    /// Adds an assign operator for this context.
291    pub fn assign_operator(mut self, value: char) -> Self {
292        // A char is always 4 bytes
293        assert_valid_symbol("assign chars", value.encode_utf8(&mut [0;4]));
294        self.assign_operators.push(value);
295        self
296    }
297
298    /// Sets the delimiter for this context.
299    pub fn delimiter(mut self, value: char) -> Self {
300        // A char is always 4 bytes
301        assert_valid_symbol("delimiters", value.encode_utf8(&mut [0;4]));
302        self.delimiter = Some(value);
303        self
304    }
305
306    /// Sets the `SuggestionSource` for this context.
307    pub fn suggestions(mut self, suggestions: SuggestionSource) -> Self {
308        self.suggestions = Some(suggestions);
309        self
310    }
311
312    /// Sets the `HelpSource` for this context.
313    pub fn help(mut self, help: HelpSource) -> Self {
314        self.help = Some(help);
315        self
316    }
317
318    /// Sets the help `CommandOption` for this context.
319    pub fn help_option(mut self, option: CommandOption) -> Self {
320        assert_is_help_option(&option);
321        self.help_option = Some(option);
322        self
323    }
324
325    /// Sets the help `Command` for this context.
326    pub fn help_command(mut self, command: Command) -> Self {
327        assert_is_help_command(&command);
328        self.help_command = Some(command);
329        self
330    }
331
332    /// Sets the version `CommandOption` for this context.
333    pub fn version_option(mut self, option: CommandOption) -> Self {
334        assert_is_version_option(&option);
335        self.version_option = Some(option);
336        self
337    }
338
339    /// Sets the version `Command` for this context.
340    pub fn version_command(mut self, command: Command) -> Self {
341        assert_is_version_command(&command);
342        self.version_command = Some(command);
343        self
344    }
345
346    /// Constructs a `Context` using this builder data.
347    pub fn build(mut self) -> Context {
348        let mut context = Context {
349            // Root command
350            root: self.root,
351
352            // Suggestion provider
353            suggestions: self.suggestions,
354
355            // Help provider
356            help: self.help.unwrap_or_else(|| HelpSource::default()),
357
358            // Option name prefixes
359            name_prefixes: {
360                if self.name_prefixes.is_empty() {
361                    self.name_prefixes.push("--".to_owned());
362                }
363                self.name_prefixes
364            },
365
366            // Option aliases prefixes
367            alias_prefixes: {
368                if self.alias_prefixes.is_empty() {
369                    self.alias_prefixes.push("-".to_owned());
370                }
371                self.alias_prefixes
372            },
373
374            // Option argument assign
375            assign_operators: {
376                if self.assign_operators.is_empty() {
377                    self.assign_operators.push('=');
378                }
379                self.assign_operators
380            },
381
382            // Argument values delimiter
383            delimiter: self.delimiter.unwrap_or(','),
384
385            // Help option
386            help_option: self.help_option,
387
388            // Help command
389            help_command: self.help_command,
390
391            // Version option
392            version_option: self.version_option,
393
394            // Version command
395            version_command: self.version_command
396        };
397
398        add_command_builtin_help_option(&mut context);
399        add_command_builtin_help_command(&mut context);
400        add_command_builtin_version_option(&mut context);
401        add_command_builtin_version_command(&mut context);
402        context
403    }
404}
405
406#[inline]
407#[doc(hidden)]
408pub fn default_version_option() -> CommandOption {
409    CommandOption::new("version")
410        .alias("v")
411        .description("Shows the version of the command")
412}
413
414#[inline]
415#[doc(hidden)]
416pub fn default_version_command() -> Command {
417    Command::new("version")
418        .description("Shows the version of the command")
419}
420
421#[inline]
422#[doc(hidden)]
423pub fn default_help_option() -> CommandOption {
424    CommandOption::new("help")
425        .alias("h")
426        .description("Shows help information about a command")
427        .hidden(true)
428        .arg(Argument::zero_or_more("command"))
429}
430
431#[inline]
432#[doc(hidden)]
433pub fn default_help_command() -> Command {
434    Command::new("help")
435        .description("Shows help information about a command")
436        .arg(Argument::zero_or_more("command"))
437}
438
439#[inline]
440fn assert_valid_symbol(source: &str, value: &str) {
441    for c in value.chars() {
442        if c.is_whitespace() {
443            panic!("{} cannot contains whitespaces: `{}`", source, value);
444        }
445
446        if c.is_ascii_alphanumeric() {
447            panic!("{} cannot contains numbers or letters: `{}`", source, value);
448        }
449    }
450}
451
452#[inline]
453fn assert_is_help_option(option: &CommandOption) {
454    let arg = option.get_arg().expect("help option must take only 1 argument");
455    assert_eq!(arg.get_values_count().min(), Some(0), "help option argument must take any count of values");
456    assert_eq!(arg.get_values_count().max(), None, "help option argument must take any count of values");
457}
458
459#[inline]
460fn assert_is_help_command(command: &Command) {
461    let arg = command.get_arg().expect("help command must take only 1 argument");
462    assert_eq!(arg.get_values_count().min(), Some(0), "help command argument must take any count of values");
463    assert_eq!(arg.get_values_count().max(), None, "help command argument must take any count of values");
464}
465
466#[inline]
467fn assert_is_version_option(option: &CommandOption) {
468    if option.get_arg().is_some() {
469        panic!("version option must take no arguments");
470    }
471}
472
473#[inline]
474fn assert_is_version_command(command: &Command) {
475    if command.get_arg().is_some() {
476        panic!("version command must take no arguments");
477    }
478}
479
480#[inline]
481fn add_command_builtin_help_option(context: &mut Context) {
482    if context.root.get_subcommands().count() > 0 {
483        if let Some(help_option) = context.help_option.as_ref().cloned() {
484            let command = &mut context.root;
485            add_option_recursive(command, help_option);
486        }
487    }
488}
489
490#[inline]
491fn add_command_builtin_help_command(context: &mut Context) {
492    if let Some(help_command) = context.help_command.as_ref().cloned() {
493        context.root.add_command(help_command);
494    }
495}
496
497#[inline]
498fn add_command_builtin_version_option(context: &mut Context) {
499    if context.root.get_subcommands().count() > 0 {
500        if let Some(version_option) = context.version_option.as_ref().cloned() {
501            let command = &mut context.root;
502            add_option_recursive(command, version_option);
503        }
504    }
505}
506
507#[inline]
508fn add_command_builtin_version_command(context: &mut Context) {
509    if let Some(version_command) = context.version_command.as_ref().cloned() {
510        context.root.add_command(version_command);
511    }
512}
513
514fn add_option_recursive(command: &mut Command, option: CommandOption) {
515    for subcommand in command.get_subcommands_mut() {
516        add_option_recursive(subcommand, option.clone());
517    }
518
519    command.add_option(option);
520}
521
522// Checks if the given string is a help command.
523pub(crate) fn is_help_command(context: &Context, name: &str) -> bool {
524    if let Some(help_command) = context.help_command.as_ref() {
525        help_command.get_name() == name
526    } else {
527        false
528    }
529}
530
531// Checks if the given string is a help option.
532pub(crate) fn is_help_option(context: &Context, name: &str) -> bool {
533    if let Some(help_option) = context.help_option.as_ref() {
534        help_option.get_name() == name || help_option.has_alias(name)
535    } else {
536        false
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543
544    #[test]
545    fn context_test(){
546        let context = Context::new(Command::root());
547        assert!(context.is_name_prefix("--"));
548        assert!(context.is_alias_prefix("-"));
549        assert!(context.assign_operators().any(|c| *c == '='));
550        assert_eq!(context.delimiter(), ',');
551    }
552
553    #[test]
554    fn context_builder_test(){
555        let context = Context::builder(Command::root())
556            .name_prefix("#")
557            .alias_prefix("##")
558            .assign_operator('>')
559            .delimiter('-')
560            .build();
561
562        assert!(context.is_name_prefix("#"));
563        assert!(context.is_alias_prefix("##"));
564        assert!(context.assign_operators().any(|c| *c == '>'));
565        assert_eq!(context.delimiter(), '-');
566    }
567
568    #[test]
569    #[should_panic(expected="prefixes cannot contains numbers or letters: `1`")]
570    fn invalid_name_prefix_test() {
571        Context::builder(Command::root()).name_prefix("1");
572    }
573
574    #[test]
575    #[should_panic(expected="prefixes cannot contains whitespaces: `\t ab`")]
576    fn invalid_alias_prefix_test() {
577        Context::builder(Command::root()).alias_prefix("\t ab");
578    }
579
580    #[test]
581    #[should_panic(expected="assign chars cannot contains whitespaces: `\t`")]
582    fn invalid_assign_chars_test() {
583        Context::builder(Command::root()).assign_operator('\t');
584    }
585
586    #[test]
587    #[should_panic(expected="delimiters cannot contains whitespaces: `\t`")]
588    fn invalid_delimiter_test() {
589        Context::builder(Command::root()).delimiter('\t');
590    }
591}