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