clipanion_core/
builder.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2
3use crate::{actions::{Check, Reducer}, errors::BuildError, machine::Machine, node::Node, shared::{Arg, ERROR_NODE_ID, INITIAL_NODE_ID, SUCCESS_NODE_ID}, CommandError};
4
5pub struct CliBuilder {
6    pub commands: Vec<CommandBuilder>,
7}
8
9impl Default for CliBuilder {
10    fn default() -> Self {
11        Self::new()
12    }
13}
14
15impl CliBuilder {
16    pub fn new() -> CliBuilder {
17        CliBuilder {
18            commands: vec![],
19        }
20    }
21
22    pub fn add_command(&mut self) -> &mut CommandBuilder {
23        let cli_index = self.commands.len();
24
25        self.commands.push(CommandBuilder::new(cli_index));
26        self.commands.last_mut().unwrap()
27    }
28
29    pub fn compile(&self) -> Machine {
30        let mut machine = Machine::new_any_of(self.commands.iter().map(|command| command.compile()));
31        machine.simplify_machine();
32
33        machine
34    }
35}
36
37pub struct Arity {
38    leading: Vec<String>,
39    optionals: Vec<String>,
40    rest: Option<String>,
41    trailing: Vec<String>,
42    proxy: bool,
43}
44
45#[derive(Debug)]
46pub struct OptionDefinition {
47    pub name_set: Vec<String>,
48    pub description: String,
49    pub arity: usize,
50    pub hidden: bool,
51    pub required: bool,
52    pub allow_binding: bool,
53}
54
55impl Default for OptionDefinition {
56    fn default() -> Self {
57        OptionDefinition {
58            name_set: vec![],
59            description: String::new(),
60            arity: 0,
61            hidden: false,
62            required: false,
63            allow_binding: true,
64        }
65    }
66}
67
68pub struct CommandBuilder {
69    pub cli_index: usize,
70
71    pub paths: Vec<Vec<String>>,
72
73    pub options: BTreeMap<String, OptionDefinition>,
74    pub preferred_names: HashMap<String, String>,
75    pub required_options: Vec<String>,
76    pub valid_bindings: HashSet<String>,
77
78
79    pub arity: Arity,
80}
81
82pub struct CommandUsageOptions {
83    pub detailed: bool,
84    pub inline_options: bool,
85}
86
87pub struct CommandUsageResult {
88    pub usage: String,
89    pub detailed_option_list: Vec<OptionUsage>,
90}
91
92pub struct OptionUsage {
93    pub preferred_name: String,
94    pub name_set: Vec<String>,
95    pub definition: String,
96    pub description: String,
97    pub required: bool,
98}
99
100impl CommandBuilder {
101    pub fn new(cli_index: usize) -> CommandBuilder {
102        CommandBuilder {
103            cli_index,
104
105            paths: vec![],
106
107            options: BTreeMap::new(),
108            preferred_names: HashMap::new(),
109            required_options: vec![],
110            valid_bindings: HashSet::new(),
111
112            arity: Arity {
113                leading: vec![],
114                optionals: vec![],
115                rest: None,
116                trailing: vec![],
117                proxy: false,
118            },
119        }
120    }
121
122    pub fn usage(&self, opts: CommandUsageOptions) -> CommandUsageResult {
123        let mut segments = self.paths.first().cloned().unwrap_or_default();
124        let mut detailed_option_list = vec![];
125
126        if opts.detailed {
127            for (preferred_name, option) in &self.options {
128                if option.hidden {
129                    continue;
130                }
131
132                let mut args = vec![];
133                for t in 0..option.arity {
134                    args.push(format!(" #{}", t));
135                }
136
137                let definition = format!("{}{}", option.name_set.join(", "), args.join(""));
138
139                if !opts.inline_options && !option.description.is_empty() {
140                    detailed_option_list.push(OptionUsage {
141                        preferred_name: preferred_name.clone(),
142                        name_set: option.name_set.clone(),
143                        definition,
144                        description: option.description.clone(),
145                        required: option.required,
146                    });
147                } else {
148                    segments.push(if option.required {
149                        format!("<{}>", definition)
150                    } else {
151                        format!("[{}]", definition)
152                    });
153                }
154            }
155        }
156
157        for name in &self.arity.leading {
158            segments.push(format!("<{}>", name));
159        }
160
161        for name in &self.arity.optionals {
162            segments.push(format!("[{}]", name));
163        }
164
165        if self.arity.rest.is_some() {
166            segments.push(format!("[{}...]", self.arity.rest.as_ref().unwrap()));
167        }
168
169        for name in &self.arity.trailing {
170            segments.push(format!("<{}>", name));
171        }
172
173        CommandUsageResult {
174            usage: segments.join(" "),
175            detailed_option_list,
176        }
177    }
178
179    pub fn make_default(&mut self) -> &mut Self {
180        self.paths.push(vec![]);
181        self
182    }
183
184    pub fn add_path(&mut self, path: Vec<String>) -> &mut Self {
185        self.paths.push(path);
186        self
187    }
188
189    pub fn add_positional(&mut self, required: bool, name: &str) -> Result<&mut Self, BuildError> {
190        if !required {
191            if self.arity.rest.is_some() {
192                return Err(BuildError::OptionalParametersAfterRest);
193            } else if !self.arity.trailing.is_empty() {
194                return Err(BuildError::OptionalParametersAfterTrailingPositionals);
195            } else {
196                self.arity.optionals.push(name.to_string());
197            }
198        } else if !self.arity.optionals.is_empty() || self.arity.rest.is_some() {
199            self.arity.trailing.push(name.to_string());
200        } else {
201            self.arity.leading.push(name.to_string());
202        }
203
204        Ok(self)
205    }
206
207    pub fn add_rest(&mut self, name: &str) -> Result<&mut Self, BuildError> {
208        if self.arity.rest.is_some() {
209            return Err(BuildError::MultipleRestParameters);
210        } else if !self.arity.trailing.is_empty() {
211            return Err(BuildError::RestAfterTrailingPositionals);
212        } else {
213            self.arity.rest = Some(name.to_string());
214        }
215
216        Ok(self)
217    }
218
219    pub fn add_proxy(&mut self, name: &str) -> Result<&mut Self, BuildError> {
220        self.add_rest(name)?;
221        self.arity.proxy = true;
222
223        Ok(self)
224    }
225
226    pub fn add_option(&mut self, option: OptionDefinition) -> Result<&mut Self, BuildError> {
227        if !option.allow_binding && option.arity > 1 {
228            return Err(BuildError::ArityTooHighForNonBindingOption);
229        }
230
231        let preferred_name = option.name_set.iter()
232            .max_by_key(|s| s.len()).unwrap()
233            .clone();
234
235        for name in &option.name_set {
236            self.preferred_names.insert(name.to_string(), preferred_name.clone());
237        }
238
239        if option.allow_binding {
240            self.valid_bindings.insert(preferred_name.clone());
241        }
242
243        if option.required {
244            self.required_options.push(preferred_name.clone());
245        }
246
247        self.options.insert(preferred_name, option);
248
249        Ok(self)
250    }
251
252    pub fn compile(&self) -> Machine {
253        let mut machine = Machine::new();
254
255        let context = &mut machine.contexts[0];
256
257        context.preferred_names = self.preferred_names.clone();
258        context.valid_bindings = self.valid_bindings.clone();
259
260        let first_node_id = machine.inject_node(Node::new());
261
262        machine.register_static(INITIAL_NODE_ID, Arg::StartOfInput, first_node_id, Reducer::InitializeState(self.cli_index, self.required_options.clone()));
263
264        let positional_argument = match self.arity.proxy {
265            true => Check::Always,
266            false => Check::IsNotOptionLike,
267        };
268
269        for path in &self.paths {
270            let mut last_path_node_id = first_node_id;
271
272            // We allow options to be specified before the path. Note that we
273            // only do this when there is a path, otherwise there would be
274            // some redundancy with the options attached later.
275            if !path.is_empty() {
276                let option_node_id = machine.inject_node(Node::new());
277                machine.register_shortcut(last_path_node_id, option_node_id, Reducer::None);
278                self.register_options(&mut machine, option_node_id);
279                last_path_node_id = option_node_id;
280            }
281
282            for t in 0..path.len() {
283                let next_path_node_id = machine.inject_node(Node::new());
284                machine.register_static(last_path_node_id, Arg::User(path[t].clone()), next_path_node_id, Reducer::PushPath);
285                last_path_node_id = next_path_node_id;
286
287                if t + 1 < path.len() {
288                    // Allow to pass `-h` (without anything after it) after each part of a path.
289                    // Note that we do not do this for the last part, otherwise there would be
290                    // some redundancy with the `useHelp` attached later.
291                    let help_node_id = machine.inject_node(Node::new());
292                    machine.register_dynamic(last_path_node_id, Check::IsHelp, help_node_id, Reducer::UseHelp);
293                    machine.register_static(help_node_id, Arg::EndOfInput, SUCCESS_NODE_ID, Reducer::None);
294                }
295            }
296
297            if !self.arity.leading.is_empty() || !self.arity.proxy {
298                let help_node_id = machine.inject_node(Node::new());
299                machine.register_dynamic(last_path_node_id, Check::IsHelp, help_node_id, Reducer::UseHelp);
300                machine.register_dynamic(help_node_id, Check::Always, help_node_id, Reducer::PushOptional);
301                machine.register_static(help_node_id, Arg::EndOfInput, SUCCESS_NODE_ID, Reducer::None);
302
303                self.register_options(&mut machine, last_path_node_id);
304            }
305
306            if !self.arity.leading.is_empty() {
307                machine.register_static(last_path_node_id, Arg::EndOfInput, ERROR_NODE_ID, Reducer::SetError(CommandError::MissingPositionalArguments));
308                machine.register_static(last_path_node_id, Arg::EndOfPartialInput, SUCCESS_NODE_ID, Reducer::AcceptState);
309            }
310
311            let mut last_leading_node_id = last_path_node_id;
312            for t in 0..self.arity.leading.len() {
313                let next_leading_node_id = machine.inject_node(Node::new());
314
315                if !self.arity.proxy || t + 1 != self.arity.leading.len() {
316                    self.register_options(&mut machine, next_leading_node_id);
317                }
318
319                if !self.arity.trailing.is_empty() || t + 1 != self.arity.leading.len() {
320                    machine.register_static(next_leading_node_id, Arg::EndOfInput, ERROR_NODE_ID, Reducer::SetError(CommandError::MissingPositionalArguments));
321                    machine.register_static(next_leading_node_id, Arg::EndOfPartialInput, SUCCESS_NODE_ID, Reducer::AcceptState);
322                }
323
324                machine.register_dynamic(last_leading_node_id, Check::IsNotOptionLike, next_leading_node_id, Reducer::PushPositional);
325                last_leading_node_id = next_leading_node_id;
326            }
327
328            let mut last_extra_node_id = last_leading_node_id;
329            if self.arity.rest.is_some() || !self.arity.optionals.is_empty() {
330                let extra_shortcut_node_id = machine.inject_node(Node::new());
331                machine.register_shortcut(last_leading_node_id, extra_shortcut_node_id, Reducer::None);
332
333                if self.arity.rest.is_some() {
334                    let extra_node_id = machine.inject_node(Node::new());
335
336                    if !self.arity.proxy {
337                        self.register_options(&mut machine, extra_node_id);
338                    }
339
340                    machine.register_dynamic(last_leading_node_id, positional_argument.clone(), extra_node_id, Reducer::PushRest);
341                    machine.register_dynamic(extra_node_id, positional_argument.clone(), extra_node_id, Reducer::PushRest);
342                    machine.register_shortcut(extra_node_id, extra_shortcut_node_id, Reducer::None);
343                } else {
344                    for _ in 0..self.arity.optionals.len() {
345                        let extra_node_id = machine.inject_node(Node::new());
346
347                        if !self.arity.proxy {
348                            self.register_options(&mut machine, extra_node_id);
349                        }
350
351                        machine.register_dynamic(last_extra_node_id, positional_argument.clone(), extra_node_id, Reducer::PushOptional);
352                        machine.register_shortcut(extra_node_id, extra_shortcut_node_id, Reducer::None);
353                        last_extra_node_id = extra_node_id;
354                    }
355                }
356
357                last_extra_node_id = extra_shortcut_node_id;
358            }
359
360            if !self.arity.trailing.is_empty() {
361                machine.register_static(last_extra_node_id, Arg::EndOfInput, ERROR_NODE_ID, Reducer::SetError(CommandError::MissingPositionalArguments));
362                machine.register_static(last_extra_node_id, Arg::EndOfPartialInput, SUCCESS_NODE_ID, Reducer::AcceptState);
363            }
364
365            let mut last_trailing_node_id = last_extra_node_id;
366            for t in 0..self.arity.trailing.len() {
367                let next_trailing_node_id = machine.inject_node(Node::new());
368
369                if !self.arity.proxy {
370                    self.register_options(&mut machine, next_trailing_node_id);
371                }
372
373                if t + 1 < self.arity.trailing.len() {
374                    machine.register_static(next_trailing_node_id, Arg::EndOfInput, ERROR_NODE_ID, Reducer::SetError(CommandError::MissingPositionalArguments));
375                    machine.register_static(next_trailing_node_id, Arg::EndOfPartialInput, SUCCESS_NODE_ID, Reducer::AcceptState);
376                }
377
378                machine.register_dynamic(last_trailing_node_id, Check::IsNotOptionLike, next_trailing_node_id, Reducer::PushPositional);
379                last_trailing_node_id = next_trailing_node_id;
380            }
381
382            machine.register_dynamic(last_trailing_node_id, positional_argument.clone(), ERROR_NODE_ID, Reducer::SetError(CommandError::ExtraneousPositionalArguments));
383            machine.register_static(last_trailing_node_id, Arg::EndOfInput, SUCCESS_NODE_ID, Reducer::AcceptState);
384            machine.register_static(last_trailing_node_id, Arg::EndOfPartialInput, SUCCESS_NODE_ID, Reducer::AcceptState);
385        }
386
387        machine
388    }
389
390    fn register_options(&self, machine: &mut Machine, node_id: usize) {
391        machine.register_dynamic(node_id, Check::IsExactOption("--".to_string()), node_id, Reducer::InhibateOptions);
392        machine.register_dynamic(node_id, Check::IsBatchOption, node_id, Reducer::PushBatch);
393        machine.register_dynamic(node_id, Check::IsBoundOption, node_id, Reducer::PushBound);
394        machine.register_dynamic(node_id, Check::IsUnsupportedOption, ERROR_NODE_ID, Reducer::SetError(CommandError::UnknownOption));
395        machine.register_dynamic(node_id, Check::IsInvalidOption, ERROR_NODE_ID, Reducer::SetError(CommandError::InvalidOption));
396
397        for (preferred_name, option) in &self.options {
398            if option.arity == 0 {
399                for name in &option.name_set {
400                    machine.register_dynamic(node_id, Check::IsExactOption(name.to_string()), node_id, Reducer::PushTrue(preferred_name.clone()));
401
402                    if name.starts_with("--") && !name.starts_with("--no-") {
403                        machine.register_dynamic(node_id, Check::IsNegatedOption(name.to_string()), node_id, Reducer::PushFalse(preferred_name.clone()));
404                    }
405                }
406            } else {
407                // We inject a new node at the end of the state machine
408                let mut last_node_id = machine.inject_node(Node::new());
409
410                // We register transitions from the starting node to this new node
411                for name in &option.name_set {
412                    machine.register_dynamic(node_id, Check::IsExactOption(name.to_string()), last_node_id, Reducer::PushNone(preferred_name.clone()));
413                }
414
415                // For each argument, we inject a new node at the end and we
416                // register a transition from the current node to this new node
417                for _ in 0..option.arity {
418                    let next_node_id = machine.inject_node(Node::new());
419
420                    // We can provide better errors when another option or EndOfInput is encountered
421                    machine.register_static(last_node_id, Arg::EndOfInput, ERROR_NODE_ID, Reducer::SetOptionArityError);
422                    machine.register_static(last_node_id, Arg::EndOfPartialInput, ERROR_NODE_ID, Reducer::SetOptionArityError);
423                    machine.register_dynamic(last_node_id, Check::IsOptionLike, ERROR_NODE_ID, Reducer::SetOptionArityError);
424
425                    // If the option has a single argument, no need to store it in an array
426                    let action = match option.arity {
427                        1 => Reducer::ResetStringValue,
428                        _ => Reducer::AppendStringValue,
429                    };
430
431                    machine.register_dynamic(last_node_id, Check::IsNotOptionLike, next_node_id, action);
432
433                    last_node_id = next_node_id;
434                }
435
436                // In the end, we register a shortcut from
437                // the last node back to the starting node
438                machine.register_shortcut(last_node_id, node_id, Reducer::None);
439            }
440        }
441    }
442}