Skip to main content

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    Arg as ClapArg, ArgAction, ArgMatches, Command,
7    builder::{PossibleValue, PossibleValuesParser},
8    error::ErrorKind,
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        && 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    cli_matches
150}
151
152fn map_matches(config: &Config, matches: &ArgMatches, cli_matches: &mut Matches) {
153    if let Some(args) = config.args() {
154        for arg in args {
155            let (occurrences, value) = if arg.takes_value {
156                if arg.multiple {
157                    matches
158                        .get_many::<String>(&arg.name)
159                        .map(|v| {
160                            let mut values = Vec::new();
161                            for value in v {
162                                values.push(Value::String(value.into()));
163                            }
164                            (values.len() as u8, Value::Array(values))
165                        })
166                        .unwrap_or((0, Value::Null))
167                } else {
168                    matches
169                        .get_one::<String>(&arg.name)
170                        .map(|v| (1, Value::String(v.clone())))
171                        .unwrap_or((0, Value::Null))
172                }
173            } else {
174                let occurrences = matches.get_count(&arg.name);
175                (occurrences, Value::Bool(occurrences > 0))
176            };
177
178            cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
179        }
180    }
181}
182
183fn get_app(
184    package_info: &PackageInfo,
185    version: String,
186    command_name: String,
187    about: Option<&String>,
188    config: &Config,
189) -> Command {
190    let mut app = Command::new(command_name)
191        .author(package_info.authors)
192        .version(version.clone());
193
194    if let Some(about) = about {
195        app = app.about(about);
196    }
197    if let Some(long_description) = config.long_description() {
198        app = app.long_about(long_description);
199    }
200    if let Some(before_help) = config.before_help() {
201        app = app.before_help(before_help);
202    }
203    if let Some(after_help) = config.after_help() {
204        app = app.after_help(after_help);
205    }
206
207    if let Some(args) = config.args() {
208        for arg in args {
209            app = app.arg(get_arg(arg.name.clone(), arg));
210        }
211    }
212
213    if let Some(subcommands) = config.subcommands() {
214        for (subcommand_name, subcommand) in subcommands {
215            let clap_subcommand = get_app(
216                package_info,
217                version.clone(),
218                subcommand_name.to_string(),
219                subcommand.description(),
220                subcommand,
221            );
222            app = app.subcommand(clap_subcommand);
223        }
224    }
225
226    app
227}
228
229fn get_arg(arg_name: String, arg: &Arg) -> ClapArg {
230    let mut clap_arg = ClapArg::new(arg_name.clone());
231
232    if arg.index.is_none() {
233        clap_arg = clap_arg.long(arg_name);
234        if let Some(short) = arg.short {
235            clap_arg = clap_arg.short(short);
236        }
237    }
238
239    clap_arg = bind_string_arg!(arg, clap_arg, description, help);
240    clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
241
242    let action = if arg.multiple {
243        ArgAction::Append
244    } else if arg.takes_value {
245        ArgAction::Set
246    } else {
247        ArgAction::Count
248    };
249
250    clap_arg = clap_arg.action(action);
251
252    clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
253
254    if let Some(values) = &arg.possible_values {
255        clap_arg = clap_arg.value_parser(PossibleValuesParser::new(
256            values
257                .iter()
258                .map(PossibleValue::new)
259                .collect::<Vec<PossibleValue>>(),
260        ));
261    }
262
263    clap_arg = match (arg.min_values, arg.max_values) {
264        (Some(min), Some(max)) => clap_arg.num_args(min..=max),
265        (Some(min), None) => clap_arg.num_args(min..),
266        (None, Some(max)) => clap_arg.num_args(0..max),
267        (None, None) => clap_arg,
268    };
269    clap_arg = clap_arg.required(arg.required);
270    clap_arg = bind_string_arg!(
271        arg,
272        clap_arg,
273        required_unless_present,
274        required_unless_present
275    );
276    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
277    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
278    clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
279    if let Some(value) = &arg.conflicts_with_all {
280        clap_arg = clap_arg.conflicts_with_all(value);
281    }
282    clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
283    if let Some(value) = &arg.requires_all {
284        clap_arg = clap_arg.requires_all(value);
285    }
286    clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
287    clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
288    clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
289    clap_arg = bind_value_arg!(arg, clap_arg, index);
290
291    clap_arg = clap_arg.global(arg.global);
292
293    clap_arg
294}