Skip to main content

embassy_shell/
shell.rs

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
17/// Commands handled by the shell itself. Shown in `help` and completed at the
18/// prompt unless a user command shadows one of them.
19const BUILTINS: [(&str, &str); 3] = [
20    ("help", "list available commands"),
21    ("history", "show command history"),
22    ("clear", "clear the screen"),
23];
24
25/// What happened while a command was running.
26#[derive(Debug, PartialEq, Eq)]
27enum ExecOutcome {
28    /// Command finished, was interrupted, or produced an error.
29    Done,
30    /// The input reached end-of-file.
31    Eof,
32}
33
34/// Result of racing a command future against the input stream.
35enum Step {
36    Done(Result<()>),
37    Byte(u8),
38    Interrupted,
39    Eof,
40}
41
42/// The shell.
43///
44/// The type parameter `'a` is the lifetime of state captured by command
45/// handlers registered with [`Shell::add_command`].
46///
47/// # Example
48///
49/// ```
50/// use embassy_shell::Shell;
51///
52/// let mut shell = Shell::new();
53/// shell.add_command("echo", "print arguments", |args, mut io| {
54///     Box::pin(async move { io.println(&args.rest()).await })
55/// });
56///
57/// let mut input: &[u8] = b"echo hello world\r\n";
58/// let mut output: Vec<u8> = Vec::new();
59/// futures::executor::block_on(shell.run(&mut input, &mut output)).unwrap();
60///
61/// assert!(String::from_utf8_lossy(&output).contains("hello world"));
62/// ```
63pub 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    /// Create an empty shell with default settings.
78    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    /// Set the prompt string.
88    pub fn prompt(&mut self, prompt: &'static str) -> &mut Self {
89        self.prompt = prompt;
90        self
91    }
92
93    /// Set the maximum number of remembered history entries.
94    pub fn max_history(&mut self, n: usize) -> &mut Self {
95        self.max_history = n;
96        self
97    }
98
99    /// Register a command.
100    ///
101    /// `name` is what the user types at the prompt, `help` is the one-line
102    /// description shown by `help`. `handler` receives the arguments and an
103    /// output handle, and must return a boxed future (wrap an `async move`
104    /// block in [`Box::pin`]) so that all commands share one concrete future
105    /// type:
106    ///
107    /// ```
108    /// # use embassy_shell::Shell;
109    /// let mut shell = Shell::new();
110    /// shell.add_command("hello", "say hello", |args, mut io| {
111    ///     Box::pin(async move {
112    ///         let name = args.get(0).unwrap_or("world");
113    ///         io.println(&format!("Hello, {name}!")).await
114    ///     })
115    /// });
116    /// ```
117    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    /// Register a command whose first argument is completed from a fixed list
130    /// of options (e.g. `led on|off|blink`).
131    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    /// Register a command with a custom completer. The completer is called
159    /// with the index of the argument being completed (`>= 1`) and the
160    /// partial token, and returns candidate replacements for that token.
161    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    /// Names of all registered user commands.
180    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    /// Run the interactive shell until end of input.
198    ///
199    /// `reader` and `writer` are the transport (UART, USB CDC, ...). They may
200    /// be any types implementing [`embedded_io_async::Read`] /
201    /// [`embedded_io_async::Write`].
202    ///
203    /// Ctrl-C interrupts a running command (the handler future is dropped, so
204    /// handlers should be cancel-safe). Bytes typed while a command runs are
205    /// not echoed and only the most recent one is kept for the next line.
206    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                // End of input.
222                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    /// Rewrite the current input line and place the cursor back at `cursor`.
367    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        // Erase any leftovers from the previous render.
376        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    /// Tab completion for the token under the cursor (which must be at the
384    /// end of the input line, like most simple terminals).
385    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        // Only complete the token at the very end of the line.
390        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            // Bash appends a space after a unique completion.
440            buf.push(' ');
441            *cursor = buf.len();
442            return self.redraw(writer, buf, *cursor).await;
443        }
444
445        // Multiple candidates: insert the common prefix, then list them all.
446        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        // User commands win over builtins (bash-style overridability).
480        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    /// Run one command, racing it against the input so Ctrl-C can cancel it.
513    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        // The line was completed to `echo ` and then executed.
670        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        // `of` is a unique prefix of `off`.
695        let out = run(&mut sh, b"led of\t\r\n");
696        assert!(out.contains("embassy> led off "), "{out}");
697        // Executed with `off`.
698        assert!(out.contains("\r\noff\r\n"), "{out}");
699        // `o` is ambiguous between `on` and `off`: both are listed.
700        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        // `two` printed twice (second run via history), `one` once.
711        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        // After Ctrl-C the line is discarded; the next line still runs.
729        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        // Type "echo o", Home, then insert "ec" is fiddly; instead:
767        // "ech o" -> Left, Right, Backspace x? Simpler: type "echX", Left,
768        // Backspace, "o" => "echo", Enter with arg... we type "ech o" then
769        // Left, Backspace => "echo ", Enter (no args) prints empty line.
770        let out = run(&mut sh, b"ech o\x1b[D\x7f\r\n");
771        assert!(!out.contains("command not found"), "{out}");
772    }
773}