rust-kernel
Rustbelt: a hobby x86_64 kernel written in Rust
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 01:32:12 -0700 |
|---|---|---|
| commit | d3dbcfaf7fae0c452e63f201fbc709209f8810ed | |
| parent | 320b50a20c463485b5abd4f9d71b5395b4c27581 | |
| tree | browse files at this commit | |
Shell: drive letters start at A: (root) instead of C: / is A:, mounted disks get B:, C:, ... in mount order (the FAT disk is B:). C: for the system drive is a floppy-era leftover; Rustbelt has no floppies. Updated shparse, sh help text, selftest.sh, disk readme, tests, scripts, README and boot.png.
| M | README.md | +69 -64 |
| M | boot.png | +0 -0 |
| M | disk/readme.txt | +1 -1 |
| M | disk/scripts/selftest.sh | +11 -11 |
| M | scripts/run-headless.sh | +4 -4 |
| M | scripts/screenshot.sh | +4 -4 |
| M | tests/shell.rs | +24 -24 |
| M | user/sh.rs | +3 -3 |
| M | user/shparse.rs | +12 -12 |
9 files changed, 128 insertions, 123 deletions
@@ -5,7 +5,7 @@ the 2nd edition of Philipp Oppermann's [Writing an OS in Rust](https://os.phil-opp.com/) and keeps going past the end of it. It boots in QEMU straight into **its own shell**: a user-mode program that has a Unix feel with DOS touches. It offers `ls`, `cat`, pipes and-redirection, plus `dir`, `type`, `ver`, and drive letters like `C:` and `D:`.+redirection, plus `dir`, `type`, `ver`, and drive letters like `A:` and `B:`. Under the shell: - **VGA text and serial output.** The kernel prints to the VGA text screen and@@ -79,8 +79,8 @@ cargo run # build + boot in a QEMU window; serial outp ``` **Using it:** `cargo run` opens a QEMU window. The kernel prints a few boot-lines, then you're at the shell prompt `C:/ $`. Click into the window and type,-for example `help`, `cd D:`, `ls` or `cat readme.txt | wc`. See+lines, then you're at the shell prompt `A:/ $`. Click into the window and type,+for example `help`, `cd B:`, `ls` or `cat readme.txt | wc`. See [The shell](#the-shell-stage-7) for everything it can do. Everything on the screen is also mirrored to your terminal over serial. Typing `exit` ends the shell, and the kernel starts a fresh one.@@ -136,29 +136,29 @@ Rustbelt 0.7.0 -- a hobby x86_64 kernel in Rust [vfs] mounted ramfs at / [vfs] mounted ramfs at /tmp [vfs] mounted fat32 at /mnt/disk0-C:/ $ ver+A:/ $ ver Rustbelt 0.7.0 (stage 7: shell) x86_64-C:/ $ drives-C: / ramfs-D: /mnt/disk0 fat32-C:/ $ pwd+A:/ $ drives+A: / ramfs+B: /mnt/disk0 fat32+A:/ $ pwd /-C:/ $ cd D:-D:/ $ ls+A:/ $ cd B:+B:/ $ ls [process] started pid 2 (ls), entry 0x8000402c00 bin/ notes/ readme.txt scripts/ [process] pid 2 (ls) exited with code 0-D:/ $ ls -l notes+B:/ $ ls -l notes [process] started pid 3 (ls), entry 0x8000402c00 125 todo.txt [process] pid 3 (ls) exited with code 0-D:/ $ type D:/readme.txt+B:/ $ type B:/readme.txt [process] started pid 4 (cat), entry 0x8000400260 Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS.-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 4 (cat) exited with code 0-D:/ $ cat d:notes/todo.txt | grep -i EXT | wc -l+B:/ $ cat b:notes/todo.txt | grep -i EXT | wc -l [process] started pid 5 (cat), entry 0x8000400260 [process] started pid 6 (grep), entry 0x8000400610 [process] pid 5 (cat) exited with code 0@@ -166,46 +166,46 @@ D:/ $ cat d:notes/todo.txt | grep -i EXT | wc -l [process] pid 6 (grep) exited with code 0 1 [process] pid 7 (wc) exited with code 0-D:/ $ dir bin | wc -l+B:/ $ dir bin | wc -l [process] started pid 8 (ls), entry 0x8000402c00 [process] started pid 9 (wc), entry 0x8000400260 [process] pid 8 (ls) exited with code 0 15 [process] pid 9 (wc) exited with code 0-D:/ $ echo first line > C:/tmp/out.txt-D:/ $ echo second line >> /tmp/out.txt-D:/ $ cat < /tmp/out.txt+B:/ $ echo first line > A:/tmp/out.txt+B:/ $ echo second line >> /tmp/out.txt+B:/ $ cat < /tmp/out.txt [process] started pid 10 (cat), entry 0x8000400260 first line second line [process] pid 10 (cat) exited with code 0-D:/ $ md /tmp/work+B:/ $ md /tmp/work [process] started pid 11 (mkdir), entry 0x8000400100 [process] pid 11 (mkdir) exited with code 0-D:/ $ copy D:/notes/todo.txt /tmp/work+B:/ $ copy B:/notes/todo.txt /tmp/work [process] started pid 12 (cp), entry 0x8000400670 [process] pid 12 (cp) exited with code 0-D:/ $ touch /tmp/work/empty.txt+B:/ $ touch /tmp/work/empty.txt [process] started pid 13 (touch), entry 0x8000400100 [process] pid 13 (touch) exited with code 0-D:/ $ ls -l C:/tmp/work+B:/ $ ls -l A:/tmp/work [process] started pid 14 (ls), entry 0x8000402c00 0 empty.txt 125 todo.txt [process] pid 14 (ls) exited with code 0-D:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work+B:/ $ del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work [process] started pid 15 (rm), entry 0x8000400100 [process] pid 15 (rm) exited with code 0-D:/ $ grep "D:" readme.txt+B:/ $ grep "B:" readme.txt [process] started pid 16 (grep), entry 0x8000400610-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`. [process] pid 16 (grep) exited with code 0-D:/ $ alias ll="ls -l"-D:/ $ ll C:/tmp+B:/ $ alias ll="ls -l"+B:/ $ ll A:/tmp [process] started pid 17 (ls), entry 0x8000402c00 23 out.txt [process] pid 17 (ls) exited with code 0-D:/ $ run --trace cat D:/notes/todo.txt+B:/ $ run --trace cat B:/notes/todo.txt [process] started pid 18 (cat), entry 0x8000400260 [trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3 [trace 18] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125@@ -216,29 +216,29 @@ Next: more file systems -- ext2, exFAT, btrfs. [trace 18] close(3) = 0 [trace 18] exit(0) [process] pid 18 (cat) exited with code 0-D:/ $ start sleep 1+B:/ $ start sleep 1 [process] started pid 19 (sleep), entry 0x8000400000 [pid 19] sleep started in the background-D:/ $ jobs+B:/ $ jobs [pid 19] running start sleep 1-D:/ $ wait+B:/ $ wait [process] pid 19 (sleep) exited with code 0 [done] pid 19 (start sleep 1), exit code 0-D:/ $ run evil+B:/ $ run evil [process] started pid 20 (evil), entry 0x80004000f0 [evil] pid 20: reading kernel memory at 0x200000... [kernel] user page fault: address Ok(VirtAddr(0x200000)), error PageFaultErrorCode(PROTECTION_VIOLATION | USER_MODE) [kernel] page fault in user mode at rip 0x800040017e [process] pid 20 (evil) killed: page fault [exit code 139]-D:/ $ nosuchprogram+B:/ $ nosuchprogram sh: nosuchprogram: command not found (try `help`)-D:/ $ cd Q:+B:/ $ cd Q: cd: there is no drive Q: (see `drives`)-D:/ $ cd C:/tmp-C:/tmp $ pwd+B:/ $ cd A:/tmp+A:/tmp $ pwd /tmp-C:/tmp $ exit 0+A:/tmp $ exit 0 [process] pid 1 (sh) exited with code 0 [ok] shell self-test finished: Exited(0) [memory] 10 frames in use@@ -285,7 +285,7 @@ heap_allocation::simple_allocation... [ok] | `tests/should_panic.rs` | a failing `assert_eq!` reaches the panic handler (`harness = false`) | | `tests/stack_overflow.rs` | stack overflow lands in the double-fault handler on the IST stack (`harness = false`) | | `tests/executor.rs` | `SimpleExecutor` runs an async fn to completion; the waker-based `Executor` interleaves two tasks that `yield_now().await` (a1 b1 a2 b2 a3 b3) |-| `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `D:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged |+| `tests/shell.rs` | **the shell**: tokenizer (operators without spaces, quotes, comments), parser (pipelines, `<` `>` `>>`, `&`, error cases), drive-letter assignment from a mount table, `B:/x` -> `/mnt/disk0/x` translation and back for the prompt; then the real `/bin/sh` runs a script (cd with a drive letter, ls, a pipe, `>`/`>>`/`<`, `ver`, `exit 3`) with its stdout in a pipe and the output is checked; background jobs (`start`, `jobs`, `wait`); 20 processes started and reaped leave `frames_in_use` unchanged | | `tests/user_mode.rs` | programs loaded from the FAT disk: `hello` exits 0, `demo` passes all its checks (exit 0), `evil` is killed with code 139, `spin` is preempted; spawn with arguments + trace + exit code 7, then reaped; a child's stdout into a pipe; spawn errors (`ENOENT`, `ENOEXEC`, `EISDIR`, `ECHILD`); `wait` with `WNOHANG` returns `EAGAIN` while the child runs; ELF loader rejects garbage; user pointers give `EFAULT` (length 0 is fine); `sleep` blocks for the right number of ticks; a `WaitQueue` wakes its waiter | | `tests/vfs.rs` | read `/hello.txt` to EOF, path normalisation (`.`, `..`, relative to a cwd), `ENOENT`/`ENOTDIR`/`EINVAL`/`EISDIR`/`EEXIST`, `/tmp` is a separate ramfs, longest-prefix mounts, sparse writes zero-fill, listing, remove (`ENOTEMPTY`, `EBUSY` on mount points), seek/stat/truncate, `getdents64` record layout, pipes pass bytes and report EOF / `EPIPE` | | `tests/block.rs` | PCI scan finds the virtio disk; sector 1 of the image is the GPT header; multi-sector reads equal single-sector reads; write + read back; out-of-range and partial-sector requests fail; the GPT partition is registered as `vda1` (with a FAT32 boot sector); MBR parsing on a RAM disk |@@ -896,19 +896,19 @@ When it exits, the thread starts a new one, the way `init` respawns a login shell. ```-Rustbelt shell. Type `help` for commands. Drives: C: = /, D: = /mnt/disk0-C:/ $ cd d:-D:/ $ ls+Rustbelt shell. Type `help` for commands. Drives: A: = /, B: = /mnt/disk0+A:/ $ cd b:+B:/ $ ls bin/ notes/ readme.txt scripts/-D:/ $ type readme.txt | grep -i rustbelt-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.-D:/ $ ls bin | wc -l+B:/ $ type readme.txt | grep -i rustbelt+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`.+B:/ $ ls bin | wc -l 15-D:/ $ echo first > C:/tmp/a.txt-D:/ $ echo second >> C:/tmp/a.txt-D:/ $ wc < C:/tmp/a.txt+B:/ $ echo first > A:/tmp/a.txt+B:/ $ echo second >> A:/tmp/a.txt+B:/ $ wc < A:/tmp/a.txt 2 2 13-D:/ $ run --trace cat notes/todo.txt+B:/ $ run --trace cat notes/todo.txt [trace 8] open("notes/todo.txt", 0o0) = 3 [trace 8] read(3, "Done: the shell (cd, ls, cat, run, start"..., 512) = 125 Done: the shell (cd, ls, cat, run, start, pipes, redirection, drive letters).@@ -917,26 +917,26 @@ Next: more file systems -- ext2, exFAT, btrfs. [trace 8] read(3, "", 512) = 0 [trace 8] close(3) = 0 [trace 8] exit(0)-D:/ $ start sleep 5+B:/ $ start sleep 5 [pid 9] sleep started in the background-D:/ $ jobs+B:/ $ jobs [pid 9] running start sleep 5-D:/ $ run evil+B:/ $ run evil [evil] pid 10: reading kernel memory at 0x200000... [process] pid 10 (evil) killed: page fault [exit code 139]-D:/ $ ver+B:/ $ ver Rustbelt 0.7.0 (stage 7: shell) x86_64-D:/ $ <- Enter on an empty line, a few seconds later+B:/ $ <- Enter on an empty line, a few seconds later [done] pid 9 (start sleep 5), exit code 0-D:/ $+B:/ $ ``` ### Commands | Command | What it does | |---|---|-| `cd [dir]` | change directory (`cd` alone = `C:/`); `cd D:` goes to the root of drive D |+| `cd [dir]` | change directory (`cd` alone = `A:/`); `cd B:` goes to the root of drive B | | `pwd` | print the current directory as a Unix path (the prompt shows the drive form) | | `echo text...` | print the arguments | | `ver` | kernel name and version, from the `uname` syscall |@@ -949,9 +949,9 @@ D:/ $ | `exit [code]` | leave the shell (with the last command's exit code, or `code`) | | `run [--trace] prog args` | run a program in the foreground; `--trace` prints every syscall it makes | | `start prog args` | run it in the background and print its pid (same as `prog args &`) |-| `prog args` | a bare name is looked up in each drive's `bin` directory, in order: `C:/bin`, `D:/bin`... |+| `prog args` | a bare name is looked up in each drive's `bin` directory, in order: `A:/bin`, `B:/bin`... | -Programs in `D:/bin` (`user/*.rs`, each 30-150 lines):+Programs in `B:/bin` (`user/*.rs`, each 30-150 lines): | Program | What it does | |---|---|@@ -1001,20 +1001,25 @@ to the pipe. Pipes have no size limit here, so that can't block. Drive letters are only shell syntax. The kernel keeps one Unix tree: -- **Mapping**: `C:` is `/`. `D:`, `E:`... go to the mounts under `/mnt`,+- **Why start at A:?** In DOS and Windows the system drive is `C:` only for+ historical reasons. `A:` and `B:` were reserved for floppy drives, so the+ first hard disk got `C:`, and Windows still keeps that habit. Rustbelt has+ no floppies, so its letters simply start at the front of the alphabet.+ (The first version of the shell used `C:` for `/`.)+- **Mapping**: `A:` is `/`. `B:`, `C:`... go to the mounts under `/mnt`, in the order they were mounted. The shell reads them from the `mounts` syscall before every prompt, so a newly mounted disk would get a letter- right away. Other mounts, such as `/tmp`, stay under C: (`C:/tmp`).+ right away. Other mounts, such as `/tmp`, stay under A: (`A:/tmp`). - **Translation**: every unquoted word that starts with a known letter is translated before any syscall sees it (`shparse::translate`):- - `D:/notes/todo.txt` becomes `/mnt/disk0/notes/todo.txt`.- - `d:` becomes `/mnt/disk0` (any case).- - `D:notes` becomes `/mnt/disk0/notes`, since drives don't have their own+ - `B:/notes/todo.txt` becomes `/mnt/disk0/notes/todo.txt`.+ - `b:` becomes `/mnt/disk0` (any case).+ - `B:notes` becomes `/mnt/disk0/notes`, since drives don't have their own current directory. - Quote a word to keep it literal: `grep "D:" readme.txt`.+ Quote a word to keep it literal: `grep "B:" readme.txt`. - **Prompt**: the prompt goes the other way (`shparse::display`). The longest- matching drive path wins, so `/mnt/disk0/bin` shows as `D:/bin`.+ matching drive path wins, so `/mnt/disk0/bin` shows as `B:/bin`. - **Unknown letters**: these are left alone, so `cd Q:` says `there is no drive Q:`.
Binary file changed.
@@ -1,3 +1,3 @@ Hello from the FAT32 disk! This file lives on a virtio-blk disk, in a GPT partition, in a FAT32 file system -- read through the kernel's VFS.-In the Rustbelt shell this disk is drive D: -- try `cd D:` and `ls`.+In the Rustbelt shell this disk is drive B: -- try `cd B:` and `ls`.
@@ -1,32 +1,32 @@ # Rustbelt shell self-test: `cargo run --features headless` runs this with-# `sh /mnt/disk0/scripts/selftest.sh` (D:/scripts/selftest.sh) and scripts/run-headless.sh checks the output.+# `sh /mnt/disk0/scripts/selftest.sh` (B:/scripts/selftest.sh) and scripts/run-headless.sh checks the output. ver drives pwd-cd D:+cd B: ls ls -l notes-type D:/readme.txt-cat d:notes/todo.txt | grep -i EXT | wc -l+type B:/readme.txt+cat b:notes/todo.txt | grep -i EXT | wc -l dir bin | wc -l-echo first line > C:/tmp/out.txt+echo first line > A:/tmp/out.txt echo second line >> /tmp/out.txt cat < /tmp/out.txt md /tmp/work-copy D:/notes/todo.txt /tmp/work+copy B:/notes/todo.txt /tmp/work touch /tmp/work/empty.txt-ls -l C:/tmp/work+ls -l A:/tmp/work del /tmp/work/todo.txt /tmp/work/empty.txt /tmp/work-grep "D:" readme.txt+grep "B:" readme.txt alias ll="ls -l"-ll C:/tmp-run --trace cat D:/notes/todo.txt+ll A:/tmp+run --trace cat B:/notes/todo.txt start sleep 1 jobs wait run evil nosuchprogram cd Q:-cd C:/tmp+cd A:/tmp pwd exit 0
@@ -32,10 +32,10 @@ set -e if [ -z "${1:-}" ] && [ "$code" = 33 ]; then expect=( "Rustbelt 0.7.0 (stage 7: shell) x86_64" # ver- "D: /mnt/disk0 fat32" # drives- "D:/ $ ls" # cd D: changed the prompt+ "B: /mnt/disk0 fat32" # drives+ "B:/ $ ls" # cd B: changed the prompt "readme.txt scripts/" # ls in columns- "In the Rustbelt shell this disk is drive D:" # type D:/readme.txt+ "In the Rustbelt shell this disk is drive B:" # type B:/readme.txt "second line" # >> and < " 125 todo.txt" # cp + ls -l '[trace 18] open("/mnt/disk0/notes/todo.txt", 0o0) = 3' # run --trace@@ -44,7 +44,7 @@ if [ -z "${1:-}" ] && [ "$code" = 33 ]; then "[exit code 139]" # run evil "sh: nosuchprogram: command not found" "cd: there is no drive Q:"- "C:/tmp $ pwd"+ "A:/tmp $ pwd" ) missing=0 for line in "${expect[@]}"; do
@@ -25,14 +25,14 @@ if [ -z "$SESSION" ]; then # and fixes it. SESSION=$(mktemp /tmp/session.XXXXXX) cat > "$SESSION" <<'TXT'-cd d:+cd b: lx{BS}s-cat D:/notes/todo.txt+cat B:/notes/todo.txt type readme.txt | grep -i rustbelt run --trace cat notes/todo.txt TXT- set -- "D:/ $ ls" "bin/ notes/" "Next: more file systems" \- "In the Rustbelt shell this disk is drive D:" \+ set -- "B:/ $ ls" "bin/ notes/" "Next: more file systems" \+ "In the Rustbelt shell this disk is drive B:" \ '] open("notes/todo.txt", 0o0) = 3' "] exit(0)" fi SOCK=$(mktemp -u /tmp/qemu-mon.XXXXXX)
@@ -94,21 +94,21 @@ const MOUNTS: &str = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 fat32\n" fn drive_letters_are_assigned_in_mount_order() { let drives = shparse::assign_drives(MOUNTS); let summary: Vec<(char, &str, &str)> = drives.iter().map(|d| (d.letter, d.path.as_str(), d.fs.as_str())).collect();- // /tmp gets no letter: it's C:/tmp.- assert_eq!(summary, [('C', "/", "ramfs"), ('D', "/mnt/disk0", "fat32"), ('E', "/mnt/disk1", "fat32")]);+ // /tmp gets no letter: it's A:/tmp.+ assert_eq!(summary, [('A', "/", "ramfs"), ('B', "/mnt/disk0", "fat32"), ('C', "/mnt/disk1", "fat32")]); } #[test_case] fn drive_letters_translate_to_paths() { let drives = shparse::assign_drives(MOUNTS); let t = |s| shparse::translate(s, &drives);- assert_eq!(t("D:").as_deref(), Some("/mnt/disk0"));- assert_eq!(t("D:/").as_deref(), Some("/mnt/disk0"));- assert_eq!(t("d:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case- assert_eq!(t("D:notes").as_deref(), Some("/mnt/disk0/notes"));- assert_eq!(t("e:/x").as_deref(), Some("/mnt/disk1/x"));- assert_eq!(t("C:").as_deref(), Some("/"));- assert_eq!(t("C:/tmp/a.txt").as_deref(), Some("/tmp/a.txt"));+ assert_eq!(t("B:").as_deref(), Some("/mnt/disk0"));+ assert_eq!(t("B:/").as_deref(), Some("/mnt/disk0"));+ assert_eq!(t("b:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case+ assert_eq!(t("B:notes").as_deref(), Some("/mnt/disk0/notes"));+ assert_eq!(t("c:/x").as_deref(), Some("/mnt/disk1/x"));+ assert_eq!(t("A:").as_deref(), Some("/"));+ assert_eq!(t("A:/tmp/a.txt").as_deref(), Some("/tmp/a.txt")); assert_eq!(t("Z:/x"), None); // no such drive assert_eq!(t("readme.txt"), None); assert_eq!(t("/mnt/disk0"), None);@@ -119,13 +119,13 @@ fn drive_letters_translate_to_paths() { fn paths_display_with_drive_letters() { let drives = shparse::assign_drives(MOUNTS); let d = |s| shparse::display(s, &drives);- assert_eq!(d("/"), "C:/");- assert_eq!(d("/tmp"), "C:/tmp");- assert_eq!(d("/mnt/disk0"), "D:/");- assert_eq!(d("/mnt/disk0/bin"), "D:/bin");- assert_eq!(d("/mnt/disk1/a/b"), "E:/a/b");- assert_eq!(d("/mnt/disk01"), "C:/mnt/disk01"); // not inside /mnt/disk0- assert_eq!(d("/mnt"), "C:/mnt");+ assert_eq!(d("/"), "A:/");+ assert_eq!(d("/tmp"), "A:/tmp");+ assert_eq!(d("/mnt/disk0"), "B:/");+ assert_eq!(d("/mnt/disk0/bin"), "B:/bin");+ assert_eq!(d("/mnt/disk1/a/b"), "C:/a/b");+ assert_eq!(d("/mnt/disk01"), "A:/mnt/disk01"); // not inside /mnt/disk0+ assert_eq!(d("/mnt"), "A:/mnt"); } // --- the real shell --------------------------------------------------------@@ -154,13 +154,13 @@ fn run_shell_script(script: &str) -> (String, ExitStatus) { #[test_case] fn scripted_shell_session() { let (out, status) = run_shell_script(- "cd D:\n\+ "cd B:\n\ pwd\n\ ls notes\n\ echo piped text | grep piped\n\- type d:notes/todo.txt | wc -l\n\- echo saved > C:/tmp/s.txt\n\- echo more >> C:/tmp/s.txt\n\+ type b:notes/todo.txt | wc -l\n\+ echo saved > A:/tmp/s.txt\n\+ echo more >> A:/tmp/s.txt\n\ cat < /tmp/s.txt\n\ ver\n\ nosuchprogram\n\@@ -169,14 +169,14 @@ fn scripted_shell_session() { ); // Each command is echoed after its prompt; the prompt shows the drive. for expected in [- "C:/ $ cd D:\n",- "D:/ $ pwd\n/mnt/disk0\n",- "D:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line+ "A:/ $ cd B:\n",+ "B:/ $ pwd\n/mnt/disk0\n",+ "B:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line "piped text\n", "wc -l\n2\n", "saved\nmore\n", "Rustbelt 0.7.0",- "D:/ $ exit 3\n",+ "B:/ $ exit 3\n", ] { assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out); }
@@ -41,7 +41,7 @@ Rustbelt shell commands run [--trace] prog args run a program (--trace prints its syscalls) start prog args run it in the background (or: prog args &) a | b | c pipes; > file >> file < file redirection- C: is /, D: E: ... are the disks (see `drives`): cd D:, cat D:/readme.txt+ A: is /, B: C: ... are the disks (see `drives`): cd B:, cat B:/readme.txt "; /// A background job started with `start` or `&`.@@ -137,7 +137,7 @@ impl Shell { String::from_utf8_lossy(&buf[..n]).into_owned() } - /// `C:/ $ `, `D:/notes $ `+ /// `A:/ $ `, `B:/notes $ ` fn prompt(&self) -> String { format!("{} $ ", shparse::display(&self.cwd(), &self.drives)) }@@ -381,7 +381,7 @@ impl Shell { } /// Find a program: names with a `/` are paths; others are looked up in- /// the `bin` directory of every drive (our PATH): C:/bin, D:/bin, ...+ /// the `bin` directory of every drive (our PATH): A:/bin, B:/bin, ... fn find_program(&self, name: &str) -> Option<String> { if name.contains('/') { return Some(name.to_string());
@@ -1,5 +1,5 @@ //! The shell's parser: command line -> tokens -> pipeline, and the drive-//! letter mapping (`D:/notes` <-> `/mnt/disk0/notes`).+//! letter mapping (`B:/notes` <-> `/mnt/disk0/notes`). //! //! Pure functions with no syscalls, so the same file is compiled into the //! shell (user/sh.rs) *and* into the kernel's test suite (tests/shell.rs),@@ -20,7 +20,7 @@ use alloc::{format, string::String, vec::Vec}; /// One word of a command. `quoted` words are never drive-translated, so-/// `grep "C:" file` searches for the text C:.+/// `grep "A:" file` searches for the text A:. #[derive(Debug, Clone, PartialEq)] pub struct Word { pub text: String,@@ -161,17 +161,17 @@ pub struct Drive { /// Assign letters from the kernel's mount table (the text from the /// `mounts` syscall: one "path fstype" per line, in mount order).-/// C: is always the root; D:, E:, ... go to the mounts under /mnt (the+/// A: is always the root; B:, C:, ... go to the mounts under /mnt (the /// disks), in the order they were mounted. Other mounts such as /tmp are-/// reachable through C: (C:/tmp).+/// reachable through A: (A:/tmp). pub fn assign_drives(mount_table: &str) -> Vec<Drive> { let mut drives = Vec::new(); let root_fs = mount_table .lines() .find_map(|l| l.split_once(' ').filter(|(p, _)| *p == "/").map(|(_, fs)| fs)) .unwrap_or("ramfs");- drives.push(Drive { letter: 'C', path: String::from("/"), fs: String::from(root_fs.trim()) });- let mut letter = b'D';+ drives.push(Drive { letter: 'A', path: String::from("/"), fs: String::from(root_fs.trim()) });+ let mut letter = b'B'; for line in mount_table.lines() { if let Some((path, fs)) = line.split_once(' ') { if path.starts_with("/mnt/") && letter <= b'Z' {@@ -183,7 +183,7 @@ pub fn assign_drives(mount_table: &str) -> Vec<Drive> { drives } -/// Does `word` start with a drive letter ("D:", "d:/x", "E:notes")?+/// Does `word` start with a drive letter ("B:", "b:/x", "C:notes")? pub fn drive_letter(word: &str) -> Option<char> { let b = word.as_bytes(); if b.len() >= 2 && b[0].is_ascii_alphabetic() && b[1] == b':' {@@ -193,9 +193,9 @@ pub fn drive_letter(word: &str) -> Option<char> { } } -/// `D:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case-/// insensitive; `D:` and `D:/` are the drive's root, and `D:notes` means-/// `D:/notes` (drives have no current directory of their own). `None` if+/// `B:/notes/todo.txt` -> `/mnt/disk0/notes/todo.txt`. Letters are case+/// insensitive; `B:` and `B:/` are the drive's root, and `B:notes` means+/// `B:/notes` (drives have no current directory of their own). `None` if /// `word` doesn't start with a known drive letter. pub fn translate(word: &str, drives: &[Drive]) -> Option<String> { let letter = drive_letter(word)?;@@ -210,8 +210,8 @@ pub fn translate(word: &str, drives: &[Drive]) -> Option<String> { }) } -/// The other direction, for the prompt: `/mnt/disk0/bin` -> `D:/bin`,-/// `/tmp` -> `C:/tmp`. Picks the drive with the longest matching path.+/// The other direction, for the prompt: `/mnt/disk0/bin` -> `B:/bin`,+/// `/tmp` -> `A:/tmp`. Picks the drive with the longest matching path. pub fn display(path: &str, drives: &[Drive]) -> String { let mut best: Option<(&Drive, &str)> = None; for d in drives {