1use alloc::boxed::Box;
2use alloc::collections::VecDeque;
3use alloc::string::String;
4use alloc::vec::Vec;
5
6use core::pin::pin;
7use core::task::Poll;
8
9use crate::command::{Args, Command, CompleterKind};
10use crate::error::Result;
11use crate::io::Io;
12use crate::keys::{Key, read_key};
13use crate::parse::{common_prefix, next_char_boundary, prev_char_boundary, tokenize_into};
14use crate::util::{StackWriter, poll_fn};
15
16const PADDING: &str = " ";
19
20fn cursor_move<const N: usize>(n: usize, dir: u8) -> StackWriter<N> {
22 let mut w = StackWriter::<N>::new();
23 w.push(0x1b);
24 w.push(b'[');
25 w.push_usize(n);
26 w.push(dir);
27 w
28}
29
30const BUILTINS: [(&str, &str); 3] = [
33 ("help", "list available commands"),
34 ("history", "show command history"),
35 ("clear", "clear the screen"),
36];
37
38#[derive(Debug, PartialEq, Eq)]
40enum ExecOutcome {
41 Done,
43 Eof,
45}
46
47enum Step {
49 Done(Result<()>),
50 Byte(u8),
51 Interrupted,
52 Eof,
53}
54
55pub struct Shell<'a> {
77 commands: Vec<Command<'a>>,
78 history: VecDeque<String>,
79 max_history: usize,
80 max_history_len: usize,
81 max_line_len: usize,
82 prompt: &'static str,
83}
84
85impl Default for Shell<'_> {
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91impl<'a> Shell<'a> {
92 pub fn new() -> Self {
94 Shell {
95 commands: Vec::new(),
96 history: VecDeque::new(),
97 max_history: 32,
98 max_history_len: 64,
99 max_line_len: 128,
100 prompt: "embassy> ",
101 }
102 }
103
104 pub fn prompt(&mut self, prompt: &'static str) -> &mut Self {
106 self.prompt = prompt;
107 self
108 }
109
110 pub fn max_history(&mut self, n: usize) -> &mut Self {
112 self.max_history = n;
113 self
114 }
115
116 pub fn max_history_len(&mut self, n: usize) -> &mut Self {
121 self.max_history_len = n;
122 self
123 }
124
125 pub fn max_line_len(&mut self, n: usize) -> &mut Self {
129 self.max_line_len = n;
130 self
131 }
132
133 pub fn add_command<F>(&mut self, name: &str, help: &'static str, handler: F)
152 where
153 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
154 {
155 self.push_command(Command {
156 name: String::from(name),
157 help,
158 handler: Box::new(handler),
159 completer: None,
160 });
161 }
162
163 pub fn add_command_with_options<F>(
166 &mut self,
167 name: &str,
168 help: &'static str,
169 options: &'static [&'static str],
170 handler: F,
171 ) where
172 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
173 {
174 self.push_command(Command {
175 name: String::from(name),
176 help,
177 handler: Box::new(handler),
178 completer: Some(CompleterKind::Options(options)),
179 });
180 }
181
182 pub fn add_command_with_completer<F, C>(
186 &mut self,
187 name: &str,
188 help: &'static str,
189 handler: F,
190 completer: C,
191 ) where
192 F: for<'x> Fn(Args<'x>, Io<'x>) -> crate::BoxFuture<'x, Result<()>> + 'a,
193 C: Fn(usize, &str) -> Vec<String> + 'a,
194 {
195 self.push_command(Command {
196 name: String::from(name),
197 help,
198 handler: Box::new(handler),
199 completer: Some(CompleterKind::Custom(Box::new(completer))),
200 });
201 }
202
203 pub fn command_names(&self) -> impl Iterator<Item = &str> {
205 self.commands.iter().map(|c| c.name.as_str())
206 }
207
208 fn push_command(&mut self, cmd: Command<'a>) {
209 if let Some(existing) = self.commands.iter_mut().find(|c| c.name == cmd.name) {
210 *existing = Command {
211 name: cmd.name,
212 help: cmd.help,
213 handler: cmd.handler,
214 completer: cmd.completer,
215 };
216 } else {
217 self.commands.push(cmd);
218 }
219 }
220
221 pub async fn run<R, W>(&mut self, reader: &mut R, writer: &mut W) -> Result<()>
231 where
232 R: embedded_io_async::Read,
233 W: embedded_io_async::Write,
234 {
235 let mut pending: Option<u8> = None;
236 let mut buf = String::new();
237 let mut cursor: usize = 0;
238 let mut hist_idx: usize = 0;
239 let mut draft = String::new();
240 let mut tokens: Vec<String> = Vec::new();
243
244 Io::new(writer).print(self.prompt).await?;
245
246 loop {
247 let Some(key) = read_key(reader, &mut pending).await? else {
248 log!("input eof");
250 return Ok(());
251 };
252 log!("key: {}", key);
253
254 match key {
255 Key::Char(c) => {
256 if buf.len() + c.len_utf8() > self.max_line_len {
257 Io::new(writer).print("\x07").await?;
259 } else {
260 let at_end = cursor == buf.len();
261 let mut tmp = [0u8; 4];
262 let s = c.encode_utf8(&mut tmp);
263 buf.insert_str(cursor, s);
264 cursor += s.len();
265 if at_end {
266 Io::new(writer).print(s).await?;
267 } else {
268 self.redraw(writer, &buf, cursor).await?;
269 }
270 }
271 }
272 Key::Enter => {
273 Io::new(writer).print("\r\n").await?;
274
275 if !buf.trim().is_empty() {
276 log!("line: {}", buf.as_str());
277 self.push_history(&buf);
278 let n = tokenize_into(&buf, &mut tokens);
279 match self
280 .exec(&tokens[..n], reader, &mut pending, writer)
281 .await?
282 {
283 ExecOutcome::Done => {}
284 ExecOutcome::Eof => return Ok(()),
285 }
286 }
287 buf.clear();
290 cursor = 0;
291 draft.clear();
292 hist_idx = self.history.len();
293 Io::new(writer).print(self.prompt).await?;
294 }
295 Key::Backspace => {
296 if cursor > 0 {
297 let start = prev_char_boundary(&buf, cursor);
298 buf.replace_range(start..cursor, "");
299 cursor = start;
300 self.redraw(writer, &buf, cursor).await?;
301 }
302 }
303 Key::Delete => {
304 if cursor < buf.len() {
305 let end = next_char_boundary(&buf, cursor);
306 buf.replace_range(cursor..end, "");
307 self.redraw(writer, &buf, cursor).await?;
308 }
309 }
310 Key::Left => {
311 if cursor > 0 {
312 cursor = prev_char_boundary(&buf, cursor);
313 Io::new(writer).print("\x1b[D").await?;
314 }
315 }
316 Key::Right => {
317 if cursor < buf.len() {
318 cursor = next_char_boundary(&buf, cursor);
319 Io::new(writer).print("\x1b[C").await?;
320 }
321 }
322 Key::Home => {
323 if cursor > 0 {
324 let n = buf[..cursor].chars().count();
325 Io::new(writer)
326 .write_all(cursor_move::<16>(n, b'D').as_bytes())
327 .await?;
328 cursor = 0;
329 }
330 }
331 Key::End => {
332 if cursor < buf.len() {
333 let n = buf[cursor..].chars().count();
334 Io::new(writer)
335 .write_all(cursor_move::<16>(n, b'C').as_bytes())
336 .await?;
337 cursor = buf.len();
338 }
339 }
340 Key::Tab => {
341 self.complete(writer, &mut buf, &mut cursor).await?;
342 }
343 Key::Up => {
344 if !self.history.is_empty() {
345 if hist_idx == self.history.len() {
346 draft = buf.clone();
347 }
348 if hist_idx > 0 {
349 hist_idx -= 1;
350 buf = self.history[hist_idx].clone();
351 cursor = buf.len();
352 self.redraw(writer, &buf, cursor).await?;
353 }
354 }
355 }
356 Key::Down => {
357 if hist_idx < self.history.len() {
358 hist_idx += 1;
359 buf = if hist_idx == self.history.len() {
360 draft.clone()
361 } else {
362 self.history[hist_idx].clone()
363 };
364 cursor = buf.len();
365 self.redraw(writer, &buf, cursor).await?;
366 }
367 }
368 Key::CtrlC => {
369 buf.clear();
370 cursor = 0;
371 hist_idx = self.history.len();
372 Io::new(writer).print("^C\r\n").await?;
373 Io::new(writer).print(self.prompt).await?;
374 }
375 Key::CtrlU => {
376 buf.clear();
377 cursor = 0;
378 self.redraw(writer, &buf, cursor).await?;
379 }
380 Key::CtrlW => {
381 let end = buf.trim_end().len();
382 let start = buf[..end]
383 .rfind(char::is_whitespace)
384 .map(|i| i + 1)
385 .unwrap_or(0);
386 if start < end {
387 buf.replace_range(start..end, "");
388 cursor = start;
389 self.redraw(writer, &buf, cursor).await?;
390 }
391 }
392 Key::CtrlL => {
393 Io::new(writer).print("\x1b[2J\x1b[H").await?;
394 self.redraw(writer, &buf, cursor).await?;
395 }
396 }
397 }
398 }
399
400 fn push_history(&mut self, line: &str) {
401 let mut end = line.len().min(self.max_history_len);
403 while end > 0 && !line.is_char_boundary(end) {
404 end -= 1;
405 }
406 let line = &line[..end];
407 if self.history.back().map(|h| h == line).unwrap_or(false) {
408 return;
409 }
410 self.history.push_back(String::from(line));
411 while self.history.len() > self.max_history {
412 self.history.pop_front();
413 }
414 }
415
416 async fn redraw<W>(&self, writer: &mut W, buf: &str, cursor: usize) -> Result<()>
418 where
419 W: embedded_io_async::Write,
420 {
421 let mut io = Io::new(writer);
423 io.print("\r").await?;
424 io.print(self.prompt).await?;
425 io.print(buf).await?;
426 io.print("\x1b[K").await?;
428 let tail = buf[cursor..].chars().count();
429 if tail > 0 {
430 io.write_all(cursor_move::<16>(tail, b'D').as_bytes())
431 .await?;
432 }
433 Ok(())
434 }
435
436 async fn complete<W>(&self, writer: &mut W, buf: &mut String, cursor: &mut usize) -> Result<()>
439 where
440 W: embedded_io_async::Write,
441 {
442 if *cursor != buf.len() {
444 return Ok(());
445 }
446
447 let ends_with_ws = buf.is_empty() || buf.ends_with(|c: char| c.is_whitespace());
448 let n_tokens = buf.split_whitespace().count();
449 let (prefix, tok_index) = if ends_with_ws {
450 ("", n_tokens)
451 } else {
452 (
453 buf.split_whitespace().last().unwrap_or(""),
454 n_tokens.saturating_sub(1),
455 )
456 };
457 let prefix_len = prefix.len();
458
459 let owned: Vec<String>;
462 let candidates: Vec<&str> = if tok_index == 0 {
463 let mut cands: Vec<&str> = self
464 .commands
465 .iter()
466 .filter(|c| c.name.starts_with(prefix))
467 .map(|c| c.name.as_str())
468 .collect();
469 for (name, _) in BUILTINS {
470 if name.starts_with(prefix) && !self.commands.iter().any(|c| c.name == name) {
471 cands.push(name);
472 }
473 }
474 cands
475 } else {
476 let first = buf.split_whitespace().next().unwrap_or("");
477 match self
478 .commands
479 .iter()
480 .find(|c| c.name == first)
481 .and_then(|c| c.completer.as_ref())
482 {
483 Some(CompleterKind::Options(options)) => options
484 .iter()
485 .filter(|o| o.starts_with(prefix))
486 .copied()
487 .collect(),
488 Some(CompleterKind::Custom(completer)) => {
489 owned = completer(tok_index, prefix);
490 owned.iter().map(String::as_str).collect()
491 }
492 None => Vec::new(),
493 }
494 };
495
496 log!("completion: {} candidates for {}", candidates.len(), prefix);
497 if candidates.is_empty() {
498 return Ok(());
499 }
500
501 if candidates.len() == 1 {
502 buf.truncate(buf.len() - prefix_len);
503 buf.push_str(candidates[0]);
504 buf.push(' ');
506 *cursor = buf.len();
507 return self.redraw(writer, buf, *cursor).await;
508 }
509
510 let cp = common_prefix(&candidates);
512 if cp.len() > prefix_len {
513 buf.truncate(buf.len() - prefix_len);
514 buf.push_str(cp);
515 *cursor = buf.len();
516 }
517
518 let mut io = Io::new(writer);
519 io.print("\r\n").await?;
520 for (i, &cand) in candidates.iter().enumerate() {
521 if i > 0 {
522 io.print(" ").await?;
523 }
524 io.print(cand).await?;
525 }
526 io.print("\r\n").await?;
527 self.redraw(writer, buf, *cursor).await
528 }
529
530 async fn exec<R, W>(
531 &self,
532 tokens: &[String],
533 reader: &mut R,
534 pending: &mut Option<u8>,
535 writer: &mut W,
536 ) -> Result<ExecOutcome>
537 where
538 R: embedded_io_async::Read,
539 W: embedded_io_async::Write,
540 {
541 let name = tokens.first().map(String::as_str).unwrap_or("");
542
543 if let Some(cmd) = self.commands.iter().find(|c| c.name == name) {
545 return self.run_cmd(cmd, tokens, reader, pending, writer).await;
546 }
547
548 let mut io = Io::new(writer);
549 match name {
550 "help" => {
551 for cmd in &self.commands {
552 let pad = &PADDING[..12usize.saturating_sub(cmd.name.chars().count())];
553 io.print(" ").await?;
554 io.print(&cmd.name).await?;
555 io.print(pad).await?;
556 io.println(cmd.help).await?;
557 }
558 for (bname, bhelp) in BUILTINS {
559 if !self.commands.iter().any(|c| c.name == bname) {
560 let pad = &PADDING[..12usize.saturating_sub(bname.chars().count())];
561 io.print(" ").await?;
562 io.print(bname).await?;
563 io.print(pad).await?;
564 io.println(bhelp).await?;
565 }
566 }
567 }
568 "clear" => {
569 io.print("\x1b[2J\x1b[H").await?;
570 }
571 "history" => {
572 for (i, entry) in self.history.iter().enumerate() {
573 let mut w = StackWriter::<8>::new();
574 w.push_usize(i + 1);
575 let num = w.as_bytes();
576 io.print(&PADDING[..4usize.saturating_sub(num.len())])
577 .await?;
578 io.write_all(num).await?;
579 io.print(" ").await?;
580 io.println(entry).await?;
581 }
582 }
583 _ => {
584 log!("unknown command: {}", name);
585 io.print(name).await?;
586 io.println(": command not found").await?;
587 }
588 }
589 Ok(ExecOutcome::Done)
590 }
591
592 async fn run_cmd<R, W>(
594 &self,
595 cmd: &Command<'a>,
596 tokens: &[String],
597 reader: &mut R,
598 pending: &mut Option<u8>,
599 writer: &mut W,
600 ) -> Result<ExecOutcome>
601 where
602 R: embedded_io_async::Read,
603 W: embedded_io_async::Write,
604 {
605 let outcome = {
606 let io = Io::new(&mut *writer);
607 let fut = (cmd.handler)(Args::new(&tokens[1..]), io);
608 let mut fut = pin!(fut);
609
610 loop {
611 let byte_f = async {
612 let mut b = [0u8; 1];
613 match reader.read(&mut b).await {
614 Ok(0) => Step::Eof,
615 Ok(_) => Step::Byte(b[0]),
616 Err(_) => Step::Eof,
617 }
618 };
619 let mut byte_f = pin!(byte_f);
620
621 let step = poll_fn(|cx| {
622 if let Poll::Ready(r) = fut.as_mut().poll(cx) {
623 return Poll::Ready(Step::Done(r));
624 }
625 byte_f.as_mut().poll(cx)
626 })
627 .await;
628
629 match step {
630 Step::Byte(3) => break Step::Interrupted,
631 Step::Byte(b) => *pending = Some(b),
632 other => break other,
633 }
634 }
635 };
636
637 match outcome {
638 Step::Done(Ok(())) => {}
639 Step::Done(Err(e)) => {
640 let mut io = Io::new(writer);
641 io.print(&cmd.name).await?;
642 io.print(": ").await?;
643 io.println(e.message()).await?;
644 }
645 Step::Interrupted => {
646 log!("{}: interrupted", cmd.name.as_str());
647 Io::new(writer).print("^C\r\n").await?;
648 }
649 Step::Eof => return Ok(ExecOutcome::Eof),
650 Step::Byte(_) => unreachable!(),
651 }
652 Ok(ExecOutcome::Done)
653 }
654}
655
656#[cfg(test)]
657mod tests {
658 use super::*;
659
660 #[derive(Debug)]
661 struct NoError;
662 impl core::fmt::Display for NoError {
663 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
664 write!(f, "NoError")
665 }
666 }
667 impl core::error::Error for NoError {}
668 impl embedded_io::Error for NoError {
669 fn kind(&self) -> embedded_io::ErrorKind {
670 embedded_io::ErrorKind::Other
671 }
672 }
673
674 struct SliceReader<'b> {
675 data: &'b [u8],
676 pos: usize,
677 }
678 impl embedded_io::ErrorType for SliceReader<'_> {
679 type Error = NoError;
680 }
681 impl embedded_io_async::Read for SliceReader<'_> {
682 async fn read(&mut self, buf: &mut [u8]) -> core::result::Result<usize, NoError> {
683 let n = buf.len().min(self.data.len() - self.pos);
684 buf[..n].copy_from_slice(&self.data[self.pos..self.pos + n]);
685 self.pos += n;
686 Ok(n)
687 }
688 }
689
690 #[derive(Default)]
691 struct VecWriter {
692 out: Vec<u8>,
693 }
694 impl embedded_io::ErrorType for VecWriter {
695 type Error = NoError;
696 }
697 impl embedded_io_async::Write for VecWriter {
698 async fn write(&mut self, buf: &[u8]) -> core::result::Result<usize, NoError> {
699 self.out.extend_from_slice(buf);
700 Ok(buf.len())
701 }
702 async fn flush(&mut self) -> core::result::Result<(), NoError> {
703 Ok(())
704 }
705 }
706
707 fn run(shell: &mut Shell<'_>, input: &[u8]) -> String {
708 let mut r = SliceReader {
709 data: input,
710 pos: 0,
711 };
712 let mut w = VecWriter::default();
713 futures::executor::block_on(shell.run(&mut r, &mut w)).unwrap();
714 String::from_utf8_lossy(&w.out).into_owned()
715 }
716
717 fn echo_cmd(shell: &mut Shell<'_>) {
718 shell.add_command("echo", "print arguments", |args, mut io| {
719 Box::pin(async move { io.println(&args.rest()).await })
720 });
721 }
722
723 #[test]
724 fn runs_command() {
725 let mut sh = Shell::new();
726 echo_cmd(&mut sh);
727 let out = run(&mut sh, b"echo hello world\r\n");
728 assert!(out.contains("hello world"), "{out}");
729 }
730
731 #[test]
732 fn crlf_and_quoted_args() {
733 let mut sh = Shell::new();
734 echo_cmd(&mut sh);
735 let out = run(&mut sh, b"echo \"one two\"\n");
736 assert!(out.contains("one two"), "{out}");
737 }
738
739 #[test]
740 fn unknown_command() {
741 let mut sh = Shell::new();
742 let out = run(&mut sh, b"foo bar\r\n");
743 assert!(out.contains("foo: command not found"), "{out}");
744 }
745
746 #[test]
747 fn tab_completes_command_uniquely() {
748 let mut sh = Shell::new();
749 echo_cmd(&mut sh);
750 let out = run(&mut sh, b"ec\t\r\n");
751 assert!(out.contains("embassy> echo "), "{out}");
753 assert!(!out.contains("command not found"), "{out}");
754 }
755
756 #[test]
757 fn tab_lists_ambiguous_candidates() {
758 let mut sh = Shell::new();
759 echo_cmd(&mut sh);
760 sh.add_command("edd", "", |_a, _io| Box::pin(core::future::ready(Ok(()))));
761 let out = run(&mut sh, b"e\t");
762 assert!(out.contains("echo"), "{out}");
763 assert!(out.contains("edd"), "{out}");
764 assert!(out.contains("embassy> e"), "{out}");
765 }
766
767 #[test]
768 fn tab_completes_options() {
769 let mut sh = Shell::new();
770 sh.add_command_with_options(
771 "led",
772 "led control",
773 &["on", "off", "blink"],
774 |args, mut io| Box::pin(async move { io.println(args.get(0).unwrap_or("?")).await }),
775 );
776 let out = run(&mut sh, b"led of\t\r\n");
778 assert!(out.contains("embassy> led off "), "{out}");
779 assert!(out.contains("\r\noff\r\n"), "{out}");
781 let out = run(&mut sh, b"led o\t");
783 assert!(out.contains("on") && out.contains("off"), "{out}");
784 assert!(out.contains("embassy> led o"), "{out}");
785 }
786
787 #[test]
788 fn history_recall() {
789 let mut sh = Shell::new();
790 echo_cmd(&mut sh);
791 let out = run(&mut sh, b"echo one\r\necho two\r\n\x1b[A\r\n");
792 assert_eq!(out.matches("\r\ntwo\r\n").count(), 2, "{out}");
794 assert_eq!(out.matches("\r\none\r\n").count(), 1, "{out}");
795 }
796
797 #[test]
798 fn history_builtin_lists() {
799 let mut sh = Shell::new();
800 echo_cmd(&mut sh);
801 let out = run(&mut sh, b"echo x\r\nhistory\r\n");
802 assert!(out.contains("echo x"), "{out}");
803 }
804
805 #[test]
806 fn ctrl_c_at_prompt_clears_line() {
807 let mut sh = Shell::new();
808 echo_cmd(&mut sh);
809 let out = run(&mut sh, b"garbage\x03echo ok\r\n");
810 assert!(out.contains("ok"), "{out}");
812 assert!(!out.contains("garbage: command not found"), "{out}");
813 }
814
815 #[test]
816 fn ctrl_c_interrupts_running_command() {
817 let mut sh = Shell::new();
818 sh.add_command("hang", "never returns", |_args, _io| {
819 Box::pin(core::future::pending::<crate::Result<()>>())
820 });
821 let out = run(&mut sh, b"hang\r\x03");
822 assert!(out.contains("^C"), "{out}");
823 }
824
825 #[test]
826 fn help_lists_commands_and_builtins() {
827 let mut sh = Shell::new();
828 echo_cmd(&mut sh);
829 let out = run(&mut sh, b"help\r\n");
830 assert!(out.contains("echo"), "{out}");
831 assert!(out.contains("list available commands"), "{out}");
832 assert!(out.contains("history"), "{out}");
833 }
834
835 #[test]
836 fn ctrl_u_kills_line() {
837 let mut sh = Shell::new();
838 echo_cmd(&mut sh);
839 let out = run(&mut sh, b"oops\x15echo fine\r\n");
840 assert!(out.contains("fine"), "{out}");
841 assert!(!out.contains("oops: command not found"), "{out}");
842 }
843
844 #[test]
845 fn left_right_editing() {
846 let mut sh = Shell::new();
847 echo_cmd(&mut sh);
848 let out = run(&mut sh, b"ech o\x1b[D\x7f\r\n");
853 assert!(!out.contains("command not found"), "{out}");
854 }
855
856 #[test]
857 fn line_cap_rejects_with_bell() {
858 let mut sh = Shell::new();
859 sh.max_line_len(5);
860 echo_cmd(&mut sh);
861 let out = run(&mut sh, b"echo hello\r\n");
864 assert!(out.contains('\u{7}'), "{out:?}");
865 assert!(!out.contains("hello"), "{out:?}");
866 assert!(!out.contains("command not found"), "{out:?}");
867 }
868
869 #[test]
870 fn history_entry_truncated() {
871 let mut sh = Shell::new();
872 sh.max_history_len(4);
873 let out = run(&mut sh, b"help XXXXXXXXXXXX\nhistory\r\n");
876 assert!(out.contains(" 1 help\r\n"), "{out:?}");
878 assert!(!out.contains(" 1 help "), "{out:?}");
879 }
880}