pub fn shell_tokenize(input: &str) -> Vec<String> {
let mut tokens = Vec::new();
let mut current = String::new();
let mut chars = input.chars().peekable();
let mut in_single = false;
let mut in_double = false;
let mut parameter_depth: u32 = 0;
while let Some(c) = chars.next() {
match c {
'\'' if !in_double => in_single = !in_single,
'"' if !in_single => in_double = !in_double,
'\\' if !in_single => {
if let Some(next) = chars.next() {
current.push(next);
}
}
'$' if !in_single && chars.peek() == Some(&'{') => {
parameter_depth += 1;
current.push(c);
}
'}' if !in_single && parameter_depth > 0 => {
parameter_depth -= 1;
current.push(c);
}
c if c.is_whitespace() && !in_single && !in_double && parameter_depth == 0 => {
if !current.is_empty() {
tokens.push(std::mem::take(&mut current));
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
tokens.push(current);
}
tokens
}
pub(super) fn quote_aware_token_end(input: &str) -> usize {
let bytes = input.as_bytes();
let len = bytes.len();
let mut i = 0;
let mut in_single = false;
let mut in_double = false;
let mut paren_depth: u32 = 0;
let mut parameter_depth: u32 = 0;
while i < len {
let ch = bytes[i];
match ch {
b'\'' if !in_double => {
in_single = !in_single;
i += 1;
}
b'"' if !in_single => {
in_double = !in_double;
i += 1;
}
b'\\' if !in_single => {
i = (i + 2).min(len);
}
b'(' if !in_single && !in_double => {
paren_depth += 1;
i += 1;
}
b')' if !in_single && !in_double && paren_depth > 0 => {
paren_depth -= 1;
i += 1;
}
b'$' if !in_single && !in_double && bytes.get(i + 1) == Some(&b'{') => {
parameter_depth += 1;
i += 1;
}
b'}' if !in_single && parameter_depth > 0 => {
parameter_depth -= 1;
i += 1;
}
b if b.is_ascii_whitespace()
&& !in_single
&& !in_double
&& paren_depth == 0
&& parameter_depth == 0 =>
{
return i;
}
_ => i += 1,
}
}
len
}
pub(super) fn extract_all_commands(command: &str) -> Vec<String> {
split_on_operators(command)
.into_iter()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect()
}
pub(super) fn split_on_operators(command: &str) -> Vec<&str> {
let mut segments = Vec::new();
let mut start = 0;
let bytes = command.as_bytes();
let len = bytes.len();
let mut i = 0;
let mut in_single_quote = false;
let mut in_double_quote = false;
let mut paren_depth: u32 = 0;
let mut brace_depth: u32 = 0;
while i < len {
let ch = bytes[i];
if in_single_quote {
if ch == b'\'' {
in_single_quote = false;
}
i += 1;
continue;
}
if in_double_quote {
match ch {
b'\\' => i = (i + 2).min(len),
b'"' => {
in_double_quote = false;
i += 1;
}
_ => i += 1,
}
continue;
}
match ch {
b'\\' => {
i = (i + 2).min(len);
}
b'\'' => {
in_single_quote = true;
i += 1;
}
b'"' => {
in_double_quote = true;
i += 1;
}
b'(' => {
paren_depth += 1;
i += 1;
}
b')' => {
paren_depth = paren_depth.saturating_sub(1);
i += 1;
}
b'{' => {
brace_depth += 1;
i += 1;
}
b'}' => {
brace_depth = brace_depth.saturating_sub(1);
i += 1;
}
b'\n' | b'\r' | b';' if paren_depth == 0 && brace_depth == 0 => {
segments.push(&command[start..i]);
i += 1;
start = i;
}
b'&' if paren_depth == 0 && brace_depth == 0 => {
if i + 1 < len && bytes[i + 1] == b'&' {
segments.push(&command[start..i]);
i += 2;
start = i;
} else if (i > 0 && bytes[i - 1] == b'>') || (i + 1 < len && bytes[i + 1] == b'>') {
i += 1;
} else {
segments.push(&command[start..i]);
i += 1;
start = i;
}
}
b'|' if paren_depth == 0 && brace_depth == 0 => {
if i + 1 < len && bytes[i + 1] == b'|' {
segments.push(&command[start..i]);
i += 2;
start = i;
} else if i > 0 && bytes[i - 1] == b'>' {
i += 1;
} else {
segments.push(&command[start..i]);
i += 1;
start = i;
}
}
_ => {
i += 1;
}
}
}
if start < len {
segments.push(&command[start..]);
}
segments
}
pub(super) fn extract_base_from_segment(segment: &str) -> String {
let trimmed = segment.trim();
if trimmed.is_empty() {
return String::new();
}
let cmd_part = skip_env_assignments(trimmed);
if cmd_part.is_empty() {
return String::new();
}
let tokens = shell_tokenize(cmd_part);
let mut token_iter = tokens.iter();
let first_token = match token_iter.next().map(String::as_str) {
Some("{") => token_iter.next().map_or("", String::as_str),
other => other.unwrap_or(""),
};
first_token
.rsplit('/')
.next()
.unwrap_or(first_token)
.to_string()
}
const ENV_SETTING_BUILTINS: &[&str] =
&["export", "unset", "readonly", "local", "declare", "typeset"];
pub(super) fn skip_env_assignments(segment: &str) -> &str {
let mut rest = segment;
loop {
let rest_trimmed = rest.trim_start();
if rest_trimmed.is_empty() {
return rest_trimmed;
}
let end = quote_aware_token_end(rest_trimmed);
if end == 0 {
return rest_trimmed;
}
let raw_token = &rest_trimmed[..end];
let unquoted: String = raw_token
.chars()
.filter(|c| *c != '"' && *c != '\'')
.collect();
let base_token = unquoted.rsplit('/').next().unwrap_or(unquoted.as_str());
if ENV_SETTING_BUILTINS.contains(&base_token) {
rest = &rest_trimmed[end..];
continue;
}
if unquoted.contains('=')
&& !unquoted.starts_with('-')
&& !unquoted.starts_with('/')
&& !unquoted.starts_with('.')
{
rest = &rest_trimmed[end..];
} else {
return rest_trimmed;
}
}
}
pub fn extract_all_commands_pub(command: &str) -> Vec<String> {
extract_all_commands(command)
}
pub fn extract_base_command(command: &str) -> String {
let first_seg = split_on_operators(command)
.into_iter()
.next()
.unwrap_or(command);
extract_base_from_segment(first_seg)
}