tauri_plugin_cli/
parser.rs

1// Copyright 2019-2023 Tauri Programme within The Commons Conservancy
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-License-Identifier: MIT
4
5use clap::{
6    builder::{PossibleValue, PossibleValuesParser},
7    error::ErrorKind,
8    Arg as ClapArg, ArgAction, ArgMatches, Command,
9};
10use serde::Serialize;
11use serde_json::Value;
12use tauri::PackageInfo;
13
14use crate::{Arg, Config};
15
16use std::collections::HashMap;
17
18#[macro_use]
19mod macros;
20
21/// The resolution of a argument match.
22#[derive(Default, Debug, Serialize, Clone)]
23#[non_exhaustive]
24pub struct ArgData {
25    /// - [`Value::Bool`] if it's a flag,
26    /// - [`Value::Array`] if it's multiple,
27    /// - [`Value::String`] if it has value,
28    /// - [`Value::Null`] otherwise.
29    pub value: Value,
30    /// The number of occurrences of the argument.
31    /// e.g. `./app --arg 1 --arg 2 --arg 2 3 4` results in three occurrences.
32    pub occurrences: u8,
33}
34
35/// The matched subcommand.
36#[derive(Default, Debug, Serialize, Clone)]
37#[non_exhaustive]
38pub struct SubcommandMatches {
39    /// The subcommand name.
40    pub name: String,
41    /// The subcommand argument matches.
42    pub matches: Matches,
43}
44
45/// The argument matches of a command.
46#[derive(Default, Debug, Serialize, Clone)]
47#[non_exhaustive]
48pub struct Matches {
49    /// Data structure mapping each found arg with its resolution.
50    pub args: HashMap<String, ArgData>,
51    /// The matched subcommand if found.
52    pub subcommand: Option<Box<SubcommandMatches>>,
53}
54
55impl Matches {
56    /// Set a arg match.
57    pub(crate) fn set_arg(&mut self, name: String, value: ArgData) {
58        self.args.insert(name, value);
59    }
60
61    /// Sets the subcommand matches.
62    pub(crate) fn set_subcommand(&mut self, name: String, matches: Matches) {
63        self.subcommand = Some(Box::new(SubcommandMatches { name, matches }));
64    }
65}
66
67/// Gets the argument matches of the CLI definition.
68///
69/// This is a low level API. If the application has been built,
70/// prefer [`App::get_cli_matches`](`crate::App#method.get_cli_matches`).
71///
72/// # Examples
73///
74/// ```rust,no_run
75/// use tauri_plugin_cli::CliExt;
76/// tauri::Builder::default()
77///   .setup(|app| {
78///     let matches = app.cli().matches()?;
79///     Ok(())
80///   });
81/// ```
82pub fn get_matches(
83    cli: &Config,
84    package_info: &PackageInfo,
85    args: Option<Vec<String>>,
86) -> crate::Result<Matches> {
87    let about = cli
88        .description()
89        .unwrap_or(&package_info.description.to_string())
90        .to_string();
91    let version = package_info.version.to_string();
92    let app = get_app(
93        package_info,
94        version,
95        package_info.name.clone(),
96        Some(&about),
97        cli,
98    );
99
100    let matches = if let Some(args) = args {
101        app.try_get_matches_from(args)
102    } else {
103        app.try_get_matches()
104    };
105
106    match matches {
107        Ok(matches) => Ok(get_matches_internal(cli, &matches)),
108        Err(e) => match e.kind() {
109            ErrorKind::DisplayHelp => {
110                let mut matches = Matches::default();
111                let help_text = e.to_string();
112                matches.args.insert(
113                    "help".to_string(),
114                    ArgData {
115                        value: Value::String(help_text),
116                        occurrences: 0,
117                    },
118                );
119                Ok(matches)
120            }
121            ErrorKind::DisplayVersion => {
122                let mut matches = Matches::default();
123                matches
124                    .args
125                    .insert("version".to_string(), Default::default());
126                Ok(matches)
127            }
128            _ => Err(e.into()),
129        },
130    }
131}
132
133fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches {
134    let mut cli_matches = Matches::default();
135    map_matches(config, matches, &mut cli_matches);
136
137    if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
138        if let Some(subcommand_config) = config
139            .subcommands
140            .as_ref()
141            .and_then(|s| s.get(subcommand_name))
142        {
143            cli_matches.set_subcommand(
144                subcommand_name.to_string(),
145                get_matches_internal(subcommand_config, subcommand_matches),
146            );
147        }
148    }
149
150    cli_matches
151}
152
153fn map_matches(config: &Config, matches: &ArgMatches, cli_matches: &mut Matches) {
154    if let Some(args) = config.args() {
155        for arg in args {
156            let (occurrences, value) = if arg.takes_value {
157                if arg.multiple {
158                    matches
159                        .get_many::<String>(&arg.name)
160                        .map(|v| {
161                            let mut values = Vec::new();
162                            for value in v {
163                                values.push(Value::String(value.into()));
164                            }
165                            (values.len() as u8, Value::Array(values))
166                        })
167                        .unwrap_or((0, Value::Null))
168                } else {
169                    matches
170                        .get_one::<String>(&arg.name)
171                        .map(|v| (1, Value::String(v.clone())))
172                        .unwrap_or((0, Value::Null))
173                }
174            } else {
175                let occurrences = matches.get_count(&arg.name);
176                (occurrences, Value::Bool(occurrences > 0))
177            };
178
179            cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
180        }
181    }
182}
183
184fn get_app(
185    package_info: &PackageInfo,
186    version: String,
187    command_name: String,
188    about: Option<&String>,
189    config: &Config,
190) -> Command {
191    let mut app = Command::new(command_name)
192        .author(package_info.authors)
193        .version(version.clone());
194
195    if let Some(about) = about {
196        app = app.about(about);
197    }
198    if let Some(long_description) = config.long_description() {
199        app = app.long_about(long_description);
200    }
201    if let Some(before_help) = config.before_help() {
202        app = app.before_help(before_help);
203    }
204    if let Some(after_help) = config.after_help() {
205        app = app.after_help(after_help);
206    }
207
208    if let Some(args) = config.args() {
209        for arg in args {
210            app = app.arg(get_arg(arg.name.clone(), arg));
211        }
212    }
213
214    if let Some(subcommands) = config.subcommands() {
215        for (subcommand_name, subcommand) in subcommands {
216            let clap_subcommand = get_app(
217                package_info,
218                version.clone(),
219                subcommand_name.to_string(),
220                subcommand.description(),
221                subcommand,
222            );
223            app = app.subcommand(clap_subcommand);
224        }
225    }
226
227    app
228}
229
230fn get_arg(arg_name: String, arg: &Arg) -> ClapArg {
231    let mut clap_arg = ClapArg::new(arg_name.clone());
232
233    if arg.index.is_none() {
234        clap_arg = clap_arg.long(arg_name);
235        if let Some(short) = arg.short {
236            clap_arg = clap_arg.short(short);
237        }
238    }
239
240    clap_arg = bind_string_arg!(arg, clap_arg, description, help);
241    clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
242
243    let action = if arg.multiple {
244        ArgAction::Append
245    } else if arg.takes_value {
246        ArgAction::Set
247    } else {
248        ArgAction::Count
249    };
250
251    clap_arg = clap_arg.action(action);
252
253    clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
254
255    if let Some(values) = &arg.possible_values {
256        clap_arg = clap_arg.value_parser(PossibleValuesParser::new(
257            values
258                .iter()
259                .map(PossibleValue::new)
260                .collect::<Vec<PossibleValue>>(),
261        ));
262    }
263
264    clap_arg = match (arg.min_values, arg.max_values) {
265        (Some(min), Some(max)) => clap_arg.num_args(min..=max),
266        (Some(min), None) => clap_arg.num_args(min..),
267        (None, Some(max)) => clap_arg.num_args(0..max),
268        (None, None) => clap_arg,
269    };
270    clap_arg = clap_arg.required(arg.required);
271    clap_arg = bind_string_arg!(
272        arg,
273        clap_arg,
274        required_unless_present,
275        required_unless_present
276    );
277    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
278    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
279    clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
280    if let Some(value) = &arg.conflicts_with_all {
281        clap_arg = clap_arg.conflicts_with_all(value);
282    }
283    clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
284    if let Some(value) = &arg.requires_all {
285        clap_arg = clap_arg.requires_all(value);
286    }
287    clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
288    clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
289    clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
290    clap_arg = bind_value_arg!(arg, clap_arg, index);
291
292    clap_arg = clap_arg.global(arg.global);
293
294    clap_arg
295}