intelli_shell/utils/
string.rs1use std::sync::LazyLock;
2
3use regex::Regex;
4use unidecode::unidecode;
5
6pub fn unify_newlines(str: impl AsRef<str>) -> String {
20 static NEW_LINES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\r\n|\r|\n"#).unwrap());
22
23 NEW_LINES.replace_all(str.as_ref(), "\n").to_string()
24}
25
26pub fn remove_newlines(str: impl AsRef<str>) -> String {
45 static NEW_LINE_AND_SPACES: LazyLock<Regex> = LazyLock::new(|| Regex::new(r#"\s*(\\)?(\r\n|\r|\n)\s*"#).unwrap());
50
51 NEW_LINE_AND_SPACES.replace_all(str.as_ref(), " ").to_string()
52}
53
54pub fn flatten_str(s: impl AsRef<str>) -> String {
69 static FLAT_STRING_FORBIDDEN_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9\s-]").unwrap());
71
72 flatten(s, &FLAT_STRING_FORBIDDEN_CHARS)
73}
74
75pub fn flatten_variable_name(variable_name: impl AsRef<str>) -> String {
90 static VARIABLE_FORBIDDEN_CHARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[^a-z0-9\s_:-]").unwrap());
92
93 flatten(variable_name, &VARIABLE_FORBIDDEN_CHARS)
94}
95
96fn flatten(s: impl AsRef<str>, forbidden_chars: &Regex) -> String {
97 let decoded = unidecode(s.as_ref()).to_lowercase();
99
100 let flattened = forbidden_chars.replace_all(&decoded, "");
102
103 static FLATTEN_COLLAPSE_WHITESPACE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s+").unwrap());
105
106 FLATTEN_COLLAPSE_WHITESPACE_REGEX
108 .replace_all(&flattened, " ")
109 .trim()
110 .to_string()
111}
112
113pub fn extract_root_cmd(command: &str) -> Option<String> {
116 fn is_env_var(s: &str) -> bool {
117 let s = s.trim_start_matches("$env:").trim_start_matches("$env.");
119
120 let mut parts = s.splitn(2, '=');
121 let name = parts.next().unwrap_or("");
122
123 if name.is_empty() || parts.next().is_none() {
124 return false;
125 }
126
127 name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == ':')
129 }
130
131 let parts = match shell_words::split(command) {
132 Ok(p) => p,
133 Err(_) => command.split_whitespace().map(|s| s.to_string()).collect(),
134 };
135
136 let mut skip_next_n = 0;
137
138 for (i, part) in parts.iter().enumerate() {
139 if skip_next_n > 0 {
140 skip_next_n -= 1;
141 continue;
142 }
143
144 let p = part.as_str();
145
146 let p = p.strip_suffix(';').unwrap_or(p);
148
149 if is_env_var(p) {
150 continue;
151 }
152
153 match p {
154 "&&" | "||" | ";" | "|" | "sudo" | "doas" | "time" | "env" | "function" | "def" | "def-env" | "export" => {
155 continue;
156 }
157 "let-env" | "let" | "mut" => {
159 if parts.get(i + 2).map(|s| s.as_str()) == Some("=") {
162 skip_next_n = 3;
163 } else if parts.get(i + 1).map(|s| s.as_str()) == Some("=") {
164 skip_next_n = 2;
166 } else {
167 skip_next_n = 2;
169 }
170 continue;
171 }
172 "set" => {
174 let mut skipped = 0;
176 for next_part in parts.iter().skip(i + 1) {
177 let next_part_stripped = next_part.as_str().strip_suffix(';').unwrap_or(next_part.as_str());
178 if matches!(next_part_stripped, ";" | "&&" | "||" | "|") {
179 break;
180 }
181 skipped += 1;
182 if next_part.as_str().ends_with(';') {
183 break;
185 }
186 }
187 skip_next_n = skipped;
188 continue;
189 }
190 _ => {}
191 }
192
193 if (p.starts_with("$env.") || p.starts_with("$env:"))
195 && parts.get(i + 1).map(|s| s.as_str()) == Some("=") {
196 skip_next_n = 2; continue;
198 }
199
200 if p.starts_with('-') {
201 continue;
202 }
203
204 let trimmed = p.strip_suffix("()").unwrap_or(p).to_string();
205 if !trimmed.is_empty() {
206 return Some(trimmed);
207 }
208 }
209 None
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215
216 #[test]
217 fn test_extract_root_cmd() {
218 assert_eq!(extract_root_cmd("VAR1=val1 VAR2=\"val2\" root argument").as_deref(), Some("root"));
219 assert_eq!(extract_root_cmd("VAR4='value 4' && root argument").as_deref(), Some("root"));
220 assert_eq!(extract_root_cmd("VAR5=val\\ 5 ; root argument").as_deref(), Some("root"));
221 assert_eq!(extract_root_cmd("sudo root arg1 arg2").as_deref(), Some("root"));
222 assert_eq!(extract_root_cmd("time sudo root arg1 arg2").as_deref(), Some("root"));
223 assert_eq!(extract_root_cmd("env VAR=1 root arg1").as_deref(), Some("root"));
224 assert_eq!(extract_root_cmd("root arg1").as_deref(), Some("root"));
225 assert_eq!(extract_root_cmd(""), None);
226 assert_eq!(extract_root_cmd("VAR=val"), None);
227 assert_eq!(extract_root_cmd("my_fn() { echo a; }").as_deref(), Some("my_fn"));
228 assert_eq!(extract_root_cmd("function my_fn() { echo a; }").as_deref(), Some("my_fn"));
229 assert_eq!(extract_root_cmd("function my_fn { echo a; }").as_deref(), Some("my_fn"));
230 assert_eq!(extract_root_cmd("ENV={{variable-name:kebab}} function my_fn() { echo a; }").as_deref(), Some("my_fn"));
231
232 assert_eq!(extract_root_cmd("$env:VAR=\"val\"; root argument").as_deref(), Some("root"));
234 assert_eq!(extract_root_cmd("$env:VAR=val; root argument").as_deref(), Some("root"));
235
236 assert_eq!(extract_root_cmd("let-env VAR = \"val\"; root argument").as_deref(), Some("root"));
238 assert_eq!(extract_root_cmd("let VAR = \"val\"; root argument").as_deref(), Some("root"));
239 assert_eq!(extract_root_cmd("$env.VAR = \"val\"; root argument").as_deref(), Some("root"));
240 assert_eq!(extract_root_cmd("def my_fn [] { echo a }").as_deref(), Some("my_fn"));
241 assert_eq!(extract_root_cmd("def-env my_fn [] { echo a }").as_deref(), Some("my_fn"));
242
243 assert_eq!(extract_root_cmd("env VAR=val root argument").as_deref(), Some("root"));
245 assert_eq!(extract_root_cmd("function my_fn; echo a; end").as_deref(), Some("my_fn"));
246 assert_eq!(extract_root_cmd("export VAR=val; root argument").as_deref(), Some("root"));
247 assert_eq!(extract_root_cmd("set -x VAR val; root argument").as_deref(), Some("root"));
248 }
249}