nostd_interactive_terminal/
parser.rs

1use heapless::{String, Vec};
2
3/// A parsed command with its arguments
4#[derive(Debug, Clone)]
5pub struct ParsedCommand<const MAX_ARGS: usize, const BUF_SIZE: usize> {
6    /// The command name
7    pub command: String<BUF_SIZE>,
8    /// Command arguments
9    pub args: Vec<String<BUF_SIZE>, MAX_ARGS>,
10}
11
12impl<const MAX_ARGS: usize, const BUF_SIZE: usize> ParsedCommand<MAX_ARGS, BUF_SIZE> {
13    /// Get the command name
14    pub fn name(&self) -> &str {
15        &self.command
16    }
17
18    /// Get the number of arguments
19    pub fn arg_count(&self) -> usize {
20        self.args.len()
21    }
22
23    /// Get an argument by index
24    pub fn arg(&self, index: usize) -> Option<&str> {
25        self.args.get(index).map(|s| s.as_str())
26    }
27
28    /// Get all arguments joined by a separator
29    pub fn args_joined(&self, separator: &str) -> Option<String<BUF_SIZE>> {
30        if self.args.is_empty() {
31            return Some(String::new());
32        }
33
34        let mut result = String::new();
35        for (i, arg) in self.args.iter().enumerate() {
36            if i > 0 {
37                result.push_str(separator).ok()?;
38            }
39            result.push_str(arg).ok()?;
40        }
41        Some(result)
42    }
43
44    /// Get the entire command line (command + args)
45    pub fn full_command(&self) -> Option<String<BUF_SIZE>> {
46        let mut result = self.command.clone();
47        for arg in &self.args {
48            result.push(' ').ok()?;
49            result.push_str(arg).ok()?;
50        }
51        Some(result)
52    }
53}
54
55/// Command parser for splitting input into command and arguments
56pub struct CommandParser;
57
58impl CommandParser {
59    /// Parse a command line into command and arguments
60    ///
61    /// Supports basic quote handling for arguments with spaces.
62    pub fn parse<const MAX_ARGS: usize, const BUF_SIZE: usize>(
63        input: &str,
64    ) -> Result<ParsedCommand<MAX_ARGS, BUF_SIZE>, ParseError> {
65        let trimmed = input.trim();
66        if trimmed.is_empty() {
67            return Err(ParseError::EmptyInput);
68        }
69
70        let mut parts = Vec::<String<BUF_SIZE>, MAX_ARGS>::new();
71        let mut current = String::<BUF_SIZE>::new();
72        let mut in_quotes = false;
73        let mut chars = trimmed.chars().peekable();
74
75        while let Some(c) = chars.next() {
76            match c {
77                '"' => {
78                    in_quotes = !in_quotes;
79                }
80                ' ' if !in_quotes => {
81                    if !current.is_empty() {
82                        parts.push(current.clone()).map_err(|_| ParseError::TooManyArgs)?;
83                        current.clear();
84                    }
85                }
86                _ => {
87                    current.push(c).map_err(|_| ParseError::ArgTooLong)?;
88                }
89            }
90        }
91
92        // Push final argument
93        if !current.is_empty() {
94            parts.push(current).map_err(|_| ParseError::TooManyArgs)?;
95        }
96
97        if parts.is_empty() {
98            return Err(ParseError::EmptyInput);
99        }
100
101        let command = parts.remove(0);
102        let args = parts;
103
104        Ok(ParsedCommand { command, args })
105    }
106
107    /// Simple split on whitespace (faster but no quote support)
108    pub fn parse_simple<const MAX_ARGS: usize, const BUF_SIZE: usize>(
109        input: &str,
110    ) -> Result<ParsedCommand<MAX_ARGS, BUF_SIZE>, ParseError> {
111        let trimmed = input.trim();
112        if trimmed.is_empty() {
113            return Err(ParseError::EmptyInput);
114        }
115
116        let mut parts = Vec::<String<BUF_SIZE>, MAX_ARGS>::new();
117        
118        for part in trimmed.split_whitespace() {
119            let s = String::<BUF_SIZE>::try_from(part).map_err(|_| ParseError::ArgTooLong)?;
120            parts.push(s).map_err(|_| ParseError::TooManyArgs)?;
121        }
122
123        if parts.is_empty() {
124            return Err(ParseError::EmptyInput);
125        }
126
127        let command = parts.remove(0);
128        let args = parts;
129
130        Ok(ParsedCommand { command, args })
131    }
132
133    /// Parse with a maximum number of splits (remaining text goes into last arg)
134    pub fn parse_max_split<const MAX_ARGS: usize, const BUF_SIZE: usize>(
135        input: &str,
136        max_splits: usize,
137    ) -> Result<ParsedCommand<MAX_ARGS, BUF_SIZE>, ParseError> {
138        let trimmed = input.trim();
139        if trimmed.is_empty() {
140            return Err(ParseError::EmptyInput);
141        }
142
143        let mut parts = Vec::<String<BUF_SIZE>, MAX_ARGS>::new();
144        let mut split_count = 0;
145        let mut remaining = trimmed;
146
147        while split_count < max_splits {
148            if let Some(pos) = remaining.find(' ') {
149                let (part, rest) = remaining.split_at(pos);
150                let s = String::<BUF_SIZE>::try_from(part.trim()).map_err(|_| ParseError::ArgTooLong)?;
151                parts.push(s).map_err(|_| ParseError::TooManyArgs)?;
152                remaining = rest.trim_start();
153                split_count += 1;
154            } else {
155                break;
156            }
157        }
158
159        // Add remaining as final argument
160        if !remaining.is_empty() {
161            let s = String::<BUF_SIZE>::try_from(remaining).map_err(|_| ParseError::ArgTooLong)?;
162            parts.push(s).map_err(|_| ParseError::TooManyArgs)?;
163        }
164
165        if parts.is_empty() {
166            return Err(ParseError::EmptyInput);
167        }
168
169        let command = parts.remove(0);
170        let args = parts;
171
172        Ok(ParsedCommand { command, args })
173    }
174}
175
176/// Errors that can occur during parsing
177#[derive(Debug, Clone, Copy, PartialEq)]
178pub enum ParseError {
179    EmptyInput,
180    TooManyArgs,
181    ArgTooLong,
182    UnclosedQuote,
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_parse_simple_command() {
191        let parsed: ParsedCommand<8, 64> = CommandParser::parse_simple("hello").unwrap();
192        assert_eq!(parsed.name(), "hello");
193        assert_eq!(parsed.arg_count(), 0);
194    }
195
196    #[test]
197    fn test_parse_with_args() {
198        let parsed: ParsedCommand<8, 64> =
199            CommandParser::parse_simple("send 192.168.1.1 message").unwrap();
200        assert_eq!(parsed.name(), "send");
201        assert_eq!(parsed.arg_count(), 2);
202        assert_eq!(parsed.arg(0), Some("192.168.1.1"));
203        assert_eq!(parsed.arg(1), Some("message"));
204    }
205
206    #[test]
207    fn test_parse_with_quotes() {
208        let parsed: ParsedCommand<8, 64> =
209            CommandParser::parse(r#"send peer "hello world""#).unwrap();
210        assert_eq!(parsed.name(), "send");
211        assert_eq!(parsed.arg_count(), 2);
212        assert_eq!(parsed.arg(1), Some("hello world"));
213    }
214
215    #[test]
216    fn test_parse_max_split() {
217        let parsed: ParsedCommand<8, 128> =
218            CommandParser::parse_max_split("broadcast this is a long message", 1).unwrap();
219        assert_eq!(parsed.name(), "broadcast");
220        assert_eq!(parsed.arg_count(), 1);
221        assert_eq!(parsed.arg(0), Some("this is a long message"));
222    }
223}