pub(crate) fn shellish_token_segments(line: &str) -> Vec<Vec<&str>> {
let mut segments = Vec::new();
let mut segment = Vec::new();
let mut token_start = None;
let mut in_single_quote = false;
let mut in_double_quote = false;
for (index, character) in line.char_indices() {
if character == '\'' && !in_double_quote {
if token_start.is_none() {
token_start = Some(index);
}
in_single_quote = !in_single_quote;
} else if character == '"' && !in_single_quote {
if token_start.is_none() {
token_start = Some(index);
}
in_double_quote = !in_double_quote;
} else if !in_single_quote && !in_double_quote && character.is_whitespace() {
push_shellish_token(line, &mut token_start, index, &mut segment);
} else if !in_single_quote && !in_double_quote && is_shell_command_separator(character) {
push_shellish_token(line, &mut token_start, index, &mut segment);
if !segment.is_empty() {
segments.push(std::mem::take(&mut segment));
}
} else if token_start.is_none() {
token_start = Some(index);
}
}
push_shellish_token(line, &mut token_start, line.len(), &mut segment);
if !segment.is_empty() {
segments.push(segment);
}
segments
}
fn push_shellish_token<'a>(
line: &'a str,
token_start: &mut Option<usize>,
end: usize,
segment: &mut Vec<&'a str>,
) {
if let Some(start) = token_start.take() {
let token = line[start..end].trim_matches(|character| matches!(character, '\'' | '"'));
if !token.is_empty() {
segment.push(token);
}
}
}
fn is_shell_command_separator(character: char) -> bool {
matches!(character, ';' | '&' | '|' | '(' | ')')
}
#[cfg(test)]
mod tests {
#[test]
fn shellish_tokens_stay_scoped_to_command_segments() {
let segments = super::shellish_token_segments(
"exec truth --version && hook-dispatch commit-msg \"$@\"; truth hook-dispatch pre-push",
);
assert_eq!(
segments,
vec![
vec!["exec", "truth", "--version"],
vec!["hook-dispatch", "commit-msg", "$@"],
vec!["truth", "hook-dispatch", "pre-push"],
]
);
}
#[test]
fn quoted_separators_do_not_split_segments() {
let segments = super::shellish_token_segments(
"exec \"/usr/local/bin/truth\" hook-dispatch commit-msg \"path;with|separators\" && echo done",
);
assert_eq!(
segments,
vec![
vec![
"exec",
"/usr/local/bin/truth",
"hook-dispatch",
"commit-msg",
"path;with|separators",
],
vec!["echo", "done"],
]
);
}
}