From 3d702c768a536d907e403d4f1f058f056f603353 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Tue, 3 Feb 2026 09:41:15 +0000 Subject: [PATCH] Initial upload with CI/CD workflow --- app/src/parser/mod.rs | 271 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 app/src/parser/mod.rs diff --git a/app/src/parser/mod.rs b/app/src/parser/mod.rs new file mode 100644 index 0000000..a90355e --- /dev/null +++ b/app/src/parser/mod.rs @@ -0,0 +1,271 @@ +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); + } +}