serde_cmd/
lib.rs

1#![doc = include_str!("../README.md")]
2#![warn(missing_docs)]
3#[cfg(feature = "serde")]
4use serde::de::{Deserialize, Deserializer, Visitor};
5use std::borrow::Cow;
6use std::process::Command;
7use std::str::CharIndices;
8
9/// A command parsed into arguments that might be borrowed from the source.
10#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Cmd<S = String> {
12    /// The path of the command to be executed.
13    pub path: S,
14    /// Optional arguments to the command.
15    pub args: Vec<S>,
16}
17
18/// A variant of the command that borrows the data and does no copying
19/// unless necessary.
20pub type CmdBorrowed<'a> = Cmd<Cow<'a, str>>;
21
22/// Iterator over arguments of the command.
23#[derive(Clone, Debug)]
24pub struct ArgIter<'a> {
25    source: &'a str,
26    chars: CharIndices<'a>,
27}
28
29impl<'a> ArgIter<'a> {
30    /// Construct a new argument iterator from the source.
31    #[inline]
32    pub fn new(source: &'a str) -> Self {
33        ArgIter {
34            source,
35            chars: source.char_indices(),
36        }
37    }
38}
39
40impl<'a> Iterator for ArgIter<'a> {
41    type Item = Cow<'a, str>;
42
43    #[inline]
44    fn next(&mut self) -> Option<Self::Item> {
45        #[derive(Clone, Copy)]
46        enum State {
47            None,
48            Quote(char),
49        }
50
51        let mut previous;
52        let mut initial;
53        let mut owned: Option<String> = None;
54        loop {
55            let (i, ch) = self.chars.next()?;
56            if !ch.is_whitespace() {
57                previous = ch;
58                initial = i;
59                break;
60            }
61        }
62        let mut last = self.source.len();
63
64        let mut state = match previous {
65            '"' | '\'' => {
66                initial += 1;
67                State::Quote(previous)
68            }
69            '\\' => {
70                initial += 1;
71                State::None
72            }
73            _ => State::None,
74        };
75        let initial_state = state;
76        let mut state_changes = 0;
77        let mut found_whitespace = false;
78
79        for (l, c) in self.chars.by_ref() {
80            last = l;
81
82            if (c == '"' || c == '\'') && previous != '\\' {
83                match state {
84                    State::None => {
85                        state_changes += 1;
86                        state = State::Quote(c)
87                    }
88                    State::Quote(q) if c == q => {
89                        state_changes += 1;
90                        state = State::None
91                    }
92                    _ => {}
93                }
94            } else if let State::None = state {
95                if c.is_whitespace() {
96                    found_whitespace = true;
97                    break;
98                }
99            }
100
101            if c != '\\' || previous == '\\' {
102                if let Some(ref mut arg) = owned {
103                    arg.push(c);
104                }
105            }
106
107            if c == '\\' && owned.is_none() {
108                owned = Some(self.source[initial..last].to_string());
109            }
110            previous = c;
111        }
112
113        if !found_whitespace && owned.is_none() {
114            last = self.source.len();
115        };
116
117        match initial_state {
118            State::Quote(_) if state_changes == 1 => {
119                if let Some(ref mut arg) = owned {
120                    arg.pop();
121                } else {
122                    last -= 1;
123                }
124            }
125            _ => {}
126        };
127
128        owned
129            .map(|s| s.into())
130            .or_else(|| self.source.get(initial..last).map(|s| s.into()))
131    }
132}
133
134impl<S: AsRef<str>> Cmd<S> {
135    /// Turn `Cmd` into an actual command that can be executed.
136    #[inline]
137    pub fn make_command(&self) -> Command {
138        let mut command = Command::new(self.path.as_ref());
139        command.args(self.args.iter().map(|arg| arg.as_ref()));
140        command
141    }
142}
143
144impl<'a> From<ArgIter<'a>> for Cmd<Cow<'a, str>> {
145    #[inline]
146    fn from(mut iter: ArgIter<'a>) -> Self {
147        let path = iter.next().unwrap_or(Cow::Borrowed(""));
148        let args = iter.collect();
149        Cmd { path, args }
150    }
151}
152
153impl From<ArgIter<'_>> for Cmd {
154    #[inline]
155    fn from(iter: ArgIter) -> Self {
156        let mut iter = iter.map(|s| s.to_string());
157        let path = iter.next().unwrap_or_else(|| "".to_string());
158        let args = iter.collect();
159        Cmd { path, args }
160    }
161}
162
163#[cfg(feature = "serde")]
164impl<'de: 'a, 'a> Deserialize<'de> for Cmd<Cow<'a, str>> {
165    #[inline]
166    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
167    where
168        D: Deserializer<'de>,
169    {
170        struct CommandVisitor;
171
172        impl<'de> Visitor<'de> for CommandVisitor {
173            type Value = Cmd<Cow<'de, str>>;
174
175            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
176                write!(formatter, "a command string")
177            }
178
179            #[inline]
180            fn visit_borrowed_str<E>(self, v: &'de str) -> Result<Self::Value, E> {
181                Ok(ArgIter::new(v).into())
182            }
183
184            #[inline]
185            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
186                let mut iter = ArgIter::new(v).map(|s| Cow::Owned(s.to_string()));
187                let path = iter.next().unwrap_or_else(|| "".into());
188                let args = iter.collect();
189                Ok(Cmd { path, args })
190            }
191        }
192
193        deserializer.deserialize_string(CommandVisitor)
194    }
195}
196
197#[cfg(feature = "serde")]
198impl<'de> Deserialize<'de> for Cmd {
199    #[inline]
200    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
201    where
202        D: Deserializer<'de>,
203    {
204        struct CommandVisitor;
205
206        impl<'de> Visitor<'de> for CommandVisitor {
207            type Value = Cmd;
208
209            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
210                write!(formatter, "a command string")
211            }
212
213            #[inline]
214            fn visit_str<E>(self, v: &str) -> Result<Self::Value, E> {
215                Ok(ArgIter::new(v).into())
216            }
217        }
218
219        deserializer.deserialize_string(CommandVisitor)
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_argiter() {
229        let source = "echo h \"hello\\\" world\" \"\" \"h\\\"\"";
230        let args: Vec<Cow<str>> = ArgIter::new(source).collect();
231        assert_eq!(args, &["echo", "h", "hello\" world", "", "h\""]);
232    }
233
234    #[test]
235    fn test_deserialize() {
236        #[derive(Debug, PartialEq, Eq, serde_derive::Deserialize)]
237        pub struct Simple<'a> {
238            #[serde(borrow)]
239            owned: CmdBorrowed<'a>,
240            #[serde(borrow)]
241            borrowed: CmdBorrowed<'a>,
242        }
243
244        let cmd = toml::de::from_str::<Simple>(include_str!("test.toml")).unwrap();
245
246        assert_eq!(
247            cmd,
248            Simple {
249                owned: Cmd {
250                    path: "echo".into(),
251                    args: vec!["hello world".into()],
252                },
253                borrowed: Cmd {
254                    path: "rm".into(),
255                    args: vec!["-rf".into()],
256                }
257            }
258        );
259        assert!(matches!(cmd.borrowed.path, Cow::Borrowed(_)));
260    }
261}