use anyhow::{bail, Result};
pub(crate) fn split_browser_command(label: &str, raw: &str) -> Result<Vec<String>> {
let mut words: Vec<String> = Vec::new();
let mut current = String::new();
let mut has_word = false;
let mut chars = raw.chars();
while let Some(c) = chars.next() {
match c {
c if c.is_whitespace() => {
if has_word {
words.push(std::mem::take(&mut current));
has_word = false;
}
}
'\'' => {
has_word = true;
loop {
match chars.next() {
Some('\'') => break,
Some(ch) => current.push(ch),
None => bail!("{label} has an unterminated single quote: {raw}"),
}
}
}
'"' => {
has_word = true;
loop {
match chars.next() {
Some('"') => break,
Some('\\') => match chars.next() {
Some(ch @ ('"' | '\\')) => current.push(ch),
Some(ch) => {
current.push('\\');
current.push(ch);
}
None => bail!("{label} has an unterminated double quote: {raw}"),
},
Some(ch) => current.push(ch),
None => bail!("{label} has an unterminated double quote: {raw}"),
}
}
}
'\\' => {
has_word = true;
match chars.next() {
Some(ch) => current.push(ch),
None => current.push('\\'),
}
}
ch => {
has_word = true;
current.push(ch);
}
}
}
if has_word {
words.push(current);
}
if words.is_empty() {
bail!("{label} is set but contains no command");
}
Ok(words)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn split_browser_command_splits_on_unquoted_whitespace() {
assert_eq!(
split_browser_command("LABEL", "chrome --new-window {url}").unwrap(),
vec!["chrome", "--new-window", "{url}"]
);
}
#[test]
fn split_browser_command_keeps_quoted_spaces_together() {
assert_eq!(
split_browser_command(
"LABEL",
"'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome' \
--profile-directory=\"Profile 1\" --new-window {url}"
)
.unwrap(),
vec![
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
"--profile-directory=Profile 1",
"--new-window",
"{url}",
]
);
}
#[test]
fn split_browser_command_handles_backslash_escapes() {
assert_eq!(
split_browser_command("LABEL", r#"chrome a\ b "c\"d" "e\\f" "g\h""#).unwrap(),
vec!["chrome", "a b", "c\"d", "e\\f", "g\\h"]
);
}
#[test]
fn split_browser_command_rejects_unterminated_quotes() {
assert!(split_browser_command("LABEL", "chrome \"--flag").is_err());
assert!(split_browser_command("LABEL", "chrome '--flag").is_err());
}
#[test]
fn split_browser_command_rejects_a_double_quote_left_open_by_a_trailing_escape() {
assert!(split_browser_command("LABEL", "\"abc\\").is_err());
}
#[test]
fn split_browser_command_treats_a_trailing_unquoted_backslash_as_literal() {
assert_eq!(
split_browser_command("LABEL", "chrome\\").unwrap(),
vec!["chrome\\"]
);
}
#[test]
fn split_browser_command_rejects_an_empty_command() {
assert!(split_browser_command("LABEL", " ").is_err());
assert!(split_browser_command("LABEL", "").is_err());
}
#[test]
fn split_browser_command_error_includes_the_label() {
let err = split_browser_command("MY_ENV_VAR", "chrome \"--flag").unwrap_err();
assert!(err.to_string().contains("MY_ENV_VAR"));
}
}