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