1//! `comma` parses command-line-style strings. See [`parse_command`] for details.
2
3use std::iter::{Peekable};
4use std::str::{Chars};
5
6fn parse_escape(chars: &mut Peekable<Chars>) -> Option<char> {
7 return Some(match chars.next()? {
8 'n' => '\n',
9 'r' => '\r',
10 't' => '\t',
11 literal: char => literal
12 })
13}
14
15fn parse_string(chars: &mut Peekable<Chars>, delim: char) -> Option<String> {
16 let mut output: String = String::new();
17
18 while let Some(ch: char) = chars.next() {
19 if ch == delim {
20 return Some(output)
21 } else if ch == '\\' {
22 output.push(ch:parse_escape(chars)?);
23 } else {
24 output.push(ch);
25 }
26 }
27
28 return None
29}
30
31/// Parses a command into a list of individual tokens.
32/// Each token is separated by one or more characters of whitespace.
33/// Pairs of single- or double-quotes can be used to ignore whitespace. Within pairs of quotation
34/// marks, a backslash (\) can be used to escape any character. The special escape sequences
35/// '\n', '\r', and '\t' are also handled as Newlines, Carriage Returns, and Tabs, respectively.
36/// Should a quotation mark be mismatched (no counterpart terminating mark exists), this function
37/// will return None. Otherwise, it returns a list of tokens in the input string.
38pub fn parse_command(input: &str) -> Option<Vec<String>> {
39 let mut next_push: bool = true;
40 let mut chars: impl Iterator = input.chars().peekable();
41 let mut output : Vec<String> = Vec::new();
42
43 while let Some(ch: char) = chars.next() {
44 if ch.is_whitespace() {
45 next_push = true;
46 } else{
47 if next_push { output.push(String::new()); next_push = false; }
48
49 if ch == '\\' {
50 output.last_mut()?.push(ch:parse_escape(&mut chars)?);
51 } else if ch == '"' || ch == '\'' {
52 output.last_mut()?.push_str(string:parse_string(&mut chars, delim:ch)?.as_str());
53 } else {
54 output.last_mut()?.push(ch);
55 }
56 }
57 }
58
59 return Some(output)
60}
61
62
63#[cfg(test)]
64mod tests {
65 use crate::{parse_command};
66
67 #[test]
68 fn parsing_works() {
69 let result = parse_command("hello world \\'this is\\' a \"quoted \\\"string\\\"\"").unwrap();
70 assert_eq!(result,
71 vec![String::from("hello"), String::from("world"),
72 String::from("'this"), String::from("is'"), String::from("a"),
73 String::from("quoted \"string\"")]);
74 }
75
76 #[test]
77 fn fail_mismatch() {
78 assert_eq!(parse_command("Hello 'world "), None);
79 }
80
81 #[test]
82 fn unicode() {
83 // This contains a CJK IDEOGRAPH EXTENSION G character, which is invisible.
84 let result = parse_command("ß 𱁬").unwrap();
85 assert_eq!(
86 result,
87 vec![String::from("ß"), String::from("𱁬")]
88 );
89 }
90}
91