1use crate::error::{ParseError, Result};
52use crate::parser::cli_parser::CliParser;
53use crate::registry::CommandRegistry;
54use std::collections::HashMap;
55
56pub struct ReplParser<'a> {
83 registry: &'a CommandRegistry,
85}
86
87#[derive(Debug, Clone, PartialEq)]
115pub struct ParsedCommand {
116 pub command_name: String,
118
119 pub arguments: HashMap<String, String>,
121}
122
123impl<'a> ReplParser<'a> {
124 pub fn new(registry: &'a CommandRegistry) -> Self {
140 Self { registry }
141 }
142
143 pub fn parse_line(&self, line: &str) -> Result<ParsedCommand> {
182 let tokens = self.tokenize(line)?;
184
185 if tokens.is_empty() {
186 return Err(ParseError::InvalidSyntax {
187 details: "Empty command line".to_string(),
188 hint: Some("Type a command or 'help' for available commands".to_string()),
189 }
190 .into());
191 }
192
193 let input_name = &tokens[0];
195
196 let command_name = self
198 .registry
199 .resolve_name(input_name)
200 .ok_or_else(|| {
201 let available: Vec<String> = self
203 .registry
204 .list_commands()
205 .iter()
206 .flat_map(|cmd| {
207 let mut names = vec![cmd.name.clone()];
208 names.extend(cmd.aliases.clone());
209 names
210 })
211 .collect();
212
213 ParseError::unknown_command_with_suggestions(input_name, &available)
214 })?
215 .to_string();
216
217 let definition = self
219 .registry
220 .get_definition(&command_name)
221 .expect("Command definition must exist after resolution");
222
223 let remaining_args: Vec<String> = tokens[1..].to_vec();
225 let cli_parser = CliParser::new(definition);
226 let arguments = cli_parser.parse(&remaining_args)?;
227
228 Ok(ParsedCommand {
229 command_name,
230 arguments,
231 })
232 }
233
234 pub fn tokenize(&self, line: &str) -> Result<Vec<String>> {
270 let mut tokens = Vec::new();
271 let mut current_token = String::new();
272 let mut in_quotes = false;
273 let mut quote_char = ' ';
274 let mut chars = line.chars().peekable();
275
276 while let Some(ch) = chars.next() {
277 match ch {
278 '"' | '\'' => {
280 if in_quotes && ch == quote_char {
281 in_quotes = false;
283 quote_char = ' ';
284 } else if !in_quotes {
285 in_quotes = true;
287 quote_char = ch;
288 } else {
289 current_token.push(ch);
291 }
292 }
293
294 ' ' | '\t' => {
296 if in_quotes {
297 current_token.push(ch);
298 } else if !current_token.is_empty() {
299 tokens.push(current_token.clone());
300 current_token.clear();
301 }
302 }
303
304 '\\' => {
306 if let Some(&next_ch) = chars.peek() {
307 if in_quotes && (next_ch == quote_char || next_ch == '\\') {
308 chars.next(); current_token.push(next_ch);
310 } else {
311 current_token.push(ch);
312 }
313 } else {
314 current_token.push(ch);
315 }
316 }
317
318 _ => {
320 current_token.push(ch);
321 }
322 }
323 }
324
325 if in_quotes {
327 return Err(ParseError::InvalidSyntax {
328 details: format!("Unbalanced quote: {}", quote_char),
329 hint: Some("Make sure all quotes are properly closed".to_string()),
330 }
331 .into());
332 }
333
334 if !current_token.is_empty() {
336 tokens.push(current_token);
337 }
338
339 Ok(tokens)
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use crate::config::schema::{
347 ArgumentDefinition, ArgumentType, CommandDefinition, OptionDefinition,
348 };
349 use crate::context::ExecutionContext;
350 use crate::executor::CommandHandler;
351
352 struct TestHandler;
354
355 impl CommandHandler for TestHandler {
356 fn execute(
357 &self,
358 _context: &mut dyn ExecutionContext,
359 _args: &HashMap<String, String>,
360 ) -> crate::error::Result<()> {
361 Ok(())
362 }
363 }
364
365 fn create_test_registry() -> CommandRegistry {
367 let mut registry = CommandRegistry::new();
368
369 let hello_def = CommandDefinition {
371 name: "hello".to_string(),
372 aliases: vec!["hi".to_string(), "greet".to_string()],
373 description: "Say hello".to_string(),
374 required: false,
375 arguments: vec![ArgumentDefinition {
376 name: "name".to_string(),
377 arg_type: ArgumentType::String,
378 required: false,
379 description: "Name to greet".to_string(),
380 validation: vec![],
381 }],
382 options: vec![OptionDefinition {
383 name: "loud".to_string(),
384 short: Some("l".to_string()),
385 long: Some("loud".to_string()),
386 option_type: ArgumentType::Bool,
387 required: false,
388 default: Some("false".to_string()),
389 description: "Loud greeting".to_string(),
390 choices: vec![],
391 }],
392 implementation: "hello_handler".to_string(),
393 };
394
395 registry.register(hello_def, Box::new(TestHandler)).unwrap();
396
397 let process_def = CommandDefinition {
399 name: "process".to_string(),
400 aliases: vec!["proc".to_string()],
401 description: "Process files".to_string(),
402 required: false,
403 arguments: vec![
404 ArgumentDefinition {
405 name: "input".to_string(),
406 arg_type: ArgumentType::Path,
407 required: true,
408 description: "Input file".to_string(),
409 validation: vec![],
410 },
411 ArgumentDefinition {
412 name: "output".to_string(),
413 arg_type: ArgumentType::Path,
414 required: false,
415 description: "Output file".to_string(),
416 validation: vec![],
417 },
418 ],
419 options: vec![OptionDefinition {
420 name: "verbose".to_string(),
421 short: Some("v".to_string()),
422 long: Some("verbose".to_string()),
423 option_type: ArgumentType::Bool,
424 required: false,
425 default: Some("false".to_string()),
426 description: "Verbose output".to_string(),
427 choices: vec![],
428 }],
429 implementation: "process_handler".to_string(),
430 };
431
432 registry
433 .register(process_def, Box::new(TestHandler))
434 .unwrap();
435
436 registry
437 }
438
439 #[test]
444 fn test_tokenize_simple() {
445 let registry = create_test_registry();
446 let parser = ReplParser::new(®istry);
447
448 let tokens = parser.tokenize("hello world").unwrap();
449 assert_eq!(tokens, vec!["hello", "world"]);
450 }
451
452 #[test]
453 fn test_tokenize_multiple_spaces() {
454 let registry = create_test_registry();
455 let parser = ReplParser::new(®istry);
456
457 let tokens = parser.tokenize("hello world test").unwrap();
458 assert_eq!(tokens, vec!["hello", "world", "test"]);
459 }
460
461 #[test]
462 fn test_tokenize_double_quotes() {
463 let registry = create_test_registry();
464 let parser = ReplParser::new(®istry);
465
466 let tokens = parser.tokenize(r#"hello "world test""#).unwrap();
467 assert_eq!(tokens, vec!["hello", "world test"]);
468 }
469
470 #[test]
471 fn test_tokenize_single_quotes() {
472 let registry = create_test_registry();
473 let parser = ReplParser::new(®istry);
474
475 let tokens = parser.tokenize("hello 'world test'").unwrap();
476 assert_eq!(tokens, vec!["hello", "world test"]);
477 }
478
479 #[test]
480 fn test_tokenize_escaped_quotes() {
481 let registry = create_test_registry();
482 let parser = ReplParser::new(®istry);
483
484 let tokens = parser.tokenize(r#"hello "say \"hi\"""#).unwrap();
485 assert_eq!(tokens, vec!["hello", r#"say "hi""#]);
486 }
487
488 #[test]
489 fn test_tokenize_unbalanced_quotes() {
490 let registry = create_test_registry();
491 let parser = ReplParser::new(®istry);
492
493 let result = parser.tokenize(r#"hello "world"#);
494 assert!(result.is_err());
495 }
496
497 #[test]
498 fn test_tokenize_empty_line() {
499 let registry = create_test_registry();
500 let parser = ReplParser::new(®istry);
501
502 let tokens = parser.tokenize("").unwrap();
503 assert!(tokens.is_empty());
504 }
505
506 #[test]
507 fn test_tokenize_only_spaces() {
508 let registry = create_test_registry();
509 let parser = ReplParser::new(®istry);
510
511 let tokens = parser.tokenize(" ").unwrap();
512 assert!(tokens.is_empty());
513 }
514
515 #[test]
520 fn test_parse_command_by_name() {
521 let registry = create_test_registry();
522 let parser = ReplParser::new(®istry);
523
524 let parsed = parser.parse_line("hello").unwrap();
525 assert_eq!(parsed.command_name, "hello");
526 }
527
528 #[test]
529 fn test_parse_command_by_alias() {
530 let registry = create_test_registry();
531 let parser = ReplParser::new(®istry);
532
533 let parsed = parser.parse_line("hi").unwrap();
534 assert_eq!(parsed.command_name, "hello");
535
536 let parsed = parser.parse_line("greet").unwrap();
537 assert_eq!(parsed.command_name, "hello");
538 }
539
540 #[test]
541 fn test_parse_unknown_command() {
542 let registry = create_test_registry();
543 let parser = ReplParser::new(®istry);
544
545 let result = parser.parse_line("unknown");
546 assert!(result.is_err());
547
548 match result.unwrap_err() {
549 crate::error::DynamicCliError::Parse(ParseError::UnknownCommand {
550 command, ..
551 }) => {
552 assert_eq!(command, "unknown");
553 }
554 other => panic!("Expected UnknownCommand error, got {:?}", other),
555 }
556 }
557
558 #[test]
559 fn test_parse_empty_line() {
560 let registry = create_test_registry();
561 let parser = ReplParser::new(®istry);
562
563 let result = parser.parse_line("");
564 assert!(result.is_err());
565 }
566
567 #[test]
572 fn test_parse_command_with_arguments() {
573 let registry = create_test_registry();
574 let parser = ReplParser::new(®istry);
575
576 let parsed = parser.parse_line("hello Alice").unwrap();
577 assert_eq!(parsed.command_name, "hello");
578 assert_eq!(parsed.arguments.get("name"), Some(&"Alice".to_string()));
579 }
580
581 #[test]
582 fn test_parse_command_with_options() {
583 let registry = create_test_registry();
584 let parser = ReplParser::new(®istry);
585
586 let parsed = parser.parse_line("hello --loud").unwrap();
587 assert_eq!(parsed.command_name, "hello");
588 assert_eq!(parsed.arguments.get("loud"), Some(&"true".to_string()));
589 }
590
591 #[test]
592 fn test_parse_command_with_short_option() {
593 let registry = create_test_registry();
594 let parser = ReplParser::new(®istry);
595
596 let parsed = parser.parse_line("hello -l").unwrap();
597 assert_eq!(parsed.command_name, "hello");
598 assert_eq!(parsed.arguments.get("loud"), Some(&"true".to_string()));
599 }
600
601 #[test]
602 fn test_parse_command_with_multiple_arguments_and_options() {
603 let registry = create_test_registry();
604 let parser = ReplParser::new(®istry);
605
606 let parsed = parser
607 .parse_line("process input.txt output.txt --verbose")
608 .unwrap();
609 assert_eq!(parsed.command_name, "process");
610 assert_eq!(
611 parsed.arguments.get("input"),
612 Some(&"input.txt".to_string())
613 );
614 assert_eq!(
615 parsed.arguments.get("output"),
616 Some(&"output.txt".to_string())
617 );
618 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
619 }
620
621 #[test]
622 fn test_parse_alias_with_arguments() {
623 let registry = create_test_registry();
624 let parser = ReplParser::new(®istry);
625
626 let parsed = parser.parse_line("proc input.txt -v").unwrap();
627 assert_eq!(parsed.command_name, "process");
628 assert_eq!(
629 parsed.arguments.get("input"),
630 Some(&"input.txt".to_string())
631 );
632 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
633 }
634
635 #[test]
640 fn test_parse_quoted_arguments() {
641 let registry = create_test_registry();
642 let parser = ReplParser::new(®istry);
643
644 let parsed = parser.parse_line(r#"hello "Alice Bob""#).unwrap();
645 assert_eq!(parsed.command_name, "hello");
646 assert_eq!(parsed.arguments.get("name"), Some(&"Alice Bob".to_string()));
647 }
648
649 #[test]
650 fn test_parse_quoted_paths() {
651 let registry = create_test_registry();
652 let parser = ReplParser::new(®istry);
653
654 let parsed = parser
655 .parse_line(r#"process "/path/with spaces/file.txt""#)
656 .unwrap();
657 assert_eq!(parsed.command_name, "process");
658 assert_eq!(
659 parsed.arguments.get("input"),
660 Some(&"/path/with spaces/file.txt".to_string())
661 );
662 }
663
664 #[test]
669 fn test_parse_complex_command_line() {
670 let registry = create_test_registry();
671 let parser = ReplParser::new(®istry);
672
673 let parsed = parser
674 .parse_line(r#"proc "input file.txt" "output file.txt" -v"#)
675 .unwrap();
676
677 assert_eq!(parsed.command_name, "process");
678 assert_eq!(
679 parsed.arguments.get("input"),
680 Some(&"input file.txt".to_string())
681 );
682 assert_eq!(
683 parsed.arguments.get("output"),
684 Some(&"output file.txt".to_string())
685 );
686 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
687 }
688
689 #[test]
690 fn test_parsed_command_debug() {
691 let mut args = HashMap::new();
692 args.insert("test".to_string(), "value".to_string());
693
694 let parsed = ParsedCommand {
695 command_name: "test".to_string(),
696 arguments: args,
697 };
698
699 let debug_str = format!("{:?}", parsed);
701 assert!(debug_str.contains("test"));
702 }
703
704 #[test]
705 fn test_parsed_command_clone() {
706 let mut args = HashMap::new();
707 args.insert("test".to_string(), "value".to_string());
708
709 let parsed = ParsedCommand {
710 command_name: "test".to_string(),
711 arguments: args,
712 };
713
714 let cloned = parsed.clone();
715 assert_eq!(parsed, cloned);
716 }
717}