use std::sync::LazyLock;
use regex::Regex;
use unidecode::unidecode;
pub fn unify_newlines(str: impl AsRef<str>) -> String {
static NEW_LINES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\r\n|\r|\n"#).unwrap());
NEW_LINES.replace_all(str.as_ref(), "\n").to_string()
}
pub fn remove_newlines(str: impl AsRef<str>) -> String {
static NEW_LINE_AND_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\s*(\\)?(\r\n|\r|\n)\s*"#).unwrap());
NEW_LINE_AND_SPACES.replace_all(str.as_ref(), " ").to_string()
}
pub fn flatten_str(s: impl AsRef<str>) -> String {
static FLAT_STRING_FORBIDDEN_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9\s-]").unwrap());
flatten(s, &FLAT_STRING_FORBIDDEN_CHARS)
}
pub fn flatten_variable_name(variable_name: impl AsRef<str>) -> String {
static VARIABLE_FORBIDDEN_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9\s_:-]").unwrap());
flatten(variable_name, &VARIABLE_FORBIDDEN_CHARS)
}
fn flatten(s: impl AsRef<str>, forbidden_chars: &Regex) -> String {
let decoded = unidecode(s.as_ref()).to_lowercase();
let flattened = forbidden_chars.replace_all(&decoded, "");
static FLATTEN_COLLAPSE_WHITESPACE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s+").unwrap());
FLATTEN_COLLAPSE_WHITESPACE_REGEX
.replace_all(&flattened, " ")
.trim()
.to_string()
}
pub fn extract_root_cmd(command: &str) -> Option<String> {
fn is_env_var(s: &str) -> bool {
let s = s.trim_start_matches("$env:").trim_start_matches("$env.");
let mut parts = s.splitn(2, '=');
let name = parts.next().unwrap_or("");
if name.is_empty() || parts.next().is_none() {
return false;
}
name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == ':')
}
let parts = match shell_words::split(command) {
Ok(p) => p,
Err(_) => command.split_whitespace().map(|s| s.to_string()).collect(),
};
let mut skip_next_n = 0;
for (i, part) in parts.iter().enumerate() {
if skip_next_n > 0 {
skip_next_n -= 1;
continue;
}
let p = part.as_str();
let p = p.strip_suffix(';').unwrap_or(p);
if is_env_var(p) {
continue;
}
match p {
"&&" | "||" | ";" | "|" | "sudo" | "doas" | "time" | "env" | "function" | "def" | "def-env" | "export" => {
continue;
}
"let-env" | "let" | "mut" => {
if parts.get(i + 2).map(|s| s.as_str()) == Some("=") {
skip_next_n = 3;
} else if parts.get(i + 1).map(|s| s.as_str()) == Some("=") {
skip_next_n = 2;
} else {
skip_next_n = 2;
}
continue;
}
"set" => {
let mut skipped = 0;
for next_part in parts.iter().skip(i + 1) {
let next_part_stripped = next_part.as_str().strip_suffix(';').unwrap_or(next_part.as_str());
if matches!(next_part_stripped, ";" | "&&" | "||" | "|") {
break;
}
skipped += 1;
if next_part.as_str().ends_with(';') {
break;
}
}
skip_next_n = skipped;
continue;
}
_ => {}
}
if (p.starts_with("$env.") || p.starts_with("$env:"))
&& parts.get(i + 1).map(|s| s.as_str()) == Some("=") {
skip_next_n = 2; continue;
}
if p.starts_with('-') {
continue;
}
let trimmed = p.strip_suffix("()").unwrap_or(p).to_string();
if !trimmed.is_empty() {
return Some(trimmed);
}
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_root_cmd() {
assert_eq!(extract_root_cmd("VAR1=val1 VAR2=\"val2\" root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("VAR4='value 4' && root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("VAR5=val\\ 5 ; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("sudo root arg1 arg2").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("time sudo root arg1 arg2").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("env VAR=1 root arg1").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("root arg1").as_deref(), Some("root"));
assert_eq!(extract_root_cmd(""), None);
assert_eq!(extract_root_cmd("VAR=val"), None);
assert_eq!(extract_root_cmd("my_fn() { echo a; }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("function my_fn() { echo a; }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("function my_fn { echo a; }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("ENV={{variable-name:kebab}} function my_fn() { echo a; }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("$env:VAR=\"val\"; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("$env:VAR=val; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("let-env VAR = \"val\"; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("let VAR = \"val\"; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("$env.VAR = \"val\"; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("def my_fn [] { echo a }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("def-env my_fn [] { echo a }").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("env VAR=val root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("function my_fn; echo a; end").as_deref(), Some("my_fn"));
assert_eq!(extract_root_cmd("export VAR=val; root argument").as_deref(), Some("root"));
assert_eq!(extract_root_cmd("set -x VAR val; root argument").as_deref(), Some("root"));
}
}