pub fn escape_single_quote_content(value: &str) -> String {
value.replace('\'', "'\\''")
}
pub fn quote_arg(arg: &str) -> String {
if arg.is_empty() {
return "''".to_string();
}
const SHELL_META: &[char] = &[
' ', '\t', '\n', '\'', '"', '\\', '$', '`', '!', '*', '?', '[', ']', '(', ')', '{', '}',
'<', '>', '|', '&', ';', '#', '~',
];
if !arg.contains(SHELL_META) {
return arg.to_string();
}
format!("'{}'", escape_single_quote_content(arg))
}
pub fn quote_args(args: &[String]) -> String {
args.iter()
.map(|a| quote_arg(a))
.collect::<Vec<_>>()
.join(" ")
}
pub fn normalize_args(args: &[String]) -> Vec<String> {
if args.len() != 1 || !args[0].contains(' ') {
return args.to_vec();
}
split_respecting_quotes(&args[0])
}
fn split_respecting_quotes(input: &str) -> Vec<String> {
let mut result = Vec::new();
let mut current = String::new();
let mut chars = input.chars().peekable();
let mut in_single_quote = false;
let mut in_double_quote = false;
while let Some(c) = chars.next() {
match c {
'\'' if !in_double_quote => {
in_single_quote = !in_single_quote;
}
'"' if !in_single_quote => {
in_double_quote = !in_double_quote;
}
' ' | '\t' if !in_single_quote && !in_double_quote => {
if !current.is_empty() {
result.push(std::mem::take(&mut current));
}
}
'\\' if in_double_quote => {
if let Some(&next) = chars.peek() {
if matches!(next, '"' | '\\' | '$' | '`') {
chars.next();
current.push(next);
} else {
current.push(c);
}
} else {
current.push(c);
}
}
_ => current.push(c),
}
}
if !current.is_empty() {
result.push(current);
}
result
}
pub fn escape_command_for_shell(command: &str) -> String {
format!("'{}'", escape_single_quote_content(command))
}
pub fn quote_path(path: &str) -> String {
format!("'{}'", escape_single_quote_content(path))
}
pub fn escape_perl_regex(pattern: &str) -> String {
let mut escaped = String::new();
for c in pattern.chars() {
match c {
'\\' | '^' | '$' | '.' | '|' | '?' | '*' | '+' | '(' | ')' | '[' | ']' | '{' | '}'
| '/' => {
escaped.push('\\');
escaped.push(c);
}
_ => escaped.push(c),
}
}
escaped
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quote_arg_simple() {
assert_eq!(quote_arg("version"), "version");
assert_eq!(quote_arg("core"), "core");
}
#[test]
fn quote_arg_with_spaces() {
assert_eq!(quote_arg("hello world"), "'hello world'");
}
#[test]
fn quote_arg_with_parens() {
assert_eq!(
quote_arg("var_export(wp_get_current_user()->ID);"),
"'var_export(wp_get_current_user()->ID);'"
);
}
#[test]
fn quote_arg_with_single_quote() {
assert_eq!(quote_arg("it's"), "'it'\\''s'");
}
#[test]
fn quote_arg_empty() {
assert_eq!(quote_arg(""), "''");
}
#[test]
fn quote_args_mixed() {
let args = vec!["eval".to_string(), "echo 'test';".to_string()];
assert_eq!(quote_args(&args), "eval 'echo '\\''test'\\'';'");
}
#[test]
fn quote_path_simple() {
assert_eq!(quote_path("/var/www"), "'/var/www'");
}
#[test]
fn quote_path_with_quote() {
assert_eq!(quote_path("/var/www/it's"), "'/var/www/it'\\''s'");
}
#[test]
fn escape_perl_regex_simple() {
assert_eq!(escape_perl_regex("hello"), "hello");
assert_eq!(escape_perl_regex("test123"), "test123");
}
#[test]
fn escape_perl_regex_special_chars() {
assert_eq!(escape_perl_regex("hello.world"), "hello\\.world");
assert_eq!(escape_perl_regex("price$100"), "price\\$100");
assert_eq!(escape_perl_regex("a|b|c"), "a\\|b\\|c");
assert_eq!(escape_perl_regex("foo+"), "foo\\+");
assert_eq!(escape_perl_regex("test*"), "test\\*");
}
#[test]
fn escape_perl_regex_brackets() {
assert_eq!(escape_perl_regex("[test]"), "\\[test\\]");
assert_eq!(escape_perl_regex("func()"), "func\\(\\)");
}
#[test]
fn escape_perl_regex_slash() {
assert_eq!(escape_perl_regex("path/to/file"), "path\\/to\\/file");
assert_eq!(escape_perl_regex("/var/www"), "\\/var\\/www");
}
#[test]
fn normalize_args_multiple_args_unchanged() {
let args = vec![
"arg1".to_string(),
"arg2".to_string(),
"--flag".to_string(),
];
assert_eq!(normalize_args(&args), args);
}
#[test]
fn normalize_args_single_arg_with_spaces_splits() {
let args = vec!["arg1 arg2 --flag".to_string()];
assert_eq!(
normalize_args(&args),
vec!["arg1".to_string(), "arg2".to_string(), "--flag".to_string()]
);
}
#[test]
fn normalize_args_single_arg_no_spaces_unchanged() {
let args = vec!["simple".to_string()];
assert_eq!(normalize_args(&args), args);
}
#[test]
fn normalize_args_empty_vec() {
let args: Vec<String> = vec![];
assert_eq!(normalize_args(&args), args);
}
#[test]
fn normalize_args_quoted_and_unquoted_equivalent() {
let unquoted = vec!["post".to_string(), "list".to_string()];
let quoted = vec!["post list".to_string()];
let normalized_unquoted = normalize_args(&unquoted);
let normalized_quoted = normalize_args("ed);
assert_eq!(normalized_unquoted, normalized_quoted);
assert_eq!(normalized_quoted, vec!["post", "list"]);
}
#[test]
fn normalize_args_respects_single_quotes() {
let args = vec!["eval 'echo foo;'".to_string()];
assert_eq!(normalize_args(&args), vec!["eval", "echo foo;"]);
}
#[test]
fn normalize_args_respects_double_quotes() {
let args = vec!["eval \"echo foo;\"".to_string()];
assert_eq!(normalize_args(&args), vec!["eval", "echo foo;"]);
}
#[test]
fn normalize_args_preserves_backslashes_in_single_quotes() {
let args = vec!["eval 'print_r(\\Namespace\\Class::method());'".to_string()];
assert_eq!(
normalize_args(&args),
vec!["eval", "print_r(\\Namespace\\Class::method());"]
);
}
#[test]
fn normalize_args_mixed_content() {
let args = vec!["cmd 'arg with spaces' --flag value".to_string()];
assert_eq!(
normalize_args(&args),
vec!["cmd", "arg with spaces", "--flag", "value"]
);
}
#[test]
fn normalize_args_wp_eval_php_code() {
let args = vec!["eval 'echo json_encode(get_option(\"blogname\"));'".to_string()];
assert_eq!(
normalize_args(&args),
vec!["eval", "echo json_encode(get_option(\"blogname\"));"]
);
}
#[test]
fn normalize_args_nested_quotes() {
let args = vec!["cmd 'say \"hello\"'".to_string()];
assert_eq!(normalize_args(&args), vec!["cmd", "say \"hello\""]);
}
#[test]
fn normalize_args_double_quote_escapes() {
let args = vec!["cmd \"path\\\\to\\\\file\"".to_string()];
assert_eq!(normalize_args(&args), vec!["cmd", "path\\to\\file"]);
}
}