1pub fn split(line: &str) -> Result<Vec<String>, String> {
7 let mut words = Vec::new();
8 let mut cur = String::new();
9 let mut in_word = false;
10 let mut chars = line.chars();
11 while let Some(c) = chars.next() {
12 match c {
13 c if c.is_whitespace() => {
14 if in_word {
15 words.push(std::mem::take(&mut cur));
16 in_word = false;
17 }
18 }
19 '#' if !in_word => break,
20 '"' => {
21 in_word = true;
22 loop {
23 match chars.next() {
24 None => return Err("unterminated \" quote".into()),
25 Some('"') => break,
26 Some('\\') => match chars.next() {
27 Some('n') => cur.push('\n'),
28 Some('t') => cur.push('\t'),
29 Some(e) => cur.push(e),
30 None => return Err("unterminated \" quote".into()),
31 },
32 Some(x) => cur.push(x),
33 }
34 }
35 }
36 '\'' => {
37 in_word = true;
38 loop {
39 match chars.next() {
40 None => return Err("unterminated ' quote".into()),
41 Some('\'') => break,
42 Some(x) => cur.push(x),
43 }
44 }
45 }
46 c => {
47 in_word = true;
48 cur.push(c);
49 }
50 }
51 }
52 if in_word {
53 words.push(cur);
54 }
55 Ok(words)
56}
57
58pub fn quote(w: &str) -> String {
60 if !w.is_empty() && !w.chars().any(|c| c.is_whitespace() || matches!(c, '"' | '\'' | '\\' | '#')) {
61 return w.to_string();
62 }
63 let mut s = String::from("\"");
64 for c in w.chars() {
65 match c {
66 '"' => s.push_str("\\\""),
67 '\\' => s.push_str("\\\\"),
68 '\n' => s.push_str("\\n"),
69 '\t' => s.push_str("\\t"),
70 c => s.push(c),
71 }
72 }
73 s.push('"');
74 s
75}
76
77#[derive(Debug, Clone, PartialEq, Default)]
79pub struct Args {
80 items: Vec<String>,
81 rest: Vec<String>,
83}
84
85fn is_option(a: &str) -> bool {
86 a.starts_with('-') && a.len() > 1 && !a.chars().nth(1).is_some_and(|c| c.is_ascii_digit())
88}
89
90impl Args {
91 pub fn new(items: impl IntoIterator<Item = impl Into<String>>) -> Self {
92 let mut all: Vec<String> = items.into_iter().map(Into::into).collect();
93 let rest = match all.iter().position(|a| a == "--") {
94 Some(i) => all.split_off(i).into_iter().skip(1).collect(),
95 None => Vec::new(),
96 };
97 Self { items: all, rest }
98 }
99
100 pub fn flag(&mut self, names: &[&str]) -> bool {
102 let mut found = false;
103 self.items.retain(|a| {
104 let hit = names.contains(&a.as_str());
105 found |= hit;
106 !hit
107 });
108 found
109 }
110
111 pub fn value(&mut self, names: &[&str]) -> Result<Option<String>, String> {
113 let mut out = None;
114 let mut i = 0;
115 while i < self.items.len() {
116 let a = self.items.get(i).cloned().unwrap_or_default();
117 if let Some(v) = names.iter().find_map(|n| a.strip_prefix(&format!("{n}="))) {
118 out = Some(v.to_string());
119 self.items.remove(i);
120 } else if names.contains(&a.as_str()) {
121 self.items.remove(i);
122 if i < self.items.len() {
123 out = Some(self.items.remove(i));
124 } else {
125 return Err(format!("{a} needs a value"));
126 }
127 } else {
128 i = i.saturating_add(1);
129 }
130 }
131 Ok(out)
132 }
133
134 pub fn number<T: std::str::FromStr>(&mut self, names: &[&str]) -> Result<Option<T>, String> {
136 match self.value(names)? {
137 Some(v) => v.parse().map(Some).map_err(|_| format!("{}: `{v}` is not a valid number", names.first().copied().unwrap_or("option"))),
138 None => Ok(None),
139 }
140 }
141
142 pub fn positional(&mut self) -> Option<String> {
144 match self.items.iter().position(|a| !is_option(a)) {
145 Some(i) => Some(self.items.remove(i)),
146 None => (!self.rest.is_empty()).then(|| self.rest.remove(0)),
147 }
148 }
149
150 pub fn rest(&mut self) -> Vec<String> {
152 let mut out = Vec::new();
153 while let Some(p) = self.positional() {
154 out.push(p);
155 }
156 out
157 }
158
159 pub fn is_empty(&self) -> bool {
161 self.items.is_empty() && self.rest.is_empty()
162 }
163
164 pub fn finish(self) -> Result<(), String> {
166 match self.items.first().or(self.rest.first()) {
167 None => Ok(()),
168 Some(a) if is_option(a) => Err(format!("unknown option {a}")),
169 Some(a) => Err(format!("unexpected argument `{a}`")),
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177
178 #[test]
179 fn splits_like_a_shell() {
180 assert_eq!(split(r#"fill @e3 "hello world""#).unwrap(), ["fill", "@e3", "hello world"]);
181 assert_eq!(split(r#"type 'it''s' "a\"b\\c\n""#).unwrap(), ["type", "its", "a\"b\\c\n"]);
182 assert_eq!(split(" click e3 # a comment").unwrap(), ["click", "e3"]);
183 assert_eq!(split("# only a comment").unwrap(), Vec::<String>::new());
184 assert_eq!(split(r#"fill e1 "" x"#).unwrap(), ["fill", "e1", "", "x"]);
185 assert_eq!(split("text=a#b").unwrap(), ["text=a#b"]);
186 assert!(split(r#"fill e1 "open"#).is_err());
187 assert!(split("fill e1 'open").is_err());
188 }
189
190 #[test]
191 fn quote_round_trips() {
192 for w in ["plain", "two words", "", "a\"b", "back\\slash", "#hash", "tab\there"] {
193 assert_eq!(split("e(w)).unwrap(), [w], "{w}");
194 }
195 }
196
197 #[test]
198 fn options_in_any_order() {
199 let mut a = Args::new(["@e3", "--count=2", "-i", "--button", "secondary", "--", "--literal"]);
200 assert!(a.flag(&["-i", "--interactive"]));
201 assert_eq!(a.number::<u32>(&["--count"]), Ok(Some(2)));
202 assert_eq!(a.value(&["--button"]), Ok(Some("secondary".into())));
203 assert_eq!(a.positional().as_deref(), Some("@e3"));
204 assert_eq!(a.positional().as_deref(), Some("--literal"));
205 assert_eq!(a.finish(), Ok(()));
206 let mut a = Args::new(["down", "-300"]);
207 assert_eq!(a.rest(), ["down", "-300"]);
208 assert_eq!(Args::new(["--nope"]).finish(), Err("unknown option --nope".into()));
209 assert_eq!(Args::new(["x"]).finish(), Err("unexpected argument `x`".into()));
210 assert_eq!(Args::new(["--timeout"]).value(&["--timeout"]), Err("--timeout needs a value".into()));
211 assert!(Args::new(["--timeout", "soon"]).number::<u64>(&["--timeout"]).is_err());
212 }
213}