Initial upload with CI/CD workflow
This commit is contained in:
271
app/src/parser/mod.rs
Normal file
271
app/src/parser/mod.rs
Normal file
@@ -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<char>,
|
||||
pub long: Option<String>,
|
||||
pub description: String,
|
||||
pub required: bool,
|
||||
pub takes_value: bool,
|
||||
pub default_value: Option<String>,
|
||||
pub possible_values: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct SubCommand {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub subcommands: Vec<SubCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct CommandInfo {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub version: Option<String>,
|
||||
pub arguments: Vec<Argument>,
|
||||
pub subcommands: Vec<SubCommand>,
|
||||
pub examples: Vec<String>,
|
||||
pub environment_variables: Vec<EnvironmentVar>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct EnvironmentVar {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub default: Option<String>,
|
||||
}
|
||||
|
||||
pub struct HelpParser;
|
||||
|
||||
impl HelpParser {
|
||||
pub async fn parse_command(command_str: &str) -> Result<CommandInfo> {
|
||||
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<String> = 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<String> {
|
||||
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<CommandInfo> {
|
||||
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<String> {
|
||||
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<Argument> {
|
||||
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<SubCommand> {
|
||||
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<String> {
|
||||
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<EnvironmentVar> {
|
||||
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<CommandInfo> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user