cmdtree/
parse.rs

1use super::*;
2use colored::*;
3use std::io::{self, Write};
4
5const PATH_SEP: char = '.';
6
7#[derive(Debug, PartialEq)]
8enum WordResult<'a, R> {
9    Help(&'a SubClass<R>),
10    Cancel,
11    Exit,
12    Class(&'a Arc<SubClass<R>>),
13    Action(&'a Action<R>),
14    Unrecognized,
15}
16
17/// The result variants of `parse_line`.
18#[derive(Debug, PartialEq)]
19pub enum LineResult<R> {
20    /// `help` command reached.
21    Help,
22    /// `cancel` commannd reached.
23    Cancel,
24    /// `exit` command reached.
25    Exit,
26    /// Navigated to a class.
27    Class,
28    /// Action invoked.
29    /// The inner data is the returned data from invocation.
30    Action(R),
31    /// No commands recognised.
32    Unrecognized,
33}
34
35impl<R> LineResult<R> {
36    /// Converts the `LineResult` into the data returned from invoking an action.
37    /// If the result was not an action, `None` is returned.
38    pub fn action_result(self) -> Option<R> {
39        match self {
40            LineResult::Action(r) => Some(r),
41            _ => None,
42        }
43    }
44}
45
46impl<R> Commander<R> {
47    /// Parse a line of commands and updates the `Commander` state.
48    ///
49    /// Parsing a line is akin to sending an input line to the commander in the run loop.
50    /// Commands are space separated, and executed within this function, any actions that are specified will be invoked.
51    ///
52    /// Most branches result in a `LineResult::Continue` apart from an exit command which will result in a `LineResult::Exit`.
53    /// It is up to the developer to decide on the behaviour.
54    ///
55    /// # Example
56    /// ```rust
57    /// use cmdtree::*;
58    /// let mut cmder = Builder::default_config("base")
59    ///        .begin_class("one", "")
60    ///        .begin_class("two", "")
61    ///     .add_action("echo", "", |_wtr, args| println!("{}", args.join(" ")))
62    ///        .into_commander().unwrap();
63    ///
64    ///    assert_eq!(cmder.path(), "base");
65    ///    cmder.parse_line("one two", true,  &mut std::io::sink());
66    ///    assert_eq!(cmder.path(), "base.one.two");
67    /// cmder.parse_line("echo Hello, world!", true, &mut std::io::sink());    // should print "Hello, world!"
68    /// ```
69    pub fn parse_line<W: Write>(
70        &mut self,
71        line: &str,
72        colourise: bool,
73        writer: &mut W,
74    ) -> LineResult<R> {
75        let line = line.replace("\n", "").replace("\r", "");
76        let words: Vec<_> = line.trim().split(' ').collect();
77        let mut idx = 0;
78        let mut words_iter = words.iter();
79        let mut next_word = words_iter.next();
80
81        // if there is no current class, use the root
82        let start_class = Arc::clone(&self.current);
83        let start_path = self.path.clone();
84
85        while let Some(word) = next_word {
86            idx += 1;
87            next_word = match parse_word(&self.current, word) {
88                WordResult::Help(sc) => {
89                    if colourise {
90                        write_help_coloured(&sc, writer).expect("failed writing output to writer");
91                    } else {
92                        write_help(&sc, writer).expect("failed writing output to writer");
93                    }
94                    self.current = Arc::clone(&start_class);
95                    self.path = start_path;
96                    return LineResult::Help;
97                }
98                WordResult::Cancel => {
99                    self.current = Arc::clone(&self.root);
100                    self.path = self.root.name.clone();
101                    return LineResult::Cancel;
102                }
103                WordResult::Exit => {
104                    return LineResult::Exit;
105                }
106                WordResult::Class(sc) => {
107                    self.path.push_str(&format!("{}{}", PATH_SEP, sc.name));
108                    self.current = Arc::clone(&sc);
109                    words_iter.next()
110                }
111                WordResult::Action(a) => {
112                    let slice = &words[idx..];
113                    let r = a.call(writer, slice);
114                    self.current = Arc::clone(&start_class);
115                    self.path = start_path;
116                    return LineResult::Action(r);
117                }
118                WordResult::Unrecognized => {
119                    let mut s = format!(
120                        "'{}' does not match any keywords, classes, or actions",
121                        word
122                    )
123                    .bright_red();
124
125                    if !colourise {
126                        s = s.white();
127                    }
128
129                    writeln!(writer, "{}", s).expect("failed writing output to writer");
130                    self.current = Arc::clone(&start_class);
131                    self.path = start_path;
132                    return LineResult::Unrecognized;
133                }
134            };
135        }
136
137        LineResult::Class // default
138    }
139}
140
141fn parse_word<'a, R>(subclass: &'a SubClass<R>, word: &str) -> WordResult<'a, R> {
142    let lwr = word.to_lowercase();
143    match lwr.as_str() {
144        "help" => WordResult::Help(subclass),
145        "cancel" | "c" => WordResult::Cancel,
146        "exit" => WordResult::Exit,
147        word => {
148            if let Some(c) = subclass.classes.iter().find(|c| c.name.as_str() == word) {
149                WordResult::Class(c)
150            } else if let Some(a) = subclass.actions.iter().find(|a| a.name.as_str() == word) {
151                WordResult::Action(a)
152            } else {
153                WordResult::Unrecognized
154            }
155        }
156    }
157}
158
159fn write_help_coloured<W: Write, R>(class: &SubClass<R>, writer: &mut W) -> io::Result<()> {
160    writeln!(
161        writer,
162        "{} -- prints the help messages",
163        "help".bright_yellow()
164    )?;
165    writeln!(
166        writer,
167        "{} | {} -- returns to the root class",
168        "cancel".bright_yellow(),
169        "c".bright_yellow()
170    )?;
171    writeln!(
172        writer,
173        "{} -- sends the exit signal to end the interactive loop",
174        "exit".bright_yellow()
175    )?;
176    if !class.classes.is_empty() {
177        writeln!(writer, "{}", "Classes:".bright_purple())?;
178        for class in class.classes.iter() {
179            writeln!(writer, "\t{} -- {}", class.name.bright_yellow(), class.help)?;
180        }
181    }
182
183    if !class.actions.is_empty() {
184        writeln!(writer, "{}", "Actions:".bright_purple())?;
185        for action in class.actions.iter() {
186            writeln!(
187                writer,
188                "\t{} -- {}",
189                action.name.bright_yellow(),
190                action.help
191            )?;
192        }
193    }
194
195    Ok(())
196}
197
198fn write_help<W: Write, R>(class: &SubClass<R>, writer: &mut W) -> io::Result<()> {
199    writeln!(writer, "help -- prints the help messages",)?;
200    writeln!(writer, "cancel | c -- returns to the root class",)?;
201    writeln!(
202        writer,
203        "exit -- sends the exit signal to end the interactive loop",
204    )?;
205    if !class.classes.is_empty() {
206        writeln!(writer, "Classes:")?;
207        for class in class.classes.iter() {
208            writeln!(writer, "\t{} -- {}", class.name, class.help)?;
209        }
210    }
211
212    if !class.actions.is_empty() {
213        writeln!(writer, "Actions:")?;
214        for action in class.actions.iter() {
215            writeln!(writer, "\t{} -- {}", action.name, action.help)?;
216        }
217    }
218
219    Ok(())
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225
226    #[test]
227    fn parse_line_test() {
228        let mut cmder = Builder::default_config("test")
229            .begin_class("class1", "class1 help")
230            .begin_class("class1-class1", "adsf")
231            .add_action("action1", "adf", |_, _| ())
232            .end_class()
233            .begin_class("class1-class2", "adsf")
234            .add_action("action2", "adsf", |_, _| ())
235            .end_class()
236            .end_class()
237            .begin_class("class2", "asdf")
238            .end_class()
239            .add_action("test-args", "", |_wtr, args| {
240                assert_eq!(&args, &["one", "two", "three"])
241            })
242            .into_commander()
243            .unwrap();
244
245        let w = &mut std::io::sink();
246
247        assert_eq!(cmder.parse_line("adsf", true, w), LineResult::Unrecognized); // unrecognised branch
248        assert_eq!(cmder.current, cmder.root);
249        assert_eq!(cmder.parse_line("adsf", false, w), LineResult::Unrecognized); // unrecognised branch
250        assert_eq!(cmder.current, cmder.root);
251
252        assert_eq!(cmder.parse_line("class1", true, w), LineResult::Class);
253        assert_ne!(cmder.current, cmder.root);
254        assert_eq!(cmder.current.name, "class1");
255
256        // should be able to action here
257        assert_eq!(
258            cmder.parse_line("class1-class1 action1", true, w),
259            LineResult::Action(())
260        );
261        assert_eq!(cmder.current.name, "class1");
262        assert_eq!(
263            cmder.parse_line("class1-class2 action2", true, w),
264            LineResult::Action(())
265        );
266        assert_eq!(cmder.current.name, "class1");
267
268        // get back to root
269        assert_eq!(cmder.parse_line("cancel", true, w), LineResult::Cancel);
270        assert_eq!(cmder.current.name, "test");
271
272        // test args
273        assert_eq!(
274            cmder.parse_line("test-args one two three", true, w),
275            LineResult::Action(())
276        );
277        assert_eq!(cmder.current.name, "test");
278
279        // test help
280        assert_eq!(cmder.parse_line("help", true, w), LineResult::Help);
281        assert_eq!(cmder.current.name, "test");
282        assert_eq!(cmder.parse_line("help", false, w), LineResult::Help);
283        assert_eq!(cmder.current.name, "test");
284
285        // test exit
286        assert_eq!(cmder.parse_line("exit", true, w), LineResult::Exit);
287    }
288
289    #[test]
290    fn parse_word_test() {
291        let mut sc = SubClass::with_name("Class-Name", "help msg");
292        assert_eq!(parse_word(&sc, "HELP"), WordResult::Help(&sc));
293        assert_eq!(parse_word(&sc, "EXIT"), WordResult::Exit);
294        assert_eq!(parse_word(&sc, "CANCEL"), WordResult::Cancel);
295        assert_eq!(parse_word(&sc, "C"), WordResult::Cancel);
296        assert_eq!(parse_word(&sc, "asdf"), WordResult::Unrecognized);
297
298        sc.classes
299            .push(Arc::new(SubClass::with_name("name", "asdf")));
300        sc.actions.push(Action::blank_fn("action", "adsf"));
301        assert_eq!(parse_word(&sc, "NAME"), WordResult::Class(&sc.classes[0]));
302        assert_eq!(
303            parse_word(&sc, "aCtIoN"),
304            WordResult::Action(&sc.actions[0])
305        );
306    }
307
308    #[test]
309    fn write_help_coloured_test() {
310        let mut sc = SubClass::with_name("Class-Name", "root class");
311        sc.classes
312            .push(Arc::new(SubClass::with_name("class1", "class 1 help")));
313        sc.classes
314            .push(Arc::new(SubClass::with_name("class2", "class 2 help")));
315        sc.actions
316            .push(Action::blank_fn("action1", "action 1 help"));
317        sc.actions
318            .push(Action::blank_fn("action2", "action 2 help"));
319
320        let mut help = Vec::new();
321        write_help_coloured(&sc, &mut help).unwrap();
322        let help = String::from_utf8_lossy(&help);
323
324        assert_eq!(
325            &help,
326            &format!(
327                r#"{} -- prints the help messages
328{} | {} -- returns to the root class
329{} -- sends the exit signal to end the interactive loop
330{}
331	{} -- class 1 help
332	{} -- class 2 help
333{}
334	{} -- action 1 help
335	{} -- action 2 help
336"#,
337                "help".bright_yellow(),
338                "cancel".bright_yellow(),
339                "c".bright_yellow(),
340                "exit".bright_yellow(),
341                "Classes:".bright_purple(),
342                "class1".bright_yellow(),
343                "class2".bright_yellow(),
344                "Actions:".bright_purple(),
345                "action1".bright_yellow(),
346                "action2".bright_yellow()
347            )
348        );
349    }
350
351    #[test]
352    fn write_help_test() {
353        let mut sc = SubClass::with_name("Class-Name", "root class");
354        sc.classes
355            .push(Arc::new(SubClass::with_name("class1", "class 1 help")));
356        sc.classes
357            .push(Arc::new(SubClass::with_name("class2", "class 2 help")));
358        sc.actions
359            .push(Action::blank_fn("action1", "action 1 help"));
360        sc.actions
361            .push(Action::blank_fn("action2", "action 2 help"));
362
363        let mut help = Vec::new();
364        write_help(&sc, &mut help).unwrap();
365        let help = String::from_utf8_lossy(&help);
366
367        assert_eq!(
368            &help,
369            r#"help -- prints the help messages
370cancel | c -- returns to the root class
371exit -- sends the exit signal to end the interactive loop
372Classes:
373	class1 -- class 1 help
374	class2 -- class 2 help
375Actions:
376	action1 -- action 1 help
377	action2 -- action 2 help
378"#
379        );
380    }
381}