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 secure: false,
382 }],
383 options: vec![OptionDefinition {
384 name: "loud".to_string(),
385 short: Some("l".to_string()),
386 long: Some("loud".to_string()),
387 option_type: ArgumentType::Bool,
388 required: false,
389 default: Some("false".to_string()),
390 description: "Loud greeting".to_string(),
391 choices: vec![],
392 }],
393 implementation: "hello_handler".to_string(),
394 };
395
396 registry.register(hello_def, Box::new(TestHandler)).unwrap();
397
398 let process_def = CommandDefinition {
400 name: "process".to_string(),
401 aliases: vec!["proc".to_string()],
402 description: "Process files".to_string(),
403 required: false,
404 arguments: vec![
405 ArgumentDefinition {
406 name: "input".to_string(),
407 arg_type: ArgumentType::Path,
408 required: true,
409 description: "Input file".to_string(),
410 validation: vec![],
411 secure: false,
412 },
413 ArgumentDefinition {
414 name: "output".to_string(),
415 arg_type: ArgumentType::Path,
416 required: false,
417 description: "Output file".to_string(),
418 validation: vec![],
419 secure: false,
420 },
421 ],
422 options: vec![OptionDefinition {
423 name: "verbose".to_string(),
424 short: Some("v".to_string()),
425 long: Some("verbose".to_string()),
426 option_type: ArgumentType::Bool,
427 required: false,
428 default: Some("false".to_string()),
429 description: "Verbose output".to_string(),
430 choices: vec![],
431 }],
432 implementation: "process_handler".to_string(),
433 };
434
435 registry
436 .register(process_def, Box::new(TestHandler))
437 .unwrap();
438
439 registry
440 }
441
442 #[test]
447 fn test_tokenize_simple() {
448 let registry = create_test_registry();
449 let parser = ReplParser::new(®istry);
450
451 let tokens = parser.tokenize("hello world").unwrap();
452 assert_eq!(tokens, vec!["hello", "world"]);
453 }
454
455 #[test]
456 fn test_tokenize_multiple_spaces() {
457 let registry = create_test_registry();
458 let parser = ReplParser::new(®istry);
459
460 let tokens = parser.tokenize("hello world test").unwrap();
461 assert_eq!(tokens, vec!["hello", "world", "test"]);
462 }
463
464 #[test]
465 fn test_tokenize_double_quotes() {
466 let registry = create_test_registry();
467 let parser = ReplParser::new(®istry);
468
469 let tokens = parser.tokenize(r#"hello "world test""#).unwrap();
470 assert_eq!(tokens, vec!["hello", "world test"]);
471 }
472
473 #[test]
474 fn test_tokenize_single_quotes() {
475 let registry = create_test_registry();
476 let parser = ReplParser::new(®istry);
477
478 let tokens = parser.tokenize("hello 'world test'").unwrap();
479 assert_eq!(tokens, vec!["hello", "world test"]);
480 }
481
482 #[test]
483 fn test_tokenize_escaped_quotes() {
484 let registry = create_test_registry();
485 let parser = ReplParser::new(®istry);
486
487 let tokens = parser.tokenize(r#"hello "say \"hi\"""#).unwrap();
488 assert_eq!(tokens, vec!["hello", r#"say "hi""#]);
489 }
490
491 #[test]
492 fn test_tokenize_unbalanced_quotes() {
493 let registry = create_test_registry();
494 let parser = ReplParser::new(®istry);
495
496 let result = parser.tokenize(r#"hello "world"#);
497 assert!(result.is_err());
498 }
499
500 #[test]
501 fn test_tokenize_empty_line() {
502 let registry = create_test_registry();
503 let parser = ReplParser::new(®istry);
504
505 let tokens = parser.tokenize("").unwrap();
506 assert!(tokens.is_empty());
507 }
508
509 #[test]
510 fn test_tokenize_only_spaces() {
511 let registry = create_test_registry();
512 let parser = ReplParser::new(®istry);
513
514 let tokens = parser.tokenize(" ").unwrap();
515 assert!(tokens.is_empty());
516 }
517
518 #[test]
523 fn test_parse_command_by_name() {
524 let registry = create_test_registry();
525 let parser = ReplParser::new(®istry);
526
527 let parsed = parser.parse_line("hello").unwrap();
528 assert_eq!(parsed.command_name, "hello");
529 }
530
531 #[test]
532 fn test_parse_command_by_alias() {
533 let registry = create_test_registry();
534 let parser = ReplParser::new(®istry);
535
536 let parsed = parser.parse_line("hi").unwrap();
537 assert_eq!(parsed.command_name, "hello");
538
539 let parsed = parser.parse_line("greet").unwrap();
540 assert_eq!(parsed.command_name, "hello");
541 }
542
543 #[test]
544 fn test_parse_unknown_command() {
545 let registry = create_test_registry();
546 let parser = ReplParser::new(®istry);
547
548 let result = parser.parse_line("unknown");
549 assert!(result.is_err());
550
551 match result.unwrap_err() {
552 crate::error::DynamicCliError::Parse(ParseError::UnknownCommand {
553 command, ..
554 }) => {
555 assert_eq!(command, "unknown");
556 }
557 other => panic!("Expected UnknownCommand error, got {:?}", other),
558 }
559 }
560
561 #[test]
562 fn test_parse_empty_line() {
563 let registry = create_test_registry();
564 let parser = ReplParser::new(®istry);
565
566 let result = parser.parse_line("");
567 assert!(result.is_err());
568 }
569
570 #[test]
575 fn test_parse_command_with_arguments() {
576 let registry = create_test_registry();
577 let parser = ReplParser::new(®istry);
578
579 let parsed = parser.parse_line("hello Alice").unwrap();
580 assert_eq!(parsed.command_name, "hello");
581 assert_eq!(parsed.arguments.get("name"), Some(&"Alice".to_string()));
582 }
583
584 #[test]
585 fn test_parse_command_with_options() {
586 let registry = create_test_registry();
587 let parser = ReplParser::new(®istry);
588
589 let parsed = parser.parse_line("hello --loud").unwrap();
590 assert_eq!(parsed.command_name, "hello");
591 assert_eq!(parsed.arguments.get("loud"), Some(&"true".to_string()));
592 }
593
594 #[test]
595 fn test_parse_command_with_short_option() {
596 let registry = create_test_registry();
597 let parser = ReplParser::new(®istry);
598
599 let parsed = parser.parse_line("hello -l").unwrap();
600 assert_eq!(parsed.command_name, "hello");
601 assert_eq!(parsed.arguments.get("loud"), Some(&"true".to_string()));
602 }
603
604 #[test]
605 fn test_parse_command_with_multiple_arguments_and_options() {
606 let registry = create_test_registry();
607 let parser = ReplParser::new(®istry);
608
609 let parsed = parser
610 .parse_line("process input.txt output.txt --verbose")
611 .unwrap();
612 assert_eq!(parsed.command_name, "process");
613 assert_eq!(
614 parsed.arguments.get("input"),
615 Some(&"input.txt".to_string())
616 );
617 assert_eq!(
618 parsed.arguments.get("output"),
619 Some(&"output.txt".to_string())
620 );
621 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
622 }
623
624 #[test]
625 fn test_parse_alias_with_arguments() {
626 let registry = create_test_registry();
627 let parser = ReplParser::new(®istry);
628
629 let parsed = parser.parse_line("proc input.txt -v").unwrap();
630 assert_eq!(parsed.command_name, "process");
631 assert_eq!(
632 parsed.arguments.get("input"),
633 Some(&"input.txt".to_string())
634 );
635 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
636 }
637
638 #[test]
643 fn test_parse_quoted_arguments() {
644 let registry = create_test_registry();
645 let parser = ReplParser::new(®istry);
646
647 let parsed = parser.parse_line(r#"hello "Alice Bob""#).unwrap();
648 assert_eq!(parsed.command_name, "hello");
649 assert_eq!(parsed.arguments.get("name"), Some(&"Alice Bob".to_string()));
650 }
651
652 #[test]
653 fn test_parse_quoted_paths() {
654 let registry = create_test_registry();
655 let parser = ReplParser::new(®istry);
656
657 let parsed = parser
658 .parse_line(r#"process "/path/with spaces/file.txt""#)
659 .unwrap();
660 assert_eq!(parsed.command_name, "process");
661 assert_eq!(
662 parsed.arguments.get("input"),
663 Some(&"/path/with spaces/file.txt".to_string())
664 );
665 }
666
667 #[test]
672 fn test_parse_complex_command_line() {
673 let registry = create_test_registry();
674 let parser = ReplParser::new(®istry);
675
676 let parsed = parser
677 .parse_line(r#"proc "input file.txt" "output file.txt" -v"#)
678 .unwrap();
679
680 assert_eq!(parsed.command_name, "process");
681 assert_eq!(
682 parsed.arguments.get("input"),
683 Some(&"input file.txt".to_string())
684 );
685 assert_eq!(
686 parsed.arguments.get("output"),
687 Some(&"output file.txt".to_string())
688 );
689 assert_eq!(parsed.arguments.get("verbose"), Some(&"true".to_string()));
690 }
691
692 #[test]
693 fn test_parsed_command_debug() {
694 let mut args = HashMap::new();
695 args.insert("test".to_string(), "value".to_string());
696
697 let parsed = ParsedCommand {
698 command_name: "test".to_string(),
699 arguments: args,
700 };
701
702 let debug_str = format!("{:?}", parsed);
704 assert!(debug_str.contains("test"));
705 }
706
707 #[test]
708 fn test_parsed_command_clone() {
709 let mut args = HashMap::new();
710 args.insert("test".to_string(), "value".to_string());
711
712 let parsed = ParsedCommand {
713 command_name: "test".to_string(),
714 arguments: args,
715 };
716
717 let cloned = parsed.clone();
718 assert_eq!(parsed, cloned);
719 }
720}