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