use crate::config::DroppedFileQuoteStyle;
use std::path::Path;
const SHELL_SPECIAL_CHARS: &[char] = &[
' ', '\t', '\n', '\r', '\'', '"', '`', '$', '!', '&', '|', ';', '(', ')', '{', '}', '[', ']', '<', '>', '*', '?', '\\', '#', '~', '^', ];
fn needs_quoting(path: &str) -> bool {
path.chars().any(|c| SHELL_SPECIAL_CHARS.contains(&c))
}
fn quote_single(path: &str) -> String {
let escaped = path.replace('\'', "'\\''");
format!("'{}'", escaped)
}
fn quote_double(path: &str) -> String {
let mut result = String::with_capacity(path.len() + 10);
result.push('"');
for c in path.chars() {
match c {
'$' | '`' | '\\' | '"' | '!' => {
result.push('\\');
result.push(c);
}
_ => result.push(c),
}
}
result.push('"');
result
}
fn quote_backslash(path: &str) -> String {
if !needs_quoting(path) {
return path.to_string();
}
let mut result = String::with_capacity(path.len() * 2);
for c in path.chars() {
if SHELL_SPECIAL_CHARS.contains(&c) {
result.push('\\');
}
result.push(c);
}
result
}
pub fn quote_path(path: &Path, style: DroppedFileQuoteStyle) -> String {
let path_str = path.to_string_lossy();
match style {
DroppedFileQuoteStyle::SingleQuotes => quote_single(&path_str),
DroppedFileQuoteStyle::DoubleQuotes => quote_double(&path_str),
DroppedFileQuoteStyle::Backslash => quote_backslash(&path_str),
DroppedFileQuoteStyle::None => path_str.into_owned(),
}
}
pub fn quote_paths(paths: &[&Path], style: DroppedFileQuoteStyle) -> String {
paths
.iter()
.map(|p| quote_path(p, style))
.collect::<Vec<_>>()
.join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_simple_path_always_quoted() {
let path = Path::new("/usr/local/bin/program");
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
"'/usr/local/bin/program'"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
"\"/usr/local/bin/program\""
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::Backslash),
"/usr/local/bin/program"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::None),
"/usr/local/bin/program"
);
}
#[test]
fn test_path_with_spaces() {
let path = Path::new("/path/to/file with spaces.txt");
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
"'/path/to/file with spaces.txt'"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
"\"/path/to/file with spaces.txt\""
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::Backslash),
"/path/to/file\\ with\\ spaces.txt"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::None),
"/path/to/file with spaces.txt"
);
}
#[test]
fn test_path_with_single_quote() {
let path = Path::new("/path/to/it's a file.txt");
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
"'/path/to/it'\\''s a file.txt'"
);
}
#[test]
fn test_path_with_dollar_sign() {
let path = Path::new("/path/to/$HOME/file.txt");
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
"'/path/to/$HOME/file.txt'"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::DoubleQuotes),
"\"/path/to/\\$HOME/file.txt\""
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::Backslash),
"/path/to/\\$HOME/file.txt"
);
}
#[test]
fn test_path_with_glob_chars() {
let path = Path::new("/path/to/*.txt");
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::SingleQuotes),
"'/path/to/*.txt'"
);
assert_eq!(
quote_path(path, DroppedFileQuoteStyle::Backslash),
"/path/to/\\*.txt"
);
}
#[test]
fn test_multiple_paths() {
let paths: Vec<&Path> = vec![
Path::new("/simple/path"),
Path::new("/path with spaces"),
Path::new("/path/with$dollar"),
];
let result = quote_paths(&paths, DroppedFileQuoteStyle::SingleQuotes);
assert_eq!(
result,
"'/simple/path' '/path with spaces' '/path/with$dollar'"
);
}
}