use regex::Regex;
use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct ParsedOverrides {
pub env_vars: HashMap<String, String>,
pub options: Vec<String>,
pub args: Vec<String>,
pub raw_input: String,
}
impl ParsedOverrides {
pub fn is_empty(&self) -> bool {
self.env_vars.is_empty() && self.options.is_empty() && self.args.is_empty()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum OverrideOperator {
Replace,
Add,
Remove,
Force,
}
fn parse_operator(option: &str) -> (OverrideOperator, &str) {
if option.starts_with("+--") {
(OverrideOperator::Add, &option[1..])
} else if option.starts_with("---") {
(OverrideOperator::Remove, &option[1..])
} else if option.starts_with("!--") {
(OverrideOperator::Force, &option[1..])
} else {
(OverrideOperator::Replace, option)
}
}
pub struct SmartOverrideParser {
env_regex: Regex,
#[allow(dead_code)]
subcommand: String,
}
impl SmartOverrideParser {
pub fn new(subcommand: impl Into<String>) -> Self {
Self {
env_regex: Regex::new(r#"^([A-Z][A-Z0-9_]*|[A-Z][a-zA-Z0-9]*)=(.+)$"#).unwrap(),
subcommand: subcommand.into(),
}
}
pub fn parse(&self, input: &str) -> ParsedOverrides {
let mut result = ParsedOverrides {
raw_input: input.to_string(),
..Default::default()
};
if let Some(stripped) = input.strip_prefix("-- ") {
result.args = shell_words::split(stripped)
.unwrap_or_else(|_| stripped.split_whitespace().map(String::from).collect());
return result;
}
let parts: Vec<&str> = input.splitn(2, " -- ").collect();
let before_dash = parts[0];
let after_dash = parts.get(1);
if let Some(args_str) = after_dash {
result.args = shell_words::split(args_str).unwrap_or_else(|_| {
args_str.split_whitespace().map(String::from).collect()
});
}
let tokens = shell_words::split(before_dash).unwrap_or_else(|_| {
before_dash.split_whitespace().map(String::from).collect()
});
let mut i = 0;
while i < tokens.len() {
let token = &tokens[i];
if let Some(captures) = self.env_regex.captures(token) {
let key = captures.get(1).unwrap().as_str().to_string();
let value = captures.get(2).unwrap().as_str().to_string();
result.env_vars.insert(key, value);
i += 1;
continue;
}
let (_operator, clean_option) = parse_operator(token);
if clean_option.starts_with("-") {
result.options.push(clean_option.to_string());
if i + 1 < tokens.len() && !tokens[i + 1].starts_with("-") {
let next_token = &tokens[i + 1];
if !self.env_regex.is_match(next_token) {
result.options.push(next_token.clone());
i += 1;
}
}
i += 1;
} else {
result.options.push(token.clone());
i += 1;
}
}
result
}
pub fn parse_vscode_input(
&self,
input: &str,
) -> (ParsedOverrides, HashMap<String, OverrideOperator>) {
let mut operators = HashMap::new();
let mut result = ParsedOverrides {
raw_input: input.to_string(),
..Default::default()
};
let parts: Vec<&str> = input.splitn(2, " -- ").collect();
let before_dash = parts[0];
let after_dash = parts.get(1);
if let Some(args_str) = after_dash {
result.args = shell_words::split(args_str)
.unwrap_or_else(|_| args_str.split_whitespace().map(String::from).collect());
}
let tokens = shell_words::split(before_dash)
.unwrap_or_else(|_| before_dash.split_whitespace().map(String::from).collect());
let mut i = 0;
while i < tokens.len() {
let token = &tokens[i];
if let Some(captures) = self.env_regex.captures(token) {
let key = captures.get(1).unwrap().as_str().to_string();
let value = captures.get(2).unwrap().as_str().to_string();
result.env_vars.insert(key, value);
i += 1;
continue;
}
let (operator, clean_option) = parse_operator(token);
if operator != OverrideOperator::Replace {
operators.insert(clean_option.to_string(), operator.clone());
}
let final_option =
if operator == OverrideOperator::Remove && clean_option.starts_with("--") {
format!("--no-{}", &clean_option[2..])
} else {
clean_option.to_string()
};
result.options.push(final_option);
if i + 1 < tokens.len() && !tokens[i + 1].starts_with("-") {
result.options.push(tokens[i + 1].clone());
i += 2;
} else {
i += 1;
}
}
(result, operators)
}
}
pub fn overrides_to_cli_args(overrides: &ParsedOverrides) -> Vec<String> {
let mut args = vec![];
for (key, value) in &overrides.env_vars {
args.push(format!("--env={key}={value}"));
}
if !overrides.options.is_empty() {
args.push(format!("--options={}", overrides.options.join(" ")));
}
if !overrides.args.is_empty() {
args.push(format!("--args={}", overrides.args.join(" ")));
}
args
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_env() {
let parser = SmartOverrideParser::new("run");
let result = parser.parse("RUST_BACKTRACE=full");
assert_eq!(
result.env_vars.get("RUST_BACKTRACE"),
Some(&"full".to_string())
);
assert!(result.options.is_empty());
assert!(result.args.is_empty());
}
#[test]
fn test_parse_quoted_env() {
let parser = SmartOverrideParser::new("run");
let result = parser.parse(r#"RUST_FLAGS="-A warnings""#);
assert_eq!(
result.env_vars.get("RUST_FLAGS"),
Some(&"-A warnings".to_string())
);
}
#[test]
fn test_parse_options() {
let parser = SmartOverrideParser::new("run");
let result = parser.parse("--release --target aarch64-apple-darwin");
assert_eq!(
result.options,
vec!["--release", "--target", "aarch64-apple-darwin"]
);
assert!(result.env_vars.is_empty());
assert!(result.args.is_empty());
}
#[test]
fn test_parse_with_args() {
let parser = SmartOverrideParser::new("test");
let result = parser.parse("--release -- --exact --nocapture");
assert_eq!(result.options, vec!["--release"]);
assert_eq!(result.args, vec!["--exact", "--nocapture"]);
}
#[test]
fn test_parse_mixed() {
let parser = SmartOverrideParser::new("run");
let result = parser.parse("RUST_LOG=debug --release --features ssr -- --port 3000");
assert_eq!(result.env_vars.get("RUST_LOG"), Some(&"debug".to_string()));
assert_eq!(result.options, vec!["--release", "--features", "ssr"]);
assert_eq!(result.args, vec!["--port", "3000"]);
}
#[test]
fn test_parse_vscode_operators() {
let parser = SmartOverrideParser::new("run");
let (result, operators) = parser.parse_vscode_input("+--features new ---default-features");
assert_eq!(
result.options,
vec!["--features", "new", "--no-default-features"]
);
assert_eq!(operators.get("--features"), Some(&OverrideOperator::Add));
assert_eq!(
operators.get("--default-features"),
Some(&OverrideOperator::Remove)
);
}
#[test]
fn test_framework_options() {
let parser = SmartOverrideParser::new("run");
let result = parser.parse("--platform web --device true");
assert_eq!(
result.options,
vec!["--platform", "web", "--device", "true"]
);
}
}