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#[derive(Clone, Debug, PartialEq, Eq)]
11pub struct Cmd<S = String> {
12 pub path: S,
14 pub args: Vec<S>,
16}
17
18pub type CmdBorrowed<'a> = Cmd<Cow<'a, str>>;
21
22#[derive(Clone, Debug)]
24pub struct ArgIter<'a> {
25 source: &'a str,
26 chars: CharIndices<'a>,
27}
28
29impl<'a> ArgIter<'a> {
30 #[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 #[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}