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