1use alloc::boxed::Box;
2use alloc::collections::VecDeque;
3use alloc::format;
4use alloc::string::String;
5use alloc::vec::Vec;
6
7use core::pin::pin;
8use core::task::Poll;
9
10use crate::command::{Args, Command};
11use crate::error::Result;
12use crate::io::Io;
13use crate::keys::{Key, read_key};
14use crate::parse::{common_prefix, next_char_boundary, prev_char_boundary, tokenize};
15use crate::util::poll_fn;
16
17const BUILTINS: [(&str, &str); 3] = [
20 ("help", "list available commands"),
21 ("history", "show command history"),
22 ("clear", "clear the screen"),
23];
24
25#[derive(Debug, PartialEq, Eq)]
27enum ExecOutcome {
28 Done,
30 Eof,
32}
33
34enum Step {
36 Done(Result<()>),
37 Byte(u8),
38 Interrupted,
39 Eof,
40}
41
42pub struct Shell<'a> {
64 commands: Vec<Command<'a>>,
65 history: VecDeque<String>,
66 max_history: usize,
67 prompt: &'static str,
68}
69
70impl Default for Shell<'_> {
71 fn default() -> Self {
72 Self::new()
73 }
74}
75
76impl<'a> Shell<'a> {
77 pub fn new() -> Self {
79 Shell {
80 commands: Vec::new(),
81 history: VecDeque::new(),
82 max_history: 32,
83 prompt: "embassy> ",
84 }
85 }
86
87 pub fn prompt(&mut self, prompt: &'static str) -> &mut Self {
89 self.prompt = prompt;
90 self
91 }
92
93 pub fn max_history(&mut self, n: usize) -> &mut Self {
95 self.max_history = n;
96 self
97 }
98
99 pub fn add_command<F>(&mut self, name: &str, help: &'static str, handler: F)
118 where
119 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
120 {
121 self.push_command(Command {
122 name: String::from(name),
123 help,
124 handler: Box::new(handler),
125 completer: None,
126 });
127 }
128
129 pub fn add_command_with_options<F>(
132 &mut self,
133 name: &str,
134 help: &'static str,
135 options: &'static [&'static str],
136 handler: F,
137 ) where
138 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
139 {
140 self.push_command(Command {
141 name: String::from(name),
142 help,
143 handler: Box::new(handler),
144 completer: Some(Box::new(move |index: usize, prefix: &str| {
145 if index == 1 {
146 options
147 .iter()
148 .filter(|o| o.starts_with(prefix))
149 .map(|o| String::from(*o))
150 .collect()
151 } else {
152 Vec::new()
153 }
154 })),
155 });
156 }
157
158 pub fn add_command_with_completer<F, C>(
162 &mut self,
163 name: &str,
164 help: &'static str,
165 handler: F,
166 completer: C,
167 ) where
168 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
169 C: Fn(usize, &str) -> Vec<String> + 'a,
170 {
171 self.push_command(Command {
172 name: String::from(name),
173 help,
174 handler: Box::new(handler),
175 completer: Some(Box::new(completer)),
176 });
177 }
178
179 pub fn command_names(&self) -> impl Iterator<Item = &str> {
181 self.commands.iter().map(|c| c.name.as_str())
182 }
183
184 fn push_command(&mut self, cmd: Command<'a>) {
185 if let Some(existing) = self.commands.iter_mut().find(|c| c.name == cmd.name) {
186 *existing = Command {
187 name: cmd.name,
188 help: cmd.help,
189 handler: cmd.handler,
190 completer: cmd.completer,
191 };
192 } else {
193 self.commands.push(cmd);
194 }
195 }
196
197 pub async fn run<R, W>(&mut self, reader: &mut R, writer: &mut W) -> Result<()>
207 where
208 R: embedded_io_async::Read,
209 W: embedded_io_async::Write,
210 {
211 let mut pending: Option<u8> = None;
212 let mut buf = String::new();
213 let mut cursor: usize = 0;
214 let mut hist_idx: usize = 0;
215 let mut draft = String::new();
216
217 Io::new(writer).print(self.prompt).await?;
218
219 loop {
220 let Some(key) = read_key(reader, &mut pending).await? else {
221 return Ok(());
223 };
224
225 match key {
226 Key::Char(c) => {
227 let at_end = cursor == buf.len();
228 let mut tmp = [0u8; 4];
229 let s = c.encode_utf8(&mut tmp);
230 buf.insert_str(cursor, s);
231 cursor += s.len();
232 if at_end {
233 Io::new(writer).print(s).await?;
234 } else {
235 self.redraw(writer, &buf, cursor).await?;
236 }
237 }
238 Key::Enter => {
239 Io::new(writer).print("\r\n").await?;
240 let line = core::mem::take(&mut buf);
241 cursor = 0;
242 draft.clear();
243
244 if !line.trim().is_empty() {
245 self.push_history(&line);
246 let tokens = tokenize(&line);
247 match self.exec(&tokens, reader, &mut pending, writer).await? {
248 ExecOutcome::Done => {}
249 ExecOutcome::Eof => return Ok(()),
250 }
251 }
252 hist_idx = self.history.len();
253 Io::new(writer).print(self.prompt).await?;
254 }
255 Key::Backspace => {
256 if cursor > 0 {
257 let start = prev_char_boundary(&buf, cursor);
258 buf.replace_range(start..cursor, "");
259 cursor = start;
260 self.redraw(writer, &buf, cursor).await?;
261 }
262 }
263 Key::Delete => {
264 if cursor < buf.len() {
265 let end = next_char_boundary(&buf, cursor);
266 buf.replace_range(cursor..end, "");
267 self.redraw(writer, &buf, cursor).await?;
268 }
269 }
270 Key::Left => {
271 if cursor > 0 {
272 cursor = prev_char_boundary(&buf, cursor);
273 Io::new(writer).print("\x1b[D").await?;
274 }
275 }
276 Key::Right => {
277 if cursor < buf.len() {
278 cursor = next_char_boundary(&buf, cursor);
279 Io::new(writer).print("\x1b[C").await?;
280 }
281 }
282 Key::Home => {
283 if cursor > 0 {
284 let n = buf[..cursor].chars().count();
285 Io::new(writer).print(&format!("\x1b[{n}D")).await?;
286 cursor = 0;
287 }
288 }
289 Key::End => {
290 if cursor < buf.len() {
291 let n = buf[cursor..].chars().count();
292 Io::new(writer).print(&format!("\x1b[{n}C")).await?;
293 cursor = buf.len();
294 }
295 }
296 Key::Tab => {
297 self.complete(writer, &mut buf, &mut cursor).await?;
298 }
299 Key::Up => {
300 if !self.history.is_empty() {
301 if hist_idx == self.history.len() {
302 draft = buf.clone();
303 }
304 if hist_idx > 0 {
305 hist_idx -= 1;
306 buf = self.history[hist_idx].clone();
307 cursor = buf.len();
308 self.redraw(writer, &buf, cursor).await?;
309 }
310 }
311 }
312 Key::Down => {
313 if hist_idx < self.history.len() {
314 hist_idx += 1;
315 buf = if hist_idx == self.history.len() {
316 draft.clone()
317 } else {
318 self.history[hist_idx].clone()
319 };
320 cursor = buf.len();
321 self.redraw(writer, &buf, cursor).await?;
322 }
323 }
324 Key::CtrlC => {
325 buf.clear();
326 cursor = 0;
327 hist_idx = self.history.len();
328 Io::new(writer).print("^C\r\n").await?;
329 Io::new(writer).print(self.prompt).await?;
330 }
331 Key::CtrlU => {
332 buf.clear();
333 cursor = 0;
334 self.redraw(writer, &buf, cursor).await?;
335 }
336 Key::CtrlW => {
337 let end = buf.trim_end().len();
338 let start = buf[..end]
339 .rfind(char::is_whitespace)
340 .map(|i| i + 1)
341 .unwrap_or(0);
342 if start < end {
343 buf.replace_range(start..end, "");
344 cursor = start;
345 self.redraw(writer, &buf, cursor).await?;
346 }
347 }
348 Key::CtrlL => {
349 Io::new(writer).print("\x1b[2J\x1b[H").await?;
350 self.redraw(writer, &buf, cursor).await?;
351 }
352 }
353 }
354 }
355
356 fn push_history(&mut self, line: &str) {
357 if self.history.back().map(|h| h == line).unwrap_or(false) {
358 return;
359 }
360 self.history.push_back(String::from(line));
361 while self.history.len() > self.max_history {
362 self.history.pop_front();
363 }
364 }
365
366 async fn redraw<W>(&self, writer: &mut W, buf: &str, cursor: usize) -> Result<()>
368 where
369 W: embedded_io_async::Write,
370 {
371 let tail = buf[cursor..].chars().count();
372 let mut out = String::from("\r");
373 out.push_str(self.prompt);
374 out.push_str(buf);
375 out.push_str("\x1b[K");
377 if tail > 0 {
378 out.push_str(&format!("\x1b[{tail}D"));
379 }
380 Io::new(writer).print(&out).await
381 }
382
383 async fn complete<W>(&self, writer: &mut W, buf: &mut String, cursor: &mut usize) -> Result<()>
386 where
387 W: embedded_io_async::Write,
388 {
389 if *cursor != buf.len() {
391 return Ok(());
392 }
393
394 let ends_with_ws = buf.is_empty() || buf.ends_with(|c: char| c.is_whitespace());
395 let n_tokens = buf.split_whitespace().count();
396 let (prefix, tok_index) = if ends_with_ws {
397 ("", n_tokens)
398 } else {
399 (
400 buf.split_whitespace().last().unwrap_or(""),
401 n_tokens.saturating_sub(1),
402 )
403 };
404 let prefix_len = prefix.len();
405
406 let candidates: Vec<String> = if tok_index == 0 {
407 let mut cands: Vec<String> = self
408 .commands
409 .iter()
410 .filter(|c| c.name.starts_with(prefix))
411 .map(|c| c.name.clone())
412 .collect();
413 for (name, _) in BUILTINS {
414 if name.starts_with(prefix) && !self.commands.iter().any(|c| c.name == name) {
415 cands.push(String::from(name));
416 }
417 }
418 cands
419 } else {
420 let first = buf.split_whitespace().next().unwrap_or("");
421 match self
422 .commands
423 .iter()
424 .find(|c| c.name == first)
425 .and_then(|c| c.completer.as_ref())
426 {
427 Some(completer) => completer(tok_index, prefix),
428 None => Vec::new(),
429 }
430 };
431
432 if candidates.is_empty() {
433 return Ok(());
434 }
435
436 if candidates.len() == 1 {
437 buf.truncate(buf.len() - prefix_len);
438 buf.push_str(&candidates[0]);
439 buf.push(' ');
441 *cursor = buf.len();
442 return self.redraw(writer, buf, *cursor).await;
443 }
444
445 let refs: Vec<&str> = candidates.iter().map(|s| s.as_str()).collect();
447 let cp = common_prefix(&refs);
448 if cp.len() > prefix_len {
449 buf.truncate(buf.len() - prefix_len);
450 buf.push_str(cp);
451 *cursor = buf.len();
452 }
453
454 let mut io = Io::new(writer);
455 io.print("\r\n").await?;
456 for (i, cand) in candidates.iter().enumerate() {
457 if i > 0 {
458 io.print(" ").await?;
459 }
460 io.print(cand).await?;
461 }
462 io.print("\r\n").await?;
463 self.redraw(writer, buf, *cursor).await
464 }
465
466 async fn exec<R, W>(
467 &self,
468 tokens: &[String],
469 reader: &mut R,
470 pending: &mut Option<u8>,
471 writer: &mut W,
472 ) -> Result<ExecOutcome>
473 where
474 R: embedded_io_async::Read,
475 W: embedded_io_async::Write,
476 {
477 let name = tokens.first().map(String::as_str).unwrap_or("");
478
479 if let Some(cmd) = self.commands.iter().find(|c| c.name == name) {
481 return self.run_cmd(cmd, tokens, reader, pending, writer).await;
482 }
483
484 let mut io = Io::new(writer);
485 match name {
486 "help" => {
487 for cmd in &self.commands {
488 io.println(&format!(" {:<12}{}", cmd.name, cmd.help))
489 .await?;
490 }
491 for (bname, bhelp) in BUILTINS {
492 if !self.commands.iter().any(|c| c.name == bname) {
493 io.println(&format!(" {bname:<12}{bhelp}")).await?;
494 }
495 }
496 }
497 "clear" => {
498 io.print("\x1b[2J\x1b[H").await?;
499 }
500 "history" => {
501 for (i, entry) in self.history.iter().enumerate() {
502 io.println(&format!("{:>4} {}", i + 1, entry)).await?;
503 }
504 }
505 _ => {
506 io.println(&format!("{name}: command not found")).await?;
507 }
508 }
509 Ok(ExecOutcome::Done)
510 }
511
512 async fn run_cmd<R, W>(
514 &self,
515 cmd: &Command<'a>,
516 tokens: &[String],
517 reader: &mut R,
518 pending: &mut Option<u8>,
519 writer: &mut W,
520 ) -> Result<ExecOutcome>
521 where
522 R: embedded_io_async::Read,
523 W: embedded_io_async::Write,
524 {
525 let outcome = {
526 let io = Io::new(&mut *writer);
527 let fut = (cmd.handler)(Args::new(&tokens[1..]), io);
528 let mut fut = pin!(fut);
529
530 loop {
531 let byte_f = async {
532 let mut b = [0u8; 1];
533 match reader.read(&mut b).await {
534 Ok(0) => Step::Eof,
535 Ok(_) => Step::Byte(b[0]),
536 Err(_) => Step::Eof,
537 }
538 };
539 let mut byte_f = pin!(byte_f);
540
541 let step = poll_fn(|cx| {
542 if let Poll::Ready(r) = fut.as_mut().poll(cx) {
543 return Poll::Ready(Step::Done(r));
544 }
545 byte_f.as_mut().poll(cx)
546 })
547 .await;
548
549 match step {
550 Step::Byte(3) => break Step::Interrupted,
551 Step::Byte(b) => *pending = Some(b),
552 other => break other,
553 }
554 }
555 };
556
557 match outcome {
558 Step::Done(Ok(())) => {}
559 Step::Done(Err(e)) => {
560 Io::new(writer)
561 .println(&format!("{}: {}", cmd.name, e))
562 .await?;
563 }
564 Step::Interrupted => {
565 Io::new(writer).print("^C\r\n").await?;
566 }
567 Step::Eof => return Ok(ExecOutcome::Eof),
568 Step::Byte(_) => unreachable!(),
569 }
570 Ok(ExecOutcome::Done)
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577
578 #[derive(Debug)]
579 struct NoError;
580 impl core::fmt::Display for NoError {
581 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
582 write!(f, "NoError")
583 }
584 }
585 impl core::error::Error for NoError {}
586 impl embedded_io::Error for NoError {
587 fn kind(&self) -> embedded_io::ErrorKind {
588 embedded_io::ErrorKind::Other
589 }
590 }
591
592 struct SliceReader<'b> {
593 data: &'b [u8],
594 pos: usize,
595 }
596 impl embedded_io::ErrorType for SliceReader<'_> {
597 type Error = NoError;
598 }
599 impl embedded_io_async::Read for SliceReader<'_> {
600 async fn read(&mut self, buf: &mut [u8]) -> core::result::Result<usize, NoError> {
601 let n = buf.len().min(self.data.len() - self.pos);
602 buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
603 self.pos += n;
604 Ok(n)
605 }
606 }
607
608 #[derive(Default)]
609 struct VecWriter {
610 out: Vec<u8>,
611 }
612 impl embedded_io::ErrorType for VecWriter {
613 type Error = NoError;
614 }
615 impl embedded_io_async::Write for VecWriter {
616 async fn write(&mut self, buf: &[u8]) -> core::result::Result<usize, NoError> {
617 self.out.extend_from_slice(buf);
618 Ok(buf.len())
619 }
620 async fn flush(&mut self) -> core::result::Result<(), NoError> {
621 Ok(())
622 }
623 }
624
625 fn run(shell: &mut Shell<'_>, input: &[u8]) -> String {
626 let mut r = SliceReader {
627 data: input,
628 pos: 0,
629 };
630 let mut w = VecWriter::default();
631 futures::executor::block_on(shell.run(&mut r, &mut w)).unwrap();
632 String::from_utf8_lossy(&w.out).into_owned()
633 }
634
635 fn echo_cmd(shell: &mut Shell<'_>) {
636 shell.add_command("echo", "print arguments", |args, mut io| {
637 Box::pin(async move { io.println(&args.rest()).await })
638 });
639 }
640
641 #[test]
642 fn runs_command() {
643 let mut sh = Shell::new();
644 echo_cmd(&mut sh);
645 let out = run(&mut sh, b"echo hello world\r\n");
646 assert!(out.contains("hello world"), "{out}");
647 }
648
649 #[test]
650 fn crlf_and_quoted_args() {
651 let mut sh = Shell::new();
652 echo_cmd(&mut sh);
653 let out = run(&mut sh, b"echo \"one two\"\n");
654 assert!(out.contains("one two"), "{out}");
655 }
656
657 #[test]
658 fn unknown_command() {
659 let mut sh = Shell::new();
660 let out = run(&mut sh, b"foo bar\r\n");
661 assert!(out.contains("foo: command not found"), "{out}");
662 }
663
664 #[test]
665 fn tab_completes_command_uniquely() {
666 let mut sh = Shell::new();
667 echo_cmd(&mut sh);
668 let out = run(&mut sh, b"ec\t\r\n");
669 assert!(out.contains("embassy> echo "), "{out}");
671 assert!(!out.contains("command not found"), "{out}");
672 }
673
674 #[test]
675 fn tab_lists_ambiguous_candidates() {
676 let mut sh = Shell::new();
677 echo_cmd(&mut sh);
678 sh.add_command("edd", "", |_a, _io| Box::pin(core::future::ready(Ok(()))));
679 let out = run(&mut sh, b"e\t");
680 assert!(out.contains("echo"), "{out}");
681 assert!(out.contains("edd"), "{out}");
682 assert!(out.contains("embassy> e"), "{out}");
683 }
684
685 #[test]
686 fn tab_completes_options() {
687 let mut sh = Shell::new();
688 sh.add_command_with_options(
689 "led",
690 "led control",
691 &["on", "off", "blink"],
692 |args, mut io| Box::pin(async move { io.println(args.get(0).unwrap_or("?")).await }),
693 );
694 let out = run(&mut sh, b"led of\t\r\n");
696 assert!(out.contains("embassy> led off "), "{out}");
697 assert!(out.contains("\r\noff\r\n"), "{out}");
699 let out = run(&mut sh, b"led o\t");
701 assert!(out.contains("on") && out.contains("off"), "{out}");
702 assert!(out.contains("embassy> led o"), "{out}");
703 }
704
705 #[test]
706 fn history_recall() {
707 let mut sh = Shell::new();
708 echo_cmd(&mut sh);
709 let out = run(&mut sh, b"echo one\r\necho two\r\n\x1b[A\r\n");
710 assert_eq!(out.matches("\r\ntwo\r\n").count(), 2, "{out}");
712 assert_eq!(out.matches("\r\none\r\n").count(), 1, "{out}");
713 }
714
715 #[test]
716 fn history_builtin_lists() {
717 let mut sh = Shell::new();
718 echo_cmd(&mut sh);
719 let out = run(&mut sh, b"echo x\r\nhistory\r\n");
720 assert!(out.contains("echo x"), "{out}");
721 }
722
723 #[test]
724 fn ctrl_c_at_prompt_clears_line() {
725 let mut sh = Shell::new();
726 echo_cmd(&mut sh);
727 let out = run(&mut sh, b"garbage\x03echo ok\r\n");
728 assert!(out.contains("ok"), "{out}");
730 assert!(!out.contains("garbage: command not found"), "{out}");
731 }
732
733 #[test]
734 fn ctrl_c_interrupts_running_command() {
735 let mut sh = Shell::new();
736 sh.add_command("hang", "never returns", |_args, _io| {
737 Box::pin(core::future::pending::<crate::Result<()>>())
738 });
739 let out = run(&mut sh, b"hang\r\x03");
740 assert!(out.contains("^C"), "{out}");
741 }
742
743 #[test]
744 fn help_lists_commands_and_builtins() {
745 let mut sh = Shell::new();
746 echo_cmd(&mut sh);
747 let out = run(&mut sh, b"help\r\n");
748 assert!(out.contains("echo"), "{out}");
749 assert!(out.contains("list available commands"), "{out}");
750 assert!(out.contains("history"), "{out}");
751 }
752
753 #[test]
754 fn ctrl_u_kills_line() {
755 let mut sh = Shell::new();
756 echo_cmd(&mut sh);
757 let out = run(&mut sh, b"oops\x15echo fine\r\n");
758 assert!(out.contains("fine"), "{out}");
759 assert!(!out.contains("oops: command not found"), "{out}");
760 }
761
762 #[test]
763 fn left_right_editing() {
764 let mut sh = Shell::new();
765 echo_cmd(&mut sh);
766 let out = run(&mut sh, b"ech o\x1b[D\x7f\r\n");
771 assert!(!out.contains("command not found"), "{out}");
772 }
773}