1use std::str::FromStr;
2
3use rustyline::Editor;
4
5pub mod prelude {
6 pub use replman_derive::ReplCmd;
7
8 pub use crate::{read_command, Repl, ReplCmd};
9}
10
11pub struct Repl {
12 editor: Editor<()>,
13}
14
15impl Repl {
16 #[allow(clippy::new_without_default)]
17 pub fn new() -> Self {
18 Self {
19 editor: Editor::new(),
20 }
21 }
22
23 pub fn read_command<R>(&mut self) -> anyhow::Result<R>
24 where
25 R: ReplCmd,
26 {
27 loop {
28 let line = self.editor.readline("> ")?;
29 let trimmed = line.trim();
30
31 if trimmed.is_empty() {
32 continue;
33 }
34
35 match R::parse(split_string_unescape(trimmed)) {
36 Ok(cmd) => {
37 self.editor.add_history_entry(trimmed);
38 return Ok(cmd);
39 }
40 Err(err) => eprintln!("Failed to parse command: {}", err),
41 }
42 }
43 }
44}
45
46pub trait ReplCmd {
47 fn help() -> &'static str;
48 fn parse<'a, I>(parts: I) -> anyhow::Result<Self>
49 where
50 Self: Sized,
51 I: Iterator<Item = anyhow::Result<&'a str>> + 'a;
52
53 fn parse_str(s: &str) -> anyhow::Result<Self>
54 where
55 Self: Sized,
56 {
57 Self::parse(split_string_unescape(s))
58 }
59}
60
61pub trait ReplCmdParse {
62 fn parse(item: Option<&str>) -> anyhow::Result<Self>
63 where
64 Self: Sized;
65
66 fn parse_default(s: &str) -> anyhow::Result<Self>
67 where
68 Self: Sized;
69}
70
71macro_rules! impl_with_from_str {
72 ($t:ty) => {
73 impl ReplCmdParse for $t {
74 fn parse(item: Option<&str>) -> anyhow::Result<Self>
75 where
76 Self: Sized,
77 {
78 Ok(item
79 .ok_or_else(|| anyhow::anyhow!("Missing field"))?
80 .parse()?)
81 }
82
83 fn parse_default(s: &str) -> anyhow::Result<Self>
84 where
85 Self: Sized,
86 {
87 Ok(s.parse()?)
88 }
89 }
90 };
91}
92
93impl_with_from_str!(std::net::IpAddr);
94impl_with_from_str!(std::net::SocketAddr);
95impl_with_from_str!(bool);
96impl_with_from_str!(char);
97impl_with_from_str!(f32);
98impl_with_from_str!(f64);
99impl_with_from_str!(i8);
100impl_with_from_str!(i16);
101impl_with_from_str!(i32);
102impl_with_from_str!(i64);
103impl_with_from_str!(i128);
104impl_with_from_str!(isize);
105impl_with_from_str!(u8);
106impl_with_from_str!(u16);
107impl_with_from_str!(u32);
108impl_with_from_str!(u64);
109impl_with_from_str!(u128);
110impl_with_from_str!(usize);
111impl_with_from_str!(std::ffi::OsString);
112impl_with_from_str!(std::net::Ipv4Addr);
113impl_with_from_str!(std::net::Ipv6Addr);
114impl_with_from_str!(std::net::SocketAddrV4);
115impl_with_from_str!(std::net::SocketAddrV6);
116impl_with_from_str!(std::num::NonZeroI8);
117impl_with_from_str!(std::num::NonZeroI16);
118impl_with_from_str!(std::num::NonZeroI32);
119impl_with_from_str!(std::num::NonZeroI64);
120impl_with_from_str!(std::num::NonZeroI128);
121impl_with_from_str!(std::num::NonZeroIsize);
122impl_with_from_str!(std::num::NonZeroU8);
123impl_with_from_str!(std::num::NonZeroU16);
124impl_with_from_str!(std::num::NonZeroU32);
125impl_with_from_str!(std::num::NonZeroU64);
126impl_with_from_str!(std::num::NonZeroU128);
127impl_with_from_str!(std::num::NonZeroUsize);
128impl_with_from_str!(std::path::PathBuf);
129impl_with_from_str!(String);
130
131impl<T> ReplCmdParse for Option<T>
132where
133 T: FromStr,
134 anyhow::Error: From<<T as FromStr>::Err>,
135{
136 fn parse(item: Option<&str>) -> anyhow::Result<Self>
137 where
138 Self: Sized,
139 {
140 Ok(item.map(|s| s.parse()).transpose()?)
141 }
142
143 fn parse_default(_s: &str) -> anyhow::Result<Self>
144 where
145 Self: Sized,
146 {
147 unimplemented!("Using default on an Option field makes no sense")
148 }
149}
150
151pub fn read_command<R>() -> anyhow::Result<R>
152where
153 R: ReplCmd,
154{
155 let mut rl = Editor::<()>::new();
156
157 loop {
158 let line = rl.readline("> ")?;
159
160 if line.trim().is_empty() {
161 continue;
162 }
163
164 match R::parse(split_string_unescape(line.trim())) {
165 Ok(cmd) => return Ok(cmd),
166 Err(err) => eprintln!("Failed to parse command: {}", err),
167 }
168 }
169}
170
171fn split_string_unescape(
172 mut s: &str,
173) -> impl Iterator<Item = anyhow::Result<&str>> {
174 std::iter::from_fn(move || {
175 if s.is_empty() {
176 return None;
177 }
178
179 let next_unescaped_space = find_next_unescaped_space(s);
180
181 let ret = match next_unescaped_space {
182 Ok(x) => match x {
183 Some(x) => {
184 let ret = &s[..x];
185 s = &s[x + 1..];
186
187 ret
188 }
189 None => {
190 let temp = s;
191 s = "";
192
193 temp
194 }
195 },
196 Err(err) => return Some(Err(err)),
197 };
198
199 Some(Ok(unescape(ret)))
200 })
201}
202
203fn unescape(s: &str) -> &str {
204 if (s.starts_with('"') && s.ends_with('"'))
205 || (s.starts_with('\'') && s.ends_with('\''))
206 {
207 &s[1..s.len() - 1]
208 } else {
209 s
210 }
211}
212
213fn find_next_unescaped_space(s: &str) -> anyhow::Result<Option<usize>> {
214 let mut is_in_double_qoutes = false;
215 let mut is_in_single_quotes = false;
216
217 let mut previous_was_quoted = false;
218 for (idx, c) in s.char_indices() {
219 if previous_was_quoted && c != ' ' {
220 return Err(anyhow::anyhow!(
221 "Invalid command fragment, expected a space or end of string, found '{}'",
222 c
223 ));
224 }
225
226 match c {
227 '"' if !is_in_double_qoutes && !is_in_single_quotes => {
228 is_in_double_qoutes = true
229 }
230 '"' if is_in_double_qoutes => {
231 is_in_double_qoutes = false;
232 previous_was_quoted = true;
233 }
234 '\'' if !is_in_double_qoutes && !is_in_single_quotes => {
235 is_in_single_quotes = true
236 }
237 '\'' if is_in_single_quotes => {
238 is_in_single_quotes = false;
239 previous_was_quoted = true;
240 }
241 _ => previous_was_quoted = false,
242 }
243
244 if is_in_double_qoutes || is_in_single_quotes {
245 continue;
246 }
247
248 if c == ' ' {
249 return Ok(Some(idx));
250 }
251 }
252
253 Ok(None)
254}
255
256#[cfg(test)]
257mod tests {
258 use test_case::test_case;
259
260 use super::*;
261
262 #[test_case("Hello", vec!["Hello"] ; "Single item")]
263 #[test_case("Hello World!", vec!["Hello", "World!"] ; "Two items")]
264 #[test_case("", vec![] ; "Empty")]
265 fn basic(s: &str, exp: Vec<&str>) {
266 let actual: Vec<_> =
267 split_string_unescape(s).map(Result::unwrap).collect();
268 assert_eq!(actual, exp);
269 }
270
271 #[test_case(r#""Hello, World!""#, vec!["Hello, World!"] ; "Single - double quotes")]
272 #[test_case(r#"'Hello, World!'"#, vec!["Hello, World!"] ; "Single - single quotes")]
273 #[test_case(r#"'Hello, World!' "What is going on?""#, vec!["Hello, World!", "What is going on?"] ; "Two items - mixed")]
274 #[test_case(r#""" "" """#, vec!["", "", ""] ; "Sequence of double quotes")]
275 fn escaped(s: &str, exp: Vec<&str>) {
276 let actual: Vec<_> =
277 split_string_unescape(s).map(Result::unwrap).collect();
278 assert_eq!(actual, exp);
279 }
280}