1use crate::error::{CliError, CliErrorKind};
2use crate::raw::RawArgs;
3use crate::spec::{ArgumentSpec, CliSpec, CommandSpec, OptionSpec, ValueMode, ValueType};
4use std::collections::HashSet;
5use std::fmt;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
8pub struct ParsedCli {
9 command_path: Vec<String>,
10 options: Vec<ParsedOption>,
11 positionals: Vec<String>,
12 passthrough: Vec<String>,
13}
14
15#[derive(Clone, Debug, PartialEq, Eq)]
16pub struct ParsedOption {
17 name: String,
18 value: Option<String>,
19}
20
21pub struct CliParser<'a> {
22 spec: &'a CliSpec,
23}
24
25impl ParsedCli {
26 pub fn command_path(&self) -> &[String] {
27 &self.command_path
28 }
29 pub fn options(&self) -> &[ParsedOption] {
30 &self.options
31 }
32 pub fn positionals(&self) -> &[String] {
33 &self.positionals
34 }
35 pub fn passthrough(&self) -> &[String] {
36 &self.passthrough
37 }
38 pub fn has_flag(&self, name: &str) -> bool {
39 self.options.iter().any(|option| option.name == name)
40 }
41 pub fn option_value(&self, name: &str) -> Option<&str> {
42 self.options
43 .iter()
44 .rev()
45 .find(|option| option.name == name)
46 .and_then(ParsedOption::value)
47 }
48 pub fn option_values<'b>(&'b self, name: &'b str) -> impl Iterator<Item = &'b str> + 'b {
49 self.options
50 .iter()
51 .filter(move |option| option.name == name)
52 .filter_map(ParsedOption::value)
53 }
54 pub fn option_occurrences(&self, name: &str) -> usize {
55 self.options
56 .iter()
57 .filter(|option| option.name == name)
58 .count()
59 }
60}
61
62impl ParsedOption {
63 pub fn name(&self) -> &str {
64 &self.name
65 }
66 pub fn value(&self) -> Option<&str> {
67 self.value.as_deref()
68 }
69}
70
71impl<'a> CliParser<'a> {
72 pub fn new(spec: &'a CliSpec) -> Self {
73 Self { spec }
74 }
75
76 pub fn parse(&self, args: &RawArgs) -> Result<ParsedCli, CliError> {
77 self.spec.validate().map_err(|error| {
78 CliError::new(CliErrorKind::InvalidSpecification, error.to_string())
79 })?;
80 let mut state = ParseState::new(self.spec);
81 let mut index = 0usize;
82 while index < args.words().len() {
83 let word = &args.words()[index];
84 if word == "--" {
85 state
86 .passthrough
87 .extend_from_slice(&args.words()[index + 1..]);
88 break;
89 }
90 index = if word.starts_with("--") {
91 parse_long_option(args.words(), index, &mut state)?
92 } else if is_short_option(word) {
93 parse_short_options(args.words(), index, &mut state)?
94 } else if state.positionals.is_empty() && state.enter_command(word) {
95 index + 1
96 } else {
97 state.push_positional(word)?;
98 index + 1
99 };
100 }
101 state.finish()
102 }
103}
104
105struct ParseState<'a> {
106 spec: &'a CliSpec,
107 commands: Vec<&'a CommandSpec>,
108 command_path: Vec<String>,
109 options: Vec<ParsedOption>,
110 positionals: Vec<String>,
111 passthrough: Vec<String>,
112}
113
114impl<'a> ParseState<'a> {
115 fn new(spec: &'a CliSpec) -> Self {
116 Self {
117 spec,
118 commands: Vec::new(),
119 command_path: Vec::new(),
120 options: Vec::new(),
121 positionals: Vec::new(),
122 passthrough: Vec::new(),
123 }
124 }
125
126 fn enter_command(&mut self, word: &str) -> bool {
127 if let Some(command) = self
128 .available_commands()
129 .iter()
130 .find(|command| command.matches(word))
131 {
132 self.command_path.push(command.name().to_string());
133 self.commands.push(command);
134 true
135 } else {
136 false
137 }
138 }
139
140 fn available_commands(&self) -> &'a [CommandSpec] {
141 self.commands
142 .last()
143 .map(|command| command.commands())
144 .unwrap_or_else(|| self.spec.commands())
145 }
146
147 fn active_arguments(&self) -> &'a [ArgumentSpec] {
148 self.commands
149 .last()
150 .map(|command| command.arguments())
151 .unwrap_or_else(|| self.spec.arguments())
152 }
153
154 fn visible_options(&self) -> Vec<&'a OptionSpec> {
155 let mut options = self.spec.options().iter().collect::<Vec<_>>();
156 for command in &self.commands {
157 options.extend(command.options());
158 }
159 options
160 }
161
162 fn find_long_option(&self, name: &str) -> Option<&'a OptionSpec> {
163 self.visible_options()
164 .into_iter()
165 .rev()
166 .find(|option| option.long() == name)
167 }
168
169 fn find_short_option(&self, short: char) -> Option<&'a OptionSpec> {
170 self.visible_options()
171 .into_iter()
172 .rev()
173 .find(|option| option.short_name() == Some(short))
174 }
175
176 fn push_option(&mut self, option: &OptionSpec, value: Option<String>) -> Result<(), CliError> {
177 if !option.is_repeatable()
178 && self
179 .options
180 .iter()
181 .any(|parsed| parsed.name == option.long())
182 {
183 return Err(CliError::new(
184 CliErrorKind::DuplicateOption,
185 format!("option `--{}` cannot be repeated", option.long()),
186 ));
187 }
188 if let Some(value) = value.as_deref() {
189 validate_value(
190 value,
191 option.value_type_kind(),
192 option.possible_values(),
193 &format!("--{}", option.long()),
194 )?;
195 }
196 self.options.push(ParsedOption {
197 name: option.long().to_string(),
198 value,
199 });
200 Ok(())
201 }
202
203 fn push_positional(&mut self, value: &str) -> Result<(), CliError> {
204 let arguments = self.active_arguments();
205 let argument = arguments
206 .get(self.positionals.len())
207 .or_else(|| arguments.last().filter(|argument| argument.is_multiple()));
208 let argument = argument.ok_or_else(|| {
209 CliError::new(
210 CliErrorKind::UnexpectedArgument,
211 format!("unexpected argument `{value}`"),
212 )
213 })?;
214 validate_value(
215 value,
216 argument.value_type_kind(),
217 argument.possible_values(),
218 argument.name(),
219 )?;
220 self.positionals.push(value.to_string());
221 Ok(())
222 }
223
224 fn finish(self) -> Result<ParsedCli, CliError> {
225 if !has_terminal_option(&self) {
226 validate_required_command(&self)?;
227 validate_required_arguments(&self)?;
228 validate_option_rules(&self)?;
229 }
230 Ok(ParsedCli {
231 command_path: self.command_path,
232 options: self.options,
233 positionals: self.positionals,
234 passthrough: self.passthrough,
235 })
236 }
237}
238
239fn has_terminal_option(state: &ParseState<'_>) -> bool {
240 state.visible_options().iter().any(|option| {
241 option.is_terminal()
242 && state
243 .options
244 .iter()
245 .any(|parsed| parsed.name == option.long())
246 })
247}
248
249fn parse_long_option(
250 words: &[String],
251 index: usize,
252 state: &mut ParseState<'_>,
253) -> Result<usize, CliError> {
254 let raw = words[index].strip_prefix("--").unwrap_or_default();
255 let (name, inline) = raw
256 .split_once('=')
257 .map_or((raw, None), |(name, value)| (name, Some(value.to_string())));
258 if name.is_empty() {
259 return Err(CliError::new(
260 CliErrorKind::UnknownOption,
261 "empty long option is not accepted",
262 ));
263 }
264 let option = state.find_long_option(name).ok_or_else(|| {
265 CliError::new(
266 CliErrorKind::UnknownOption,
267 format!("unknown option `--{name}`"),
268 )
269 })?;
270 let (value, next) = option_value(words, index, inline, option, false)?;
271 state.push_option(option, value)?;
272 Ok(next)
273}
274
275fn parse_short_options(
276 words: &[String],
277 index: usize,
278 state: &mut ParseState<'_>,
279) -> Result<usize, CliError> {
280 let raw = words[index].strip_prefix('-').unwrap_or_default();
281 let mut chars = raw.char_indices().peekable();
282 while let Some((_, short)) = chars.next() {
283 let option = state.find_short_option(short).ok_or_else(|| {
284 CliError::new(
285 CliErrorKind::UnknownOption,
286 format!("unknown option `-{short}`"),
287 )
288 })?;
289 let remainder = chars.peek().map(|(offset, _)| raw[*offset..].to_string());
290 let (value, next) = option_value(words, index, remainder, option, true)?;
291 state.push_option(option, value)?;
292 if option.value_mode() != ValueMode::Forbidden {
293 return Ok(next);
294 }
295 }
296 Ok(index + 1)
297}
298
299fn option_value(
300 words: &[String],
301 index: usize,
302 inline: Option<String>,
303 option: &OptionSpec,
304 short: bool,
305) -> Result<(Option<String>, usize), CliError> {
306 match option.value_mode() {
307 ValueMode::Forbidden if inline.is_some() && !short => Err(CliError::new(
308 CliErrorKind::InvalidValue,
309 format!("option `--{}` does not accept a value", option.long()),
310 )),
311 ValueMode::Forbidden => Ok((None, index + 1)),
312 ValueMode::Optional => Ok((inline, index + 1)),
313 ValueMode::Required => {
314 if let Some(value) = inline {
315 Ok((Some(value), index + 1))
316 } else if let Some(value) = words.get(index + 1) {
317 Ok((Some(value.clone()), index + 2))
318 } else {
319 Err(CliError::new(
320 CliErrorKind::MissingValue,
321 format!("option `--{}` requires a value", option.long()),
322 ))
323 }
324 }
325 }
326}
327
328fn validate_required_command(state: &ParseState<'_>) -> Result<(), CliError> {
329 let required = state
330 .commands
331 .last()
332 .map(|command| command.is_command_required())
333 .unwrap_or_else(|| state.spec.is_command_required());
334 if required && !state.available_commands().is_empty() {
335 return Err(CliError::new(
336 CliErrorKind::MissingCommand,
337 format!(
338 "a command is required after `{}`",
339 if state.command_path.is_empty() {
340 state.spec.name().to_string()
341 } else {
342 state.command_path.join(" ")
343 }
344 ),
345 ));
346 }
347 Ok(())
348}
349
350fn validate_required_arguments(state: &ParseState<'_>) -> Result<(), CliError> {
351 let provided = state.positionals.len();
352 for (index, argument) in state.active_arguments().iter().enumerate() {
353 if argument.is_required() && index >= provided {
354 return Err(CliError::new(
355 CliErrorKind::MissingValue,
356 format!("missing required argument `<{}>`", argument.name()),
357 ));
358 }
359 }
360 Ok(())
361}
362
363fn validate_option_rules(state: &ParseState<'_>) -> Result<(), CliError> {
364 let present = state
365 .options
366 .iter()
367 .map(|option| option.name.as_str())
368 .collect::<HashSet<_>>();
369 for option in state.visible_options() {
370 if option.is_required() && !present.contains(option.long()) {
371 return Err(CliError::new(
372 CliErrorKind::MissingOption,
373 format!("required option `--{}` was not provided", option.long()),
374 ));
375 }
376 if !present.contains(option.long()) {
377 continue;
378 }
379 if let Some(conflict) = option
380 .conflicts()
381 .iter()
382 .find(|name| present.contains(name.as_str()))
383 {
384 return Err(CliError::new(
385 CliErrorKind::OptionConflict,
386 format!("option `--{}` conflicts with `--{conflict}`", option.long()),
387 ));
388 }
389 if let Some(required) = option
390 .requirements()
391 .iter()
392 .find(|name| !present.contains(name.as_str()))
393 {
394 return Err(CliError::new(
395 CliErrorKind::MissingRequirement,
396 format!("option `--{}` requires `--{required}`", option.long()),
397 ));
398 }
399 }
400 Ok(())
401}
402
403fn validate_value(
404 value: &str,
405 value_type: ValueType,
406 possible: &[String],
407 owner: &str,
408) -> Result<(), CliError> {
409 let typed = match value_type {
410 ValueType::String => true,
411 ValueType::Bool => matches!(value, "true" | "false"),
412 ValueType::I64 => value.parse::<i64>().is_ok(),
413 ValueType::U64 => value.parse::<u64>().is_ok(),
414 };
415 if !typed {
416 return Err(CliError::new(
417 CliErrorKind::InvalidValue,
418 format!("invalid value `{value}` for `{owner}`; expected {value_type}"),
419 ));
420 }
421 if !possible.is_empty() && !possible.iter().any(|candidate| candidate == value) {
422 return Err(CliError::new(
423 CliErrorKind::InvalidValue,
424 format!(
425 "invalid value `{value}` for `{owner}`; expected one of: {}",
426 possible.join(", ")
427 ),
428 ));
429 }
430 Ok(())
431}
432
433impl fmt::Display for ValueType {
434 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435 formatter.write_str(match self {
436 Self::String => "text",
437 Self::Bool => "true or false",
438 Self::I64 => "a signed integer",
439 Self::U64 => "an unsigned integer",
440 })
441 }
442}
443
444fn is_short_option(word: &str) -> bool {
445 word.starts_with('-') && !word.starts_with("--") && word.len() > 1
446}