use std::{fmt, process::Command};
use serde::{
de::{Error, SeqAccess, Visitor},
ser::SerializeSeq,
Deserializer, Serializer,
};
pub fn shell(line: &str) -> Command {
let (program, flag) = if cfg!(windows) {
("cmd", "/C")
} else {
("/bin/sh", "-c")
};
let mut cmd = Command::new(program);
cmd.arg(flag).arg(line);
cmd
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Command, D::Error> {
deserializer.deserialize_any(CommandVisitor)
}
pub fn serialize<S: Serializer>(cmd: &Command, serializer: S) -> Result<S::Ok, S::Error> {
let args: Vec<_> = cmd
.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect();
let mut seq = serializer.serialize_seq(Some(args.len() + 1))?;
seq.serialize_element(&cmd.get_program().to_string_lossy())?;
for arg in &args {
seq.serialize_element(arg)?;
}
seq.end()
}
struct CommandVisitor;
impl<'de> Visitor<'de> for CommandVisitor {
type Value = Command;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str(
"a shell command line (string, wrapped through the platform shell) \
or a non-empty list of strings (program + args)",
)
}
fn visit_str<E: Error>(self, v: &str) -> Result<Self::Value, E> {
let line = v.trim();
if line.is_empty() {
return Err(E::custom("command cannot be empty"));
}
Ok(shell(line))
}
fn visit_seq<A: SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {
let Some(program) = seq.next_element::<String>()? else {
return Err(<A::Error as Error>::custom("command cannot be empty"));
};
let mut cmd = Command::new(program);
while let Some(arg) = seq.next_element::<String>()? {
cmd.arg(arg);
}
Ok(cmd)
}
}
#[cfg(test)]
mod tests {
use std::process::Command;
use serde::de::value::{Error, SeqDeserializer, StringDeserializer};
fn de_str(s: &str) -> Result<Command, Error> {
let d = StringDeserializer::<Error>::new(s.to_owned());
super::deserialize(d)
}
fn de_seq<'a, I: IntoIterator<Item = &'a str>>(items: I) -> Result<Command, Error> {
let owned: Vec<String> = items.into_iter().map(String::from).collect();
let d = SeqDeserializer::<_, Error>::new(owned.into_iter());
super::deserialize(d)
}
fn parts(cmd: &Command) -> (String, Vec<String>) {
(
cmd.get_program().to_string_lossy().into_owned(),
cmd.get_args()
.map(|a| a.to_string_lossy().into_owned())
.collect(),
)
}
#[test]
fn deserialize_string_wraps_in_platform_shell() {
let cmd = de_str("pass show foo | tail -1").unwrap();
let expected = super::shell("pass show foo | tail -1");
assert_eq!(parts(&cmd), parts(&expected));
}
#[test]
fn deserialize_empty_or_blank_string() {
assert_eq!(
de_str("").unwrap_err().to_string(),
"command cannot be empty"
);
assert_eq!(
de_str(" \n\t").unwrap_err().to_string(),
"command cannot be empty"
);
}
#[test]
fn deserialize_seq() {
let cmd = de_seq(["pass", "show", "foo"]).unwrap();
let (prog, args) = parts(&cmd);
assert_eq!(prog, "pass");
assert_eq!(args, vec!["show", "foo"]);
}
#[test]
fn deserialize_empty_seq() {
let v: [&str; 0] = [];
assert_eq!(
de_seq(v).unwrap_err().to_string(),
"command cannot be empty"
);
}
}