use anyhow::{Result, anyhow, Context}; use regex::Regex; use std::process::Command; use serde::{Serialize, Deserialize}; use std::collections::HashMap; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Argument { pub name: String, pub short: Option, pub long: Option, pub description: String, pub required: bool, pub takes_value: bool, pub default_value: Option, pub possible_values: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SubCommand { pub name: String, pub description: String, pub arguments: Vec, pub subcommands: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct CommandInfo { pub name: String, pub description: String, pub version: Option, pub arguments: Vec, pub subcommands: Vec, pub examples: Vec, pub environment_variables: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct EnvironmentVar { pub name: String, pub description: String, pub default: Option, } pub struct HelpParser; impl HelpParser { pub async fn parse_command(command_str: &str) -> Result { let parts: Vec<&str> = command_str.split_whitespace().collect(); if parts.is_empty() { return Err(anyhow!("Empty command provided")); } let executable = parts[0]; let args: Vec = parts[1..].iter().map(|s| (*s).to_string()).collect(); let help_output = Self::fetch_help_output(executable, &args)?; Self::parse_help_output(executable, &help_output) } fn fetch_help_output(executable: &str, extra_args: &[String]) -> Result { let mut cmd = Command::new(executable); cmd.arg("--help"); for arg in extra_args { cmd.arg(arg); } let output = cmd.output() .with_context(|| format!("Failed to execute '{} --help'", executable))?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); return Err(anyhow!("Command failed: {}", stderr)); } let stdout = String::from_utf8_lossy(&output.stdout); Ok(stdout.to_string()) } fn parse_help_output(name: &str, help_text: &str) -> Result { let mut info = CommandInfo { name: name.to_string(), description: String::new(), version: None, arguments: Vec::new(), subcommands: Vec::new(), examples: Vec::new(), environment_variables: Vec::new(), }; info.description = Self::extract_description(help_text); info.version = Self::extract_version(help_text); info.arguments = Self::extract_arguments(help_text); info.subcommands = Self::extract_subcommands(help_text); info.examples = Self::extract_examples(help_text); info.environment_variables = Self::extract_env_vars(help_text); Ok(info) } fn extract_description(help_text: &str) -> String { let re = Regex::new(r"(?s)^(.+?)(?:\n\n|\n[A-Z])").unwrap(); if let Some(caps) = re.captures(help_text) { if let Some(m) = caps.get(1) { return m.as_str().trim().to_string(); } } String::new() } fn extract_version(help_text: &str) -> Option { let re = Regex::new(r"(?:version|Version)[:\s]+([^\s\n]+)").unwrap(); re.captures(help_text)?.get(1).map(|m| m.as_str().to_string()) } fn extract_arguments(help_text: &str) -> Vec { let mut arguments = Vec::new(); let arg_pattern = Regex::new( r"(?m)^\s*(?:(-([a-zA-Z]),?\s+)?(--([a-zA-Z0-9_-]+))?)\s*(?:<([^>]+)>)?\s*(?:\[[^\]]+\])?\s*(.*)$" ).unwrap(); let desc_pattern = Regex::new(r"(?s)\s+(.+?)(?:\n\s{2,}|\n\n|$)").unwrap(); for line in help_text.lines() { if line.starts_with('-') || line.starts_with("--") { let mut arg = Argument { name: String::new(), short: None, long: None, description: String::new(), required: false, takes_value: false, default_value: None, possible_values: Vec::new(), }; let short_re = Regex::new(r"-([a-zA-Z]),?\s*").unwrap(); let long_re = Regex::new(r"--([a-zA-Z][a-zA-Z0-9_-]*)").unwrap(); let required_re = Regex::new(r"(?:required|needed)").unwrap(); let default_re = Regex::new(r"(?:default|Default)[:\s]+['"]?([^'",\n]+)['"]?").unwrap(); let values_re = Regex::new(r"(?:options|choices|values)[:\s]+\[?([^\]]+)\]?").unwrap(); if let Some(caps) = short_re.captures(line) { arg.short = caps.get(1).map(|c| c.as_str().chars().next().unwrap()); } if let Some(caps) = long_re.captures(line) { arg.long = caps.get(1).map(|c| c.as_str().to_string()); } if let Some(long) = &arg.long { arg.name = long.clone(); } else if let Some(short) = arg.short { arg.name = short.to_string(); } arg.description = line .splitn(2, " ") .nth(1) .map(|s| s.trim().to_string()) .unwrap_or_default(); arg.required = required_re.is_match(line); arg.takes_value = line.contains('<') || line.contains("ARG") || line.contains("FILE"); arg.default_value = default_re.captures(line).and_then(|c| c.get(1).map(|m| m.as_str().to_string())); arg.possible_values = values_re.captures(line) .and_then(|c| c.get(1).map(|m| m.as_str().split(',').map(|s| s.trim().to_string()).collect())) .unwrap_or_default(); if !arg.name.is_empty() { arguments.push(arg); } } } arguments } fn extract_subcommands(help_text: &str) -> Vec { let mut subcommands = Vec::new(); let re = Regex::new(r"(?m)^ ([a-z][a-z0-9_-]*)\s+(.+?)(?:\n\s{4,}|\n\n|$)").unwrap(); for caps in re.captures_iter(help_text) { let subcmd = SubCommand { name: caps[1].to_string(), description: caps[2].trim().to_string(), arguments: Vec::new(), subcommands: Vec::new(), }; subcommands.push(subcmd); } subcommands } fn extract_examples(help_text: &str) -> Vec { let examples_section_re = Regex::new(r"(?i)(?s)(?:examples?|usage)[:\s]*\n(.+?)(?:\n\n|\n[A-Z][a-z]+:|$)").unwrap(); let mut examples = Vec::new(); if let Some(caps) = examples_section_re.captures(help_text) { if let Some(section) = caps.get(1) { let example_re = Regex::new(r"(?m)^\$ (.+)$").unwrap(); for cap in example_re.captures_iter(section.as_str()) { examples.push(cap[1].to_string()); } } } examples } fn extract_env_vars(help_text: &str) -> Vec { let mut env_vars = Vec::new(); let re = Regex::new(r"(?mi)^(?:ENVIRONMENT|ENVIRONMENT VARIABLE)[:\s]*\n(.+?)(?:\n\n|\n[A-Z])").unwrap(); if let Some(caps) = re.captures(help_text) { if let Some(section) = caps.get(1) { let var_re = Regex::new(r"\$([A-Z_][A-Z0-9_]*)\s+(.+)").unwrap(); for cap in var_re.captures_iter(section.as_str()) { env_vars.push(EnvironmentVar { name: cap[1].to_string(), description: cap[2].trim().to_string(), default: None, }); } } } env_vars } pub async fn parse_default_commands() -> Result { Self::parse_command("git").await } } #[cfg(test)] mod tests { use super::*; #[test] fn test_extract_description() { let help = "A test command description\n\nUsage:\n test [OPTIONS]"; let desc = HelpParser::extract_description(help); assert_eq!(desc, "A test command description"); } #[test] fn test_extract_version() { let help = "Some tool\nVersion: 1.2.3\n\nOptions:"; let version = HelpParser::extract_version(help); assert_eq!(version, Some("1.2.3".to_string())); } #[test] fn test_extract_arguments_basic() { let help = r#"Usage: test [OPTIONS] Options: -v, --verbose Enable verbose output -o, --output FILE Write to file"#; let args = HelpParser::extract_arguments(help); assert!(args.len() >= 2); } }