#![doc = include_str!("../README.md")]
#![warn(missing_docs)]
#[cfg(feature = "serde")]
use serde::de::{Deserialize, Deserializer, Visitor};
use std::borrow::Cow;
use std::process::Command;
use std::str::CharIndices;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Cmd<S = String> {
pub path: S,
pub args: Vec<S>,
}
pub type CmdBorrowed<'a> = Cmd<Cow<'a, str>>;
#[derive(Clone, Debug)]
pub struct ArgIter<'a> {
source: &'a str,
chars: CharIndices<'a>,
}
impl<'a> ArgIter<'a> {
#[inline]
pub fn new(source: &'a str) -> Self {
ArgIter {
source,
chars: source.char_indices(),
}
}
}
impl<'a> Iterator for ArgIter<'a> {
type Item = Cow<'a, str>;
#[inline]
fn next(&mut self) -> Option<Self::Item> {
#[derive(Clone, Copy)]
enum State {
None,
Quote(char),
}
let mut previous;
let mut initial;
let mut owned: Option<String> = None;
loop {
let (i, ch) = self.chars.next()?;
if !ch.is_whitespace() {
previous = ch;
initial = i;
break;
}
}
let mut last = self.source.len();
let mut state = match previous {
'"' | '\'' => {
initial += 1;
State::Quote(previous)
}
'\\' => {
initial += 1;
State::None
}
_ => State::None,
};
let initial_state = state;
let mut state_changes = 0;
let mut found_whitespace = false;
for (l, c) in self.chars.by_ref() {
last = l;
if (c == '"' || c == '\'') && previous != '\\' {
match state {
State::None => {
state_changes += 1;
state = State::Quote(c)
}
State::Quote(q) if c == q => {
state_changes += 1;
state = State::None
}
_ => {}
}
} else if let State::None = state {
if c.is_whitespace() {
found_whitespace = true;
break;
}
}
if c != '\\' || previous == '\\' {
if let Some(ref mut arg) = owned {
arg.push(c);
}
}
if c == '\\' && owned.is_none() {
owned = Some(self.source[initial..last].to_string());
}
previous = c;
}
if !found_whitespace && owned.is_none() {
last = self.source.len();
};
match initial_state {
State::Quote(_) if state_changes == 1 => {
if let Some(ref mut arg) = owned {
arg.pop();
} else {
last -= 1;
}
}
_ => {}
};
owned
.map(|s| s.into())
.or_else(|| self.source.get(initial..last).map(|s| s.into()))
}
}
impl<S: AsRef<str>> Cmd<S> {
#[inline]
pub fn make_command(&self) -> Command {
let mut command = Command::new(self.path.as_ref());
command.args(self.args.iter().map(|arg| arg.as_ref()));
command
}
}
impl<'a> From<ArgIter<'a>> for Cmd<Cow<'a, str>> {
#[inline]
fn from(mut iter: ArgIter<'a>) -> Self {
let path = iter.next().unwrap_or(Cow::Borrowed(""));
let args = iter.collect();
Cmd { path, args }
}
}
impl From<ArgIter<'_>> for Cmd {
#[inline]
fn from(iter: ArgIter) -> Self {
let mut iter = iter.map(|s| s.to_string());
let path = iter.next().unwrap_or_else(|| "".to_string());
let args = iter.collect();
Cmd { path, args }
}
}
#[cfg(feature = "serde")]
impl<'de: 'a, 'a> Deserialize<'de> for Cmd<Cow<'a, str>> {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct CommandVisitor;
impl<'de> Visitor<'de> for CommandVisitor {
type Value = Cmd<Cow<'de, str>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a command string")
}
#[inline]
fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> {
Ok(ArgIter::new(v).into())
}
#[inline]
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
let mut iter = ArgIter::new(v).map(|s| Cow::Owned(s.to_string()));
let path = iter.next().unwrap_or_else(|| "".into());
let args = iter.collect();
Ok(Cmd { path, args })
}
}
deserializer.deserialize_string(CommandVisitor)
}
}
#[cfg(feature = "serde")]
impl<'de> Deserialize<'de> for Cmd {
#[inline]
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
struct CommandVisitor;
impl<'de> Visitor<'de> for CommandVisitor {
type Value = Cmd;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a command string")
}
#[inline]
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
Ok(ArgIter::new(v).into())
}
}
deserializer.deserialize_string(CommandVisitor)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_argiter() {
let source = "echo h \"hello\\\" world\" \"\" \"h\\\"\"";
let args: Vec<Cow<str>> = ArgIter::new(source).collect();
assert_eq!(args, &["echo", "h", "hello\" world", "", "h\""]);
}
#[test]
fn test_deserialize() {
#[derive(Debug, PartialEq, Eq, serde_derive::Deserialize)]
pub struct Simple<'a> {
#[serde(borrow)]
owned: CmdBorrowed<'a>,
#[serde(borrow)]
borrowed: CmdBorrowed<'a>,
}
let cmd = toml::de::from_str::<Simple>(include_str!("test.toml")).unwrap();
assert_eq!(
cmd,
Simple {
owned: Cmd {
path: "echo".into(),
args: vec!["hello world".into()],
},
borrowed: Cmd {
path: "rm".into(),
args: vec!["-rf".into()],
}
}
);
assert!(matches!(cmd.borrowed.path, Cow::Borrowed(_)));
}
}