Skip to main content

embassy_shell/
shell.rs

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
16/// Used to emulate `{:<12}` / `{:>4}` padding in `help`/`history` without
17/// allocating via `format!`.
18const PADDING: &str = "            ";
19
20/// Build an ANSI cursor-move sequence `ESC [ n dir` without `core::fmt`.
21fn 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
30/// Commands handled by the shell itself. Shown in `help` and completed at the
31/// prompt unless a user command shadows one of them.
32const BUILTINS: [(&str, &str); 3] = [
33    ("help", "list available commands"),
34    ("history", "show command history"),
35    ("clear", "clear the screen"),
36];
37
38/// What happened while a command was running.
39#[derive(Debug, PartialEq, Eq)]
40enum ExecOutcome {
41    /// Command finished, was interrupted, or produced an error.
42    Done,
43    /// The input reached end-of-file.
44    Eof,
45}
46
47/// Result of racing a command future against the input stream.
48enum Step {
49    Done(Result<()>),
50    Byte(u8),
51    Interrupted,
52    Eof,
53}
54
55/// The shell.
56///
57/// The type parameter `'a` is the lifetime of state captured by command
58/// handlers registered with [`Shell::add_command`].
59///
60/// # Example
61///
62/// ```
63/// use embassy_shell::Shell;
64///
65/// let mut shell = Shell::new();
66/// shell.add_command("echo", "print arguments", |args, mut io| {
67///     Box::pin(async move { io.println(&args.rest()).await })
68/// });
69///
70/// let mut input: &[u8] = b"echo hello world\r\n";
71/// let mut output: Vec<u8> = Vec::new();
72/// futures::executor::block_on(shell.run(&mut input, &mut output)).unwrap();
73///
74/// assert!(String::from_utf8_lossy(&output).contains("hello world"));
75/// ```
76pub 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    /// Create an empty shell with default settings.
93    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    /// Set the prompt string.
105    pub fn prompt(&mut self, prompt: &'static str) -> &mut Self {
106        self.prompt = prompt;
107        self
108    }
109
110    /// Set the maximum number of remembered history entries.
111    pub fn max_history(&mut self, n: usize) -> &mut Self {
112        self.max_history = n;
113        self
114    }
115
116    /// Set the maximum length (in bytes) of a remembered history entry.
117    /// Longer lines are truncated (at a character boundary) before being
118    /// stored. Defaults to 64; `0` disables history storage of content
119    /// beyond the empty string.
120    pub fn max_history_len(&mut self, n: usize) -> &mut Self {
121        self.max_history_len = n;
122        self
123    }
124
125    /// Set the maximum length (in bytes) of a line typed at the prompt.
126    /// Once the buffer is full, further characters are rejected with a `BEL`
127    /// (`\x07`). Defaults to 128.
128    pub fn max_line_len(&mut self, n: usize) -> &mut Self {
129        self.max_line_len = n;
130        self
131    }
132
133    /// Register a command.
134    ///
135    /// `name` is what the user types at the prompt, `help` is the one-line
136    /// description shown by `help`. `handler` receives the arguments and an
137    /// output handle, and must return a boxed future (wrap an `async move`
138    /// block in [`Box::pin`]) so that all commands share one concrete future
139    /// type:
140    ///
141    /// ```
142    /// # use embassy_shell::Shell;
143    /// let mut shell = Shell::new();
144    /// shell.add_command("hello", "say hello", |args, mut io| {
145    ///     Box::pin(async move {
146    ///         let name = args.get(0).unwrap_or("world");
147    ///         io.println(&format!("Hello, {name}!")).await
148    ///     })
149    /// });
150    /// ```
151    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    /// Register a command whose first argument is completed from a fixed list
164    /// of options (e.g. `led on|off|blink`).
165    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    /// Register a command with a custom completer. The completer is called
183    /// with the index of the argument being completed (`>= 1`) and the
184    /// partial token, and returns candidate replacements for that token.
185    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    /// Names of all registered user commands.
204    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    /// Run the interactive shell until end of input.
222    ///
223    /// `reader` and `writer` are the transport (UART, USB CDC, ...). They may
224    /// be any types implementing [`embedded_io_async::Read`] /
225    /// [`embedded_io_async::Write`].
226    ///
227    /// Ctrl-C interrupts a running command (the handler future is dropped, so
228    /// handlers should be cancel-safe). Bytes typed while a command runs are
229    /// not echoed and only the most recent one is kept for the next line.
230    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        // Token slots are reused across lines: after the longest line ever
241        // executed, tokenization stops allocating.
242        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                // End of input.
249                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                        // Line buffer is full: beep and drop the character.
258                        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                    // Clear (rather than replace) so the buffer's capacity is
288                    // reused by the next line.
289                    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        // Store at most `max_history_len` bytes, cut at a char boundary.
402        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    /// Rewrite the current input line and place the cursor back at `cursor`.
417    async fn redraw<W>(&self, writer: &mut W, buf: &str, cursor: usize) -> Result<()>
418    where
419        W: embedded_io_async::Write,
420    {
421        // Rendered as several small writes instead of one allocated string.
422        let mut io = Io::new(writer);
423        io.print("\r").await?;
424        io.print(self.prompt).await?;
425        io.print(buf).await?;
426        // Erase any leftovers from the previous render.
427        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    /// Tab completion for the token under the cursor (which must be at the
437    /// end of the input line, like most simple terminals).
438    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        // Only complete the token at the very end of the line.
443        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        // Candidates are borrowed where possible; custom completers still
460        // return owned Strings, kept alive by `owned`.
461        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            // Bash appends a space after a unique completion.
505            buf.push(' ');
506            *cursor = buf.len();
507            return self.redraw(writer, buf, *cursor).await;
508        }
509
510        // Multiple candidates: insert the common prefix, then list them all.
511        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        // User commands win over builtins (bash-style overridability).
544        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    /// Run one command, racing it against the input so Ctrl-C can cancel it.
593    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        // The line was completed to `echo ` and then executed.
752        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        // `of` is a unique prefix of `off`.
777        let out = run(&mut sh, b"led of\t\r\n");
778        assert!(out.contains("embassy> led off "), "{out}");
779        // Executed with `off`.
780        assert!(out.contains("\r\noff\r\n"), "{out}");
781        // `o` is ambiguous between `on` and `off`: both are listed.
782        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        // `two` printed twice (second run via history), `one` once.
793        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        // After Ctrl-C the line is discarded; the next line still runs.
811        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        // Type "echo o", Home, then insert "ec" is fiddly; instead:
849        // "ech o" -> Left, Right, Backspace x? Simpler: type "echX", Left,
850        // Backspace, "o" => "echo", Enter with arg... we type "ech o" then
851        // Left, Backspace => "echo ", Enter (no args) prints empty line.
852        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        // "echo " fills the 5-byte cap; the argument is rejected with BELs
862        // and never reaches the command.
863        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        // `help` is a builtin: its own output cannot be confused with the
874        // history listing below.
875        let out = run(&mut sh, b"help XXXXXXXXXXXX\nhistory\r\n");
876        // The listing shows the entry truncated to 4 bytes.
877        assert!(out.contains("   1  help\r\n"), "{out:?}");
878        assert!(!out.contains("   1  help "), "{out:?}");
879    }
880}