1#[allow(unused_imports)]
43use crate::config::schema::{ArgumentDefinition, CommandDefinition, OptionDefinition};
44use crate::error::{ParseError, Result};
45use crate::parser::type_parser;
46use std::collections::HashMap;
47
48pub struct CliParser<'a> {
95 definition: &'a CommandDefinition,
97}
98
99impl<'a> CliParser<'a> {
100 pub fn new(definition: &'a CommandDefinition) -> Self {
124 Self { definition }
125 }
126
127 pub fn parse(&self, args: &[String]) -> Result<HashMap<String, String>> {
181 let mut result = HashMap::new();
182 let mut positional_index = 0;
183 let mut i = 0;
184
185 while i < args.len() {
187 let arg = &args[i];
188
189 if arg.starts_with("--") {
190 self.parse_long_option(arg, args, &mut i, &mut result)?;
192 } else if arg.starts_with('-') && arg.len() > 1 {
193 if arg
195 .chars()
196 .nth(1)
197 .map(|c| c.is_ascii_digit())
198 .unwrap_or(false)
199 {
200 self.parse_positional_argument(arg, positional_index, &mut result)?;
202 positional_index += 1;
203 } else {
204 self.parse_short_option(arg, args, &mut i, &mut result)?;
205 }
206 } else {
207 self.parse_positional_argument(arg, positional_index, &mut result)?;
209 positional_index += 1;
210 }
211
212 i += 1;
213 }
214
215 self.apply_defaults(&mut result)?;
217
218 self.validate_required_arguments(&result)?;
220 self.validate_required_options(&result)?;
221
222 Ok(result)
223 }
224
225 fn parse_long_option(
227 &self,
228 arg: &str,
229 args: &[String],
230 index: &mut usize,
231 result: &mut HashMap<String, String>,
232 ) -> Result<()> {
233 let arg_without_dashes = &arg[2..];
234
235 if let Some(eq_pos) = arg_without_dashes.find('=') {
237 let option_name = &arg_without_dashes[..eq_pos];
238 let value = &arg_without_dashes[eq_pos + 1..];
239
240 let option = self.find_option_by_long(option_name)?;
241 let parsed_value = type_parser::parse_value(value, option.option_type)?;
242 result.insert(option.name.clone(), parsed_value);
243 } else {
244 let option = self.find_option_by_long(arg_without_dashes)?;
246
247 if matches!(
249 option.option_type,
250 crate::config::schema::ArgumentType::Bool
251 ) {
252 result.insert(option.name.clone(), "true".to_string());
253 } else {
254 *index += 1;
256 if *index >= args.len() {
257 return Err(ParseError::InvalidSyntax {
258 details: format!(
259 "Option --{} requires a value",
260 option.long.as_ref().unwrap()
261 ),
262 hint: Some(format!(
263 "Usage: --{}=<value> or --{} <value>",
264 option.long.as_ref().unwrap(),
265 option.long.as_ref().unwrap()
266 )),
267 }
268 .into());
269 }
270
271 let value = &args[*index];
272 let parsed_value = type_parser::parse_value(value, option.option_type)?;
273 result.insert(option.name.clone(), parsed_value);
274 }
275 }
276
277 Ok(())
278 }
279
280 fn parse_short_option(
282 &self,
283 arg: &str,
284 args: &[String],
285 index: &mut usize,
286 result: &mut HashMap<String, String>,
287 ) -> Result<()> {
288 let short_flag = &arg[1..2];
289 let option = self.find_option_by_short(short_flag)?;
290
291 if matches!(
293 option.option_type,
294 crate::config::schema::ArgumentType::Bool
295 ) {
296 result.insert(option.name.clone(), "true".to_string());
297 } else {
298 if arg.len() > 2 {
300 let value = &arg[2..];
301 let parsed_value = type_parser::parse_value(value, option.option_type)?;
302 result.insert(option.name.clone(), parsed_value);
303 } else {
304 *index += 1;
306 if *index >= args.len() {
307 return Err(ParseError::InvalidSyntax {
308 details: format!("Option -{} requires a value", short_flag),
309 hint: Some(format!(
310 "Usage: -{}<value> or -{} <value>",
311 short_flag, short_flag
312 )),
313 }
314 .into());
315 }
316
317 let value = &args[*index];
318 let parsed_value = type_parser::parse_value(value, option.option_type)?;
319 result.insert(option.name.clone(), parsed_value);
320 }
321 }
322
323 Ok(())
324 }
325
326 fn parse_positional_argument(
328 &self,
329 value: &str,
330 index: usize,
331 result: &mut HashMap<String, String>,
332 ) -> Result<()> {
333 if index >= self.definition.arguments.len() {
334 return Err(ParseError::too_many_arguments(
335 &self.definition.name,
336 self.definition.arguments.len(),
337 index + 1,
338 )
339 .into());
340 }
341
342 let arg_def = &self.definition.arguments[index];
343 let parsed_value = type_parser::parse_value(value, arg_def.arg_type)?;
344 result.insert(arg_def.name.clone(), parsed_value);
345
346 Ok(())
347 }
348
349 fn apply_defaults(&self, result: &mut HashMap<String, String>) -> Result<()> {
351 for option in &self.definition.options {
352 if !result.contains_key(&option.name) {
353 if let Some(ref default) = option.default {
354 let parsed_default = type_parser::parse_value(default, option.option_type)?;
356 result.insert(option.name.clone(), parsed_default);
357 }
358 }
359 }
360 Ok(())
361 }
362
363 fn validate_required_arguments(&self, result: &HashMap<String, String>) -> Result<()> {
365 for arg in &self.definition.arguments {
366 if arg.required && !result.contains_key(&arg.name) {
367 return Err(ParseError::missing_argument(&arg.name, &self.definition.name).into());
368 }
369 }
370 Ok(())
371 }
372
373 fn validate_required_options(&self, result: &HashMap<String, String>) -> Result<()> {
375 for option in &self.definition.options {
376 if option.required && !result.contains_key(&option.name) {
377 return Err(ParseError::missing_option(
378 &option
379 .long
380 .clone()
381 .or(option.short.clone())
382 .unwrap_or_default(),
383 &self.definition.name,
384 )
385 .into());
386 }
387 }
388 Ok(())
389 }
390
391 fn find_option_by_long(&self, long: &str) -> Result<&OptionDefinition> {
393 self.definition
394 .options
395 .iter()
396 .find(|opt| opt.long.as_deref() == Some(long))
397 .ok_or_else(|| {
398 let available: Vec<String> = self
399 .definition
400 .options
401 .iter()
402 .filter_map(|o| o.long.clone())
403 .collect();
404 ParseError::unknown_option_with_suggestions(
405 &format!("--{}", long),
406 &self.definition.name,
407 &available,
408 )
409 .into()
410 })
411 }
412
413 fn find_option_by_short(&self, short: &str) -> Result<&OptionDefinition> {
415 self.definition
416 .options
417 .iter()
418 .find(|opt| opt.short.as_deref() == Some(short))
419 .ok_or_else(|| {
420 let available: Vec<String> = self
421 .definition
422 .options
423 .iter()
424 .filter_map(|o| o.short.clone())
425 .collect();
426 ParseError::unknown_option_with_suggestions(
427 &format!("-{}", short),
428 &self.definition.name,
429 &available,
430 )
431 .into()
432 })
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439 use crate::config::schema::{ArgumentType, OptionDefinition};
440
441 fn create_test_definition() -> CommandDefinition {
443 CommandDefinition {
444 name: "test".to_string(),
445 aliases: vec![],
446 description: "Test command".to_string(),
447 required: false,
448 arguments: vec![
449 ArgumentDefinition {
450 name: "input".to_string(),
451 arg_type: ArgumentType::Path,
452 required: true,
453 description: "Input file".to_string(),
454 validation: vec![],
455 },
456 ArgumentDefinition {
457 name: "output".to_string(),
458 arg_type: ArgumentType::Path,
459 required: false,
460 description: "Output file".to_string(),
461 validation: vec![],
462 },
463 ],
464 options: vec![
465 OptionDefinition {
466 name: "verbose".to_string(),
467 short: Some("v".to_string()),
468 long: Some("verbose".to_string()),
469 option_type: ArgumentType::Bool,
470 required: false,
471 default: Some("false".to_string()),
472 description: "Verbose output".to_string(),
473 choices: vec![],
474 },
475 OptionDefinition {
476 name: "count".to_string(),
477 short: Some("c".to_string()),
478 long: Some("count".to_string()),
479 option_type: ArgumentType::Integer,
480 required: false,
481 default: Some("10".to_string()),
482 description: "Count".to_string(),
483 choices: vec![],
484 },
485 ],
486 implementation: "handler".to_string(),
487 }
488 }
489
490 #[test]
495 fn test_parse_single_positional_argument() {
496 let definition = create_test_definition();
497 let parser = CliParser::new(&definition);
498
499 let args = vec!["input.txt".to_string()];
500 let result = parser.parse(&args).unwrap();
501
502 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
503 }
504
505 #[test]
506 fn test_parse_multiple_positional_arguments() {
507 let definition = create_test_definition();
508 let parser = CliParser::new(&definition);
509
510 let args = vec!["input.txt".to_string(), "output.txt".to_string()];
511 let result = parser.parse(&args).unwrap();
512
513 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
514 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
515 }
516
517 #[test]
518 fn test_parse_missing_required_argument() {
519 let definition = create_test_definition();
520 let parser = CliParser::new(&definition);
521
522 let args: Vec<String> = vec![];
523 let result = parser.parse(&args);
524
525 assert!(result.is_err());
526 match result.unwrap_err() {
527 crate::error::DynamicCliError::Parse(ParseError::MissingArgument {
528 argument, ..
529 }) => {
530 assert_eq!(argument, "input");
531 }
532 other => panic!("Expected MissingArgument error, got {:?}", other),
533 }
534 }
535
536 #[test]
537 fn test_parse_too_many_positional_arguments() {
538 let definition = create_test_definition();
539 let parser = CliParser::new(&definition);
540
541 let args = vec![
542 "input.txt".to_string(),
543 "output.txt".to_string(),
544 "extra.txt".to_string(),
545 ];
546 let result = parser.parse(&args);
547
548 assert!(result.is_err());
549 match result.unwrap_err() {
550 crate::error::DynamicCliError::Parse(ParseError::TooManyArguments { .. }) => {}
551 other => panic!("Expected TooManyArguments error, got {:?}", other),
552 }
553 }
554
555 #[test]
560 fn test_parse_long_boolean_option() {
561 let definition = create_test_definition();
562 let parser = CliParser::new(&definition);
563
564 let args = vec!["input.txt".to_string(), "--verbose".to_string()];
565 let result = parser.parse(&args).unwrap();
566
567 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
568 }
569
570 #[test]
571 fn test_parse_long_option_with_equals() {
572 let definition = create_test_definition();
573 let parser = CliParser::new(&definition);
574
575 let args = vec!["input.txt".to_string(), "--count=42".to_string()];
576 let result = parser.parse(&args).unwrap();
577
578 assert_eq!(result.get("count"), Some(&"42".to_string()));
579 }
580
581 #[test]
582 fn test_parse_long_option_with_space() {
583 let definition = create_test_definition();
584 let parser = CliParser::new(&definition);
585
586 let args = vec![
587 "input.txt".to_string(),
588 "--count".to_string(),
589 "42".to_string(),
590 ];
591 let result = parser.parse(&args).unwrap();
592
593 assert_eq!(result.get("count"), Some(&"42".to_string()));
594 }
595
596 #[test]
597 fn test_parse_unknown_long_option() {
598 let definition = create_test_definition();
599 let parser = CliParser::new(&definition);
600
601 let args = vec!["input.txt".to_string(), "--unknown".to_string()];
602 let result = parser.parse(&args);
603
604 assert!(result.is_err());
605 match result.unwrap_err() {
606 crate::error::DynamicCliError::Parse(ParseError::UnknownOption { .. }) => {}
607 other => panic!("Expected UnknownOption error, got {:?}", other),
608 }
609 }
610
611 #[test]
616 fn test_parse_short_boolean_option() {
617 let definition = create_test_definition();
618 let parser = CliParser::new(&definition);
619
620 let args = vec!["input.txt".to_string(), "-v".to_string()];
621 let result = parser.parse(&args).unwrap();
622
623 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
624 }
625
626 #[test]
627 fn test_parse_short_option_with_space() {
628 let definition = create_test_definition();
629 let parser = CliParser::new(&definition);
630
631 let args = vec!["input.txt".to_string(), "-c".to_string(), "42".to_string()];
632 let result = parser.parse(&args).unwrap();
633
634 assert_eq!(result.get("count"), Some(&"42".to_string()));
635 }
636
637 #[test]
638 fn test_parse_short_option_attached_value() {
639 let definition = create_test_definition();
640 let parser = CliParser::new(&definition);
641
642 let args = vec!["input.txt".to_string(), "-c42".to_string()];
643 let result = parser.parse(&args).unwrap();
644
645 assert_eq!(result.get("count"), Some(&"42".to_string()));
646 }
647
648 #[test]
649 fn test_parse_negative_number_as_positional() {
650 let definition = create_test_definition();
651 let parser = CliParser::new(&definition);
652
653 let args = vec!["-123".to_string()];
655 let result = parser.parse(&args).unwrap();
656
657 assert_eq!(result.get("input"), Some(&"-123".to_string()));
658 }
659
660 #[test]
665 fn test_apply_default_values() {
666 let definition = create_test_definition();
667 let parser = CliParser::new(&definition);
668
669 let args = vec!["input.txt".to_string()];
670 let result = parser.parse(&args).unwrap();
671
672 assert_eq!(result.get("verbose"), Some(&"false".to_string()));
674 assert_eq!(result.get("count"), Some(&"10".to_string()));
675 }
676
677 #[test]
678 fn test_override_default_values() {
679 let definition = create_test_definition();
680 let parser = CliParser::new(&definition);
681
682 let args = vec![
683 "input.txt".to_string(),
684 "-v".to_string(),
685 "-c".to_string(),
686 "5".to_string(),
687 ];
688 let result = parser.parse(&args).unwrap();
689
690 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
692 assert_eq!(result.get("count"), Some(&"5".to_string()));
693 }
694
695 #[test]
700 fn test_type_conversion_error() {
701 let definition = create_test_definition();
702 let parser = CliParser::new(&definition);
703
704 let args = vec![
706 "input.txt".to_string(),
707 "--count".to_string(),
708 "abc".to_string(),
709 ];
710 let result = parser.parse(&args);
711
712 assert!(result.is_err());
713 }
714
715 #[test]
720 fn test_parse_complex_command_line() {
721 let definition = create_test_definition();
722 let parser = CliParser::new(&definition);
723
724 let args = vec![
725 "input.txt".to_string(),
726 "output.txt".to_string(),
727 "--verbose".to_string(),
728 "--count=100".to_string(),
729 ];
730 let result = parser.parse(&args).unwrap();
731
732 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
733 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
734 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
735 assert_eq!(result.get("count"), Some(&"100".to_string()));
736 }
737
738 #[test]
739 fn test_parse_mixed_options_and_arguments() {
740 let definition = create_test_definition();
741 let parser = CliParser::new(&definition);
742
743 let args = vec![
745 "--verbose".to_string(),
746 "input.txt".to_string(),
747 "-c".to_string(),
748 "50".to_string(),
749 "output.txt".to_string(),
750 ];
751 let result = parser.parse(&args).unwrap();
752
753 assert_eq!(result.get("input"), Some(&"input.txt".to_string()));
754 assert_eq!(result.get("output"), Some(&"output.txt".to_string()));
755 assert_eq!(result.get("verbose"), Some(&"true".to_string()));
756 assert_eq!(result.get("count"), Some(&"50".to_string()));
757 }
758}