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)]
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)]
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)]
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(cli: &Config, package_info: &PackageInfo) -> crate::Result<Matches> {
83    let about = cli
84        .description()
85        .unwrap_or(&package_info.description.to_string())
86        .to_string();
87    let version = package_info.version.to_string();
88    let app = get_app(
89        package_info,
90        version,
91        package_info.name.clone(),
92        Some(&about),
93        cli,
94    );
95    match app.try_get_matches() {
96        Ok(matches) => Ok(get_matches_internal(cli, &matches)),
97        Err(e) => match e.kind() {
98            ErrorKind::DisplayHelp => {
99                let mut matches = Matches::default();
100                let help_text = e.to_string();
101                matches.args.insert(
102                    "help".to_string(),
103                    ArgData {
104                        value: Value::String(help_text),
105                        occurrences: 0,
106                    },
107                );
108                Ok(matches)
109            }
110            ErrorKind::DisplayVersion => {
111                let mut matches = Matches::default();
112                matches
113                    .args
114                    .insert("version".to_string(), Default::default());
115                Ok(matches)
116            }
117            _ => Err(e.into()),
118        },
119    }
120}
121
122fn get_matches_internal(config: &Config, matches: &ArgMatches) -> Matches {
123    let mut cli_matches = Matches::default();
124    map_matches(config, matches, &mut cli_matches);
125
126    if let Some((subcommand_name, subcommand_matches)) = matches.subcommand() {
127        if let Some(subcommand_config) = config
128            .subcommands
129            .as_ref()
130            .and_then(|s| s.get(subcommand_name))
131        {
132            cli_matches.set_subcommand(
133                subcommand_name.to_string(),
134                get_matches_internal(subcommand_config, subcommand_matches),
135            );
136        }
137    }
138
139    cli_matches
140}
141
142fn map_matches(config: &Config, matches: &ArgMatches, cli_matches: &mut Matches) {
143    if let Some(args) = config.args() {
144        for arg in args {
145            let (occurrences, value) = if arg.takes_value {
146                if arg.multiple {
147                    matches
148                        .get_many::<String>(&arg.name)
149                        .map(|v| {
150                            let mut values = Vec::new();
151                            for value in v {
152                                values.push(Value::String(value.into()));
153                            }
154                            (values.len() as u8, Value::Array(values))
155                        })
156                        .unwrap_or((0, Value::Null))
157                } else {
158                    matches
159                        .get_one::<String>(&arg.name)
160                        .map(|v| (1, Value::String(v.clone())))
161                        .unwrap_or((0, Value::Null))
162                }
163            } else {
164                let occurrences = matches.get_count(&arg.name);
165                (occurrences, Value::Bool(occurrences > 0))
166            };
167
168            cli_matches.set_arg(arg.name.clone(), ArgData { value, occurrences });
169        }
170    }
171}
172
173fn get_app(
174    package_info: &PackageInfo,
175    version: String,
176    command_name: String,
177    about: Option<&String>,
178    config: &Config,
179) -> Command {
180    let mut app = Command::new(command_name)
181        .author(package_info.authors)
182        .version(version.clone());
183
184    if let Some(about) = about {
185        app = app.about(about);
186    }
187    if let Some(long_description) = config.long_description() {
188        app = app.long_about(long_description);
189    }
190    if let Some(before_help) = config.before_help() {
191        app = app.before_help(before_help);
192    }
193    if let Some(after_help) = config.after_help() {
194        app = app.after_help(after_help);
195    }
196
197    if let Some(args) = config.args() {
198        for arg in args {
199            app = app.arg(get_arg(arg.name.clone(), arg));
200        }
201    }
202
203    if let Some(subcommands) = config.subcommands() {
204        for (subcommand_name, subcommand) in subcommands {
205            let clap_subcommand = get_app(
206                package_info,
207                version.clone(),
208                subcommand_name.to_string(),
209                subcommand.description(),
210                subcommand,
211            );
212            app = app.subcommand(clap_subcommand);
213        }
214    }
215
216    app
217}
218
219fn get_arg(arg_name: String, arg: &Arg) -> ClapArg {
220    let mut clap_arg = ClapArg::new(arg_name.clone());
221
222    if arg.index.is_none() {
223        clap_arg = clap_arg.long(arg_name);
224        if let Some(short) = arg.short {
225            clap_arg = clap_arg.short(short);
226        }
227    }
228
229    clap_arg = bind_string_arg!(arg, clap_arg, description, help);
230    clap_arg = bind_string_arg!(arg, clap_arg, long_description, long_help);
231
232    let action = if arg.multiple {
233        ArgAction::Append
234    } else if arg.takes_value {
235        ArgAction::Set
236    } else {
237        ArgAction::Count
238    };
239
240    clap_arg = clap_arg.action(action);
241
242    clap_arg = bind_value_arg!(arg, clap_arg, number_of_values);
243
244    if let Some(values) = &arg.possible_values {
245        clap_arg = clap_arg.value_parser(PossibleValuesParser::new(
246            values
247                .iter()
248                .map(PossibleValue::new)
249                .collect::<Vec<PossibleValue>>(),
250        ));
251    }
252
253    clap_arg = match (arg.min_values, arg.max_values) {
254        (Some(min), Some(max)) => clap_arg.num_args(min..=max),
255        (Some(min), None) => clap_arg.num_args(min..),
256        (None, Some(max)) => clap_arg.num_args(0..max),
257        (None, None) => clap_arg,
258    };
259    clap_arg = clap_arg.required(arg.required);
260    clap_arg = bind_string_arg!(
261        arg,
262        clap_arg,
263        required_unless_present,
264        required_unless_present
265    );
266    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_all);
267    clap_arg = bind_string_slice_arg!(arg, clap_arg, required_unless_present_any);
268    clap_arg = bind_string_arg!(arg, clap_arg, conflicts_with, conflicts_with);
269    if let Some(value) = &arg.conflicts_with_all {
270        clap_arg = clap_arg.conflicts_with_all(value);
271    }
272    clap_arg = bind_string_arg!(arg, clap_arg, requires, requires);
273    if let Some(value) = &arg.requires_all {
274        clap_arg = clap_arg.requires_all(value);
275    }
276    clap_arg = bind_if_arg!(arg, clap_arg, requires_if);
277    clap_arg = bind_if_arg!(arg, clap_arg, required_if_eq);
278    clap_arg = bind_value_arg!(arg, clap_arg, require_equals);
279    clap_arg = bind_value_arg!(arg, clap_arg, index);
280
281    clap_arg
282}