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