use std::{borrow::Cow, ffi::OsStr};
use anyhow::Result;
pub trait Quote<S> {
fn quote(&self) -> Result<S>;
}
impl Quote<String> for String {
fn quote(&self) -> Result<String> {
self.as_str().quote()
}
}
impl Quote<String> for &str {
fn quote(&self) -> Result<String> {
try_quote(self)
}
}
impl Quote<String> for Cow<'_, str> {
fn quote(&self) -> Result<String> {
try_quote(self)
}
}
impl Quote<String> for &OsStr {
fn quote(&self) -> Result<String> {
self.to_string_lossy().quote()
}
}
fn must_quote(byte: u8) -> bool {
matches!(byte, b' ' | b'\'' | b'"')
}
fn try_quote(s: &str) -> Result<String> {
if !s.bytes().any(must_quote) {
Ok(s.into())
} else {
let len = s.len();
let mut escaped = Vec::with_capacity(len + 2);
escaped.push(b'\'');
for byte in s.bytes() {
match byte {
b'\'' | b'"' => {
escaped.reserve(4);
escaped.push(b'\'');
escaped.push(b'\\');
escaped.push(byte);
escaped.push(b'\'');
}
_ => escaped.push(byte),
}
}
escaped.push(b'\'');
let s = String::from_utf8(escaped)?;
crate::log_info!("try quote: #{s}#");
Ok(s)
}
}
pub trait JoinQuote {
fn join_quote(&self, sep: &str) -> String;
}
impl JoinQuote for &Vec<String> {
fn join_quote(&self, sep: &str) -> String {
self.iter()
.filter_map(|fp| fp.quote().ok())
.collect::<Vec<_>>()
.join(sep)
}
}
pub fn append_files_to_shell_command(shell_command: String, files: Option<Vec<String>>) -> String {
let Some(files) = &files else {
return shell_command;
};
shell_command + " " + &files.join_quote(" ")
}