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 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 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 let mut last_node_id = machine.inject_node(Node::new());
409
410 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 _ in 0..option.arity {
418 let next_node_id = machine.inject_node(Node::new());
419
420 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 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 machine.register_shortcut(last_node_id, node_id, Reducer::None);
439 }
440 }
441 }
442}