RustForge

foundry

Foundry OS: the operating system built on the Rustbelt kernel

authorTyler Fowler <tyler@localhost>2026-10-30 01:22:29 -0700
commitb10b990e384b578fd41000614e51b20a726c3103
parent519a3fb17204c73a6a4fa94ef6433ef32866c3bb
treebrowse files at this commit
Stage 7: Rustbelt shell

- /bin/sh as a ring-3 program: raw-mode line editor (Backspace, history),
  builtins (cd pwd echo help exit cls/clear ver alias drives history jobs wait),
  run / run --trace / start / &, PATH over each drive's bin, multi-stage
  pipes, < > >>, DOS aliases (dir type del md copy), drive letters
- new programs: ls cat wc grep mkdir rm touch cp sleep (echo removed)
- kernel: ioctl raw tty, uname, mounts, wait WNOHANG, O_APPEND, process
  memory freed on reap (frame free list), strace-style trace output,
  console \x08 / \x0c, ELF loader rejects shared pages with mixed flags
- fixes: pc-keyboard Oem7 -> Oem5 so the | key works; user link.ld aligns
  before .data so .bss never lands on the .rodata page
- boot runs the shell (respawned on exit); old demos behind --features demos;
  headless boot runs a scripted shell session as the self-test
- tests/shell.rs (parser, drives, scripted session, jobs, memory reuse)
- kernel named Rustbelt 0.7.0; README shell section; new boot.png
Acat.rs+41 -0
Acp.rs+52 -0
Mdemo.rs+1 -1
Decho.rs+0 -23
Agrep.rs+85 -0
Mlink.ld+6 -1
Als.rs+126 -0
Amkdir.rs+26 -0
Arm.rs+30 -0
Ash.rs+554 -0
Ashparse.rs+236 -0
Asleep.rs+28 -0
Atouch.rs+25 -0
Mulib.rs+206 -2
Awc.rs+83 -0

15 files changed, 1499 insertions, 27 deletions

@@ -0,0 +1,41 @@+//! cat [file...] -- print files (or stdin, also for "-"). Shell alias: type.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// Copy everything from `fd` to stdout.+fn copy(fd: i64) -> i64 {+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n <= 0 {+            return n; // 0 = end of file, < 0 = error+        }+        write_all(1, &buf[..n as usize]);+    }+}++fn main() -> i64 {+    if argc() < 2 {+        return if copy(0) < 0 { 1 } else { 0 };+    }+    let mut status = 0;+    for i in 1..argc() {+        let name = arg(i).unwrap_or("");+        let fd = if name == "-" { 0 } else { open(name, 0) };+        let r = if fd < 0 { fd } else { copy(fd) };+        if r < 0 {+            eprintln!("cat: {}: {}", name, strerror(r));+            status = 1;+        }+        if fd > 0 {+            close(fd);+        }+    }+    status+}
@@ -0,0 +1,52 @@+//! cp source dest -- copy a file. If `dest` is a directory the copy goes+//! inside it with the same name (shell alias: copy).+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String};+use ulib::*;++fn main() -> i64 {+    let (Some(src), Some(dst), 3) = (arg(1), arg(2), argc()) else {+        eprintln!("usage: cp source dest");+        return 2;+    };+    let mut st = Stat::default();+    let mut dst = String::from(dst);+    if stat(&dst, &mut st) == 0 && st.kind == KIND_DIR {+        let name = src.rsplit('/').next().unwrap_or(src);+        dst = format!("{}/{}", dst.trim_end_matches('/'), name);+    }+    let from = open(src, 0);+    if from < 0 {+        eprintln!("cp: {}: {}", src, strerror(from));+        return 1;+    }+    let to = open(&dst, O_CREAT | O_TRUNC);+    if to < 0 {+        eprintln!("cp: {}: {}", dst, strerror(to));+        close(from);+        return 1;+    }+    let mut buf = [0u8; 512];+    let mut status = 0;+    loop {+        let n = read(from, &mut buf);+        if n <= 0 || write_all(to, &buf[..n.max(0) as usize]) < 0 {+            if n < 0 {+                eprintln!("cp: {}: {}", src, strerror(n));+                status = 1;+            }+            break;+        }+    }+    close(from);+    close(to);+    status+}
@@ -58,7 +58,7 @@ fn main() -> i64 {      // --- list a directory with getdents64 -----------------------------------     let dir = open("bin", 0);-    let mut dents = [0u8; 512];+    let mut dents = [0u8; 1024];     let len = getdents64(dir, &mut dents);     check(len > 0, "getdents64 bin");     let (mut saw_demo, mut saw_child) = (false, false);
echo.rs
@@ -1,23 +0,0 @@-//! Reads lines from the keyboard (fd 0) and echoes them back. "exit" quits.-#![no_std]-#![no_main]--#[macro_use]-#[path = "ulib.rs"]-mod ulib;--fn main() -> i64 {-    println!("[echo] pid {}: type a line and press Enter", ulib::getpid());-    let mut buf = [0u8; 256];-    loop {-        let n = ulib::read(0, &mut buf); // blocks until Enter-        if n <= 0 {-            return 1;-        }-        let line = core::str::from_utf8(&buf[..n as usize]).unwrap_or("?").trim_end();-        println!("[echo] you typed: {}", line);-        if line == "exit" {-            return 0;-        }-    }-}
@@ -0,0 +1,85 @@+//! grep [-i] [-v] [-n] pattern [file...] -- print lines containing+//! `pattern` (plain text, no regular expressions). -i: ignore case,+//! -v: lines that do *not* match, -n: show line numbers.+//! Exit code 0 if something matched, 1 if not, 2 on errors (like grep).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn contains(line: &[u8], pattern: &[u8], ignore_case: bool) -> bool {+    pattern.is_empty()+        || line.windows(pattern.len()).any(|w| match ignore_case {+            true => w.eq_ignore_ascii_case(pattern),+            false => w == pattern,+        })+}++fn main() -> i64 {+    let (mut ignore_case, mut invert, mut numbers) = (false, false, false);+    let mut i = 1;+    while let Some(a) = arg(i).filter(|a| a.starts_with('-') && a.len() > 1) {+        for flag in a[1..].chars() {+            match flag {+                'i' => ignore_case = true,+                'v' => invert = true,+                'n' => numbers = true,+                _ => {+                    eprintln!("grep: unknown option -{}", flag);+                    return 2;+                }+            }+        }+        i += 1;+    }+    let Some(pattern) = arg(i) else {+        eprintln!("usage: grep [-i] [-v] [-n] pattern [file...]");+        return 2;+    };+    let files = i + 1..argc();+    let many = files.len() > 1;+    let (mut matched, mut status) = (false, 0);+    let mut search = |fd: i64, name: &str| {+        let mut number = 0;+        for_each_line(fd, |line| {+            number += 1;+            if contains(line, pattern.as_bytes(), ignore_case) != invert {+                matched = true;+                if many {+                    print!("{}:", name);+                }+                if numbers {+                    print!("{}:", number);+                }+                write_all(1, line);+                write(1, b"\n");+            }+        })+    };+    if files.is_empty() {+        search(0, "");+    }+    for f in files {+        let name = arg(f).unwrap_or("");+        let fd = open(name, 0);+        let r = if fd < 0 { fd } else { search(fd, name) };+        if fd >= 0 {+            close(fd);+        }+        if r < 0 {+            eprintln!("grep: {}: {}", name, strerror(r));+            status = 2;+        }+    }+    if status != 0 {+        status+    } else if matched {+        0+    } else {+        1+    }+}
@@ -0,0 +1,126 @@+//! ls [-l] [path...] -- list directories (shell alias: dir).+//!+//! On the console the names are printed in columns; into a pipe or file+//! one per line (like the real `ls`, so `ls | wc -l` counts files).+//! -l adds sizes, DOS style: `<DIR>` or the size in bytes.+#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use alloc::{format, string::String, vec::Vec};+use ulib::*;++struct Entry {+    name: String,+    dir: bool,+    size: u64,+}++fn main() -> i64 {+    let mut long = false;+    let mut paths = Vec::new();+    for i in 1..argc() {+        match arg(i) {+            Some("-l") => long = true,+            Some(a) => paths.push(a),+            None => {}+        }+    }+    if paths.is_empty() {+        paths.push(".");+    }+    let mut st = Stat::default();+    let columns = !long && fstat(1, &mut st) == 0 && st.kind == KIND_CONSOLE;++    let mut status = 0;+    for (i, path) in paths.iter().enumerate() {+        let r = stat(path, &mut st);+        if r < 0 {+            eprintln!("ls: {}: {}", path, strerror(r));+            status = 1;+            continue;+        }+        if st.kind != KIND_DIR {+            print_entries(&[Entry { name: String::from(*path), dir: false, size: st.size }], long, columns);+            continue;+        }+        if paths.len() > 1 {+            println!("{}{}:", if i > 0 { "\n" } else { "" }, path);+        }+        match read_dir(path, long) {+            Ok(entries) => print_entries(&entries, long, columns),+            Err(e) => {+                eprintln!("ls: {}: {}", path, strerror(e));+                status = 1;+            }+        }+    }+    status+}++/// All entries of a directory, sorted by name.+fn read_dir(path: &str, long: bool) -> Result<Vec<Entry>, i64> {+    let fd = open(path, 0);+    if fd < 0 {+        return Err(fd);+    }+    let mut entries = Vec::new();+    let mut buf = [0u8; 1024];+    loop {+        // Each call returns as many records as fit; 0 = no more.+        let n = getdents64(fd, &mut buf);+        if n <= 0 {+            break;+        }+        for_each_dirent(&buf, n as usize, |name, d_type| {+            if name != "." && name != ".." {+                entries.push(Entry { name: String::from(name), dir: d_type == 4, size: 0 });+            }+        });+    }+    close(fd);+    entries.sort_by(|a, b| a.name.cmp(&b.name));+    if long {+        let mut st = Stat::default();+        for e in entries.iter_mut().filter(|e| !e.dir) {+            if stat(&format!("{}/{}", path, e.name), &mut st) == 0 {+                e.size = st.size;+            }+        }+    }+    Ok(entries)+}++fn print_entries(entries: &[Entry], long: bool, columns: bool) {+    if long {+        for e in entries {+            if e.dir {+                println!("     <DIR>  {}/", e.name);+            } else {+                println!("{:>10}  {}", e.size, e.name);+            }+        }+        return;+    }+    let shown = |e: &Entry| if e.dir { format!("{}/", e.name) } else { e.name.clone() };+    if !columns {+        entries.iter().for_each(|e| println!("{}", shown(e)));+        return;+    }+    // Columns across an 80-character screen.+    let width = entries.iter().map(|e| shown(e).len() + 2).max().unwrap_or(1);+    let per_line = (79 / width).max(1);+    for (i, e) in entries.iter().enumerate() {+        let last = i + 1 == entries.len() || (i + 1) % per_line == 0;+        if last {+            println!("{}", shown(e));+        } else {+            print!("{:<w$}", shown(e), w = width);+        }+    }+}
@@ -0,0 +1,26 @@+//! mkdir dir... -- create directories (shell alias: md).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    if argc() < 2 {+        eprintln!("usage: mkdir dir...");+        return 2;+    }+    let mut status = 0;+    for i in 1..argc() {+        let dir = arg(i).unwrap_or("");+        let r = mkdir(dir);+        if r < 0 {+            eprintln!("mkdir: {}: {}", dir, strerror(r));+            status = 1;+        }+    }+    status+}
@@ -0,0 +1,30 @@+//! rm path... -- delete files, and empty directories (shell alias: del).+//! (Real rm needs -r for directories; this one just tries rmdir.)+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    if argc() < 2 {+        eprintln!("usage: rm path...");+        return 2;+    }+    let mut status = 0;+    for i in 1..argc() {+        let path = arg(i).unwrap_or("");+        let mut r = unlink(path);+        if r == -EISDIR {+            r = rmdir(path); // fails with ENOTEMPTY if there's something in it+        }+        if r < 0 {+            eprintln!("rm: {}: {}", path, strerror(r));+            status = 1;+        }+    }+    status+}
@@ -0,0 +1,554 @@+//! sh -- the Rustbelt shell, a normal user program (/bin/sh on the disk).+//!+//!   sh               interactive: prompt, line editing, history+//!   sh script.sh     run the commands in a file (echoing each one)+//!+//! It reads a line, parses it (shparse.rs: words, `|`, `<`, `>`, `>>`, `&`,+//! drive letters), then either runs a *builtin* (cd, pwd, echo, ... -- these+//! must change the shell itself, or are just tiny) or *spawns* a program+//! from a `bin` directory, connecting pipes and files to its stdin/stdout.+//! Everything goes through the same syscalls any program could use.++#![no_std]+#![no_main]++extern crate alloc;++#[macro_use]+#[path = "ulib.rs"]+mod ulib;+#[path = "shparse.rs"]+mod shparse;++use alloc::{+    format,+    string::{String, ToString},+    vec::Vec,+};+use core::fmt::Write;+use shparse::Drive;+use ulib::*;++/// Built-in aliases (DOS names for the Unix-y programs).+const DEFAULT_ALIASES: &[(&str, &str)] = &[("dir", "ls"), ("type", "cat"), ("del", "rm"), ("md", "mkdir"), ("copy", "cp")];++const HELP: &str = "\+Rustbelt shell commands+  builtins   cd [dir]  pwd  echo [text]  ver  drives  alias [name=value]+             history  jobs  wait [pid]  cls / clear  help  exit [code]+  programs   ls [-l]  cat  wc  grep [-i] [-v]  mkdir  rm  touch  cp  sleep+  (bin dirs) hello  demo  evil  spin  child  sh+  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 background job started with `start` or `&`.+struct Job {+    pid: i64,+    command: String,+}++/// One command of a pipeline, after alias, drive letter and `run`/`start`+/// processing.+struct Stage {+    argv: Vec<String>,+    stdin: Option<String>,+    stdout: Option<(String, bool)>, // (file, append)+    trace: bool,+    program_only: bool, // `run x` always runs a program, never a builtin+}++struct Shell {+    drives: Vec<Drive>,+    aliases: Vec<(String, String)>,+    history: Vec<String>,+    jobs: Vec<Job>,+    status: i64,            // exit code of the last command+    exit: Option<i64>,      // set by the `exit` builtin+}++fn main() -> i64 {+    let mut sh = Shell {+        drives: Vec::new(),+        aliases: DEFAULT_ALIASES.iter().map(|(a, b)| (a.to_string(), b.to_string())).collect(),+        history: Vec::new(),+        jobs: Vec::new(),+        status: 0,+        exit: None,+    };++    // Where do commands come from? A script file, or stdin. If stdin is the+    // console we're interactive; otherwise (a pipe, `sh < file`) we read+    // lines just like from a script.+    let fd = match arg(1) {+        Some(path) => {+            let fd = open(path, 0);+            if fd < 0 {+                eprintln!("sh: {}: {}", path, strerror(fd));+                return 127;+            }+            fd+        }+        None => 0,+    };+    let mut st = Stat::default();+    let interactive = fd == 0 && fstat(0, &mut st) == 0 && st.kind == KIND_CONSOLE;+    let mut input = LineReader { fd, buf: Vec::new(), eof: false };++    sh.refresh_drives();+    if interactive {+        let disks: Vec<String> = sh.drives.iter().map(|d| format!("{}: = {}", d.letter, d.path)).collect();+        println!("Rustbelt shell. Type `help` for commands. Drives: {}", disks.join(", "));+    }++    while sh.exit.is_none() {+        sh.refresh_drives();+        sh.reap_jobs();+        let prompt = sh.prompt();+        let line = if interactive {+            sh.edit_line(&prompt)+        } else {+            match input.read_line() {+                Some(line) => {+                    println!("{}{}", prompt, line); // show what's being run+                    line+                }+                None => break, // end of the script+            }+        };+        sh.run_line(&line, interactive);+    }+    sh.exit.unwrap_or(sh.status)+}++impl Shell {+    /// Ask the kernel for the mount table and (re)assign drive letters.+    fn refresh_drives(&mut self) {+        let mut buf = [0u8; 1024];+        let n = mounts(&mut buf).max(0) as usize;+        self.drives = shparse::assign_drives(core::str::from_utf8(&buf[..n]).unwrap_or(""));+    }++    fn cwd(&self) -> String {+        let mut buf = [0u8; 256];+        let n = getcwd(&mut buf).max(0) as usize;+        String::from_utf8_lossy(&buf[..n]).into_owned()+    }++    /// `C:/ $ `, `D:/notes $ `+    fn prompt(&self) -> String {+        format!("{} $ ", shparse::display(&self.cwd(), &self.drives))+    }++    /// Print "[done]" for background jobs that have finished (without+    /// blocking: WNOHANG returns -EAGAIN while a job is still running).+    fn reap_jobs(&mut self) {+        self.jobs.retain(|job| {+            let code = try_wait(job.pid);+            if code == -EAGAIN {+                return true;+            }+            println!("[done] pid {} ({}), exit code {}", job.pid, job.command, code);+            false+        });+    }++    /// Read a line from the keyboard with editing. The console is put in+    /// raw mode so we get every key (and arrow keys as ESC [ A..D) and do+    /// the echo ourselves; back to normal ("cooked") before running+    /// anything, so programs see the usual line-at-a-time input.+    fn edit_line(&mut self, prompt: &str) -> String {+        print!("{}", prompt);+        set_raw(0, true);+        let mut line: Vec<u8> = Vec::new();+        let mut hist = self.history.len(); // position while browsing history+        let mut esc = 0; // 0: normal, 1: after ESC, 2: after ESC [+        let mut buf = [0u8; 16];+        'read: loop {+            let n = read(0, &mut buf);+            if n <= 0 {+                break;+            }+            for &b in &buf[..n as usize] {+                match (esc, b) {+                    (0, 0x1b) => esc = 1,+                    (1, b'[') => esc = 2,+                    (1, _) => esc = 0,+                    (2, b'0'..=b'9' | b';') => {} // parameters, e.g. ESC [ 3 ~+                    (2, key) => {+                        esc = 0;+                        // Up / Down: replace the line with an older/newer one.+                        let new = match key {+                            b'A' if hist > 0 => hist - 1,+                            b'B' if hist < self.history.len() => hist + 1,+                            _ => continue, // Left/Right/Home/... not supported+                        };+                        hist = new;+                        let text = self.history.get(hist).map(|s| s.as_bytes()).unwrap_or(b"");+                        erase(line.len());+                        line = text.to_vec();+                        write(1, &line);+                    }+                    (_, b'\n' | b'\r') => {+                        println!();+                        break 'read;+                    }+                    (_, 0x08 | 0x7f) => {+                        if line.pop().is_some() {+                            erase(1);+                        }+                    }+                    (_, 0x20..=0x7e) => {+                        line.push(b);+                        write(1, &[b]); // echo+                    }+                    _ => {} // Tab, other control keys: ignored+                }+            }+        }+        set_raw(0, false);+        String::from_utf8_lossy(&line).into_owned()+    }++    fn run_line(&mut self, line: &str, interactive: bool) {+        let line = line.trim();+        if line.is_empty() {+            return;+        }+        if interactive && self.history.last().map(|h| h.as_str()) != Some(line) {+            self.history.push(line.to_string());+        }+        match shparse::parse(line) {+            Ok(Some(pipeline)) => self.status = self.run_pipeline(pipeline, line),+            Ok(None) => {} // just a comment+            Err(e) => {+                eprintln!("sh: {}", e);+                self.status = 2;+            }+        }+    }++    /// Words -> argv strings: drive letters translated (unless quoted),+    /// aliases expanded, `run [--trace]` / `start` prefixes removed.+    fn prepare(&self, cmd: &shparse::Command, background: &mut bool) -> Result<Stage, String> {+        let path = |w: &shparse::Word| match w.quoted {+            false => shparse::translate(&w.text, &self.drives).unwrap_or_else(|| w.text.clone()),+            true => w.text.clone(),+        };+        let mut argv: Vec<String> = cmd.words.iter().map(path).collect();+        let mut stage = Stage {+            argv: Vec::new(),+            stdin: cmd.stdin.as_ref().map(path),+            stdout: cmd.stdout.as_ref().map(|(w, append)| (path(w), *append)),+            trace: false,+            program_only: false,+        };+        // `run` / `start` prefixes (`start run --trace x` works too).+        loop {+            match argv.first().map(|s| s.as_str()) {+                Some("run") => {+                    argv.remove(0);+                    stage.program_only = true;+                    if argv.first().map(|s| s.as_str()) == Some("--trace") {+                        argv.remove(0);+                        stage.trace = true;+                    }+                }+                Some("start") => {+                    argv.remove(0);+                    stage.program_only = true;+                    *background = true;+                }+                _ => break,+            }+        }+        if argv.is_empty() {+            return Err(String::from("run/start: which program?"));+        }+        // Aliases: replace the command name (a few rounds, for aliases of+        // aliases; the limit stops `alias a=a` from looping forever).+        for _ in 0..5 {+            let Some((_, value)) = self.aliases.iter().find(|(name, _)| *name == argv[0]) else { break };+            let mut words: Vec<String> = value.split_whitespace().map(|s| s.to_string()).collect();+            if words.is_empty() || words[0] == argv[0] {+                break;+            }+            words.extend(argv.drain(1..));+            argv = words;+        }+        stage.argv = argv;+        Ok(stage)+    }++    /// Run `a < in | b | c > out`: create the pipes, spawn every program+    /// with the right stdin/stdout, then wait for them (unless background).+    /// Returns the exit code of the last command.+    fn run_pipeline(&mut self, pipeline: shparse::Pipeline, line: &str) -> i64 {+        let mut background = pipeline.background;+        let mut stages = Vec::new();+        for cmd in &pipeline.commands {+            match self.prepare(cmd, &mut background) {+                Ok(stage) => stages.push(stage),+                Err(e) => {+                    eprintln!("sh: {}", e);+                    return 2;+                }+            }+        }++        let mut opened: Vec<i64> = Vec::new(); // fds to close when done+        let mut children: Vec<(i64, String)> = Vec::new();+        let mut status = 0;+        let mut prev_pipe = -1; // read end of the pipe from the previous stage+        let mut last_pid = None; // the last stage, if it's a program+        for (i, stage) in stages.iter().enumerate() {+            // stdin: `< file`, else the previous pipe, else ours (-1).+            let mut stdin = prev_pipe;+            if let Some(file) = &stage.stdin {+                stdin = open(file, 0);+                if stdin < 0 {+                    eprintln!("sh: {}: {}", file, strerror(stdin));+                    status = 1;+                    break;+                }+                opened.push(stdin);+            }+            // stdout: `> file` / `>> file`, else a new pipe to the next+            // stage, else ours (-1).+            let mut stdout = -1;+            prev_pipe = -1;+            if i + 1 < stages.len() {+                let (r, w) = pipe().unwrap_or((-1, -1));+                opened.extend([r, w]);+                stdout = w;+                prev_pipe = r;+            }+            if let Some((file, append)) = &stage.stdout {+                let flags = O_CREAT | if *append { O_APPEND } else { O_TRUNC };+                stdout = open(file, flags);+                if stdout < 0 {+                    eprintln!("sh: {}: {}", file, strerror(stdout));+                    status = 1;+                    break;+                }+                opened.push(stdout);+            }++            if !stage.program_only {+                if let Some(code) = self.builtin(&stage.argv, if stdout < 0 { 1 } else { stdout }) {+                    status = code;+                    continue;+                }+            }+            match self.spawn(stage, stdin, stdout) {+                Ok(pid) => {+                    children.push((pid, stage.argv[0].clone()));+                    if i + 1 == stages.len() {+                        last_pid = Some(pid);+                    }+                }+                Err(code) => {+                    status = code;+                    break;+                }+            }+        }+        // Close our copies of the pipe ends and files: a reader only sees+        // end-of-file once *every* write end is closed, including ours.+        for fd in opened {+            close(fd);+        }++        if background {+            for (pid, name) in children {+                println!("[pid {}] {} started in the background", pid, name);+                self.jobs.push(Job { pid, command: line.to_string() });+            }+            return 0;+        }+        for (pid, _) in &children {+            let code = wait(*pid);+            if Some(*pid) == last_pid {+                status = code; // the last stage's exit code is the pipeline's+            }+        }+        if status != 0 && last_pid.is_some() {+            println!("[exit code {}]", status);+        }+        status+    }++    /// 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, ...+    fn find_program(&self, name: &str) -> Option<String> {+        if name.contains('/') {+            return Some(name.to_string());+        }+        let mut st = Stat::default();+        self.drives.iter().map(|d| format!("{}/bin/{}", d.path.trim_end_matches('/'), name)).find(|p| stat(p, &mut st) == 0 && st.kind == KIND_FILE)+    }++    fn spawn(&self, stage: &Stage, stdin: i64, stdout: i64) -> Result<i64, i64> {+        let name = &stage.argv[0];+        let Some(path) = self.find_program(name) else {+            eprintln!("sh: {}: command not found (try `help`)", name);+            return Err(127);+        };+        // spawn takes the arguments as one NUL-separated string.+        let argv = stage.argv.join("\0");+        let opts = SpawnOptions { stdin, stdout, stderr: -1, flags: if stage.trace { SPAWN_TRACE } else { 0 } };+        let pid = spawn(&path, &argv, &opts);+        if pid < 0 {+            eprintln!("sh: {}: {}", name, strerror(pid));+            return Err(126);+        }+        Ok(pid)+    }++    /// Run a builtin, writing its output to `out`. `None` if `argv[0]`+    /// isn't one.+    fn builtin(&mut self, argv: &[String], out: i64) -> Option<i64> {+        let mut out = Fd(out);+        let arg1 = argv.get(1).map(|s| s.as_str());+        let code = match argv[0].as_str() {+            "cd" => {+                let dir = arg1.unwrap_or("/");+                let r = chdir(dir);+                if r < 0 {+                    match shparse::drive_letter(dir) {+                        // Still has its letter: translate() didn't know it.+                        Some(letter) => eprintln!("cd: there is no drive {}: (see `drives`)", letter),+                        None => eprintln!("cd: {}: {}", dir, strerror(r)),+                    }+                    1+                } else {+                    0+                }+            }+            "pwd" => {+                let _ = writeln!(out, "{}", self.cwd());+                0+            }+            "echo" => {+                let _ = writeln!(out, "{}", argv[1..].join(" "));+                0+            }+            "help" => {+                let _ = write!(out, "{}", HELP);+                0+            }+            "exit" => {+                self.exit = Some(arg1.and_then(|s| s.parse().ok()).unwrap_or(self.status));+                0+            }+            "cls" | "clear" => {+                let _ = write!(out, "\x0c"); // form feed: the console clears the screen+                0+            }+            "ver" => {+                let mut buf = [0u8; 6 * 65];+                let [sysname, _node, release, version, machine, _] = uname(&mut buf);+                let _ = writeln!(out, "{} {} ({}) {}", sysname, release, version, machine);+                0+            }+            "drives" => {+                for d in &self.drives {+                    let _ = writeln!(out, "{}:  {:<12} {}", d.letter, d.path, d.fs);+                }+                0+            }+            "alias" => match arg1 {+                None => {+                    for (name, value) in &self.aliases {+                        let _ = writeln!(out, "{} = {}", name, value);+                    }+                    0+                }+                Some(def) => match def.split_once('=') {+                    Some((name, value)) => {+                        self.aliases.retain(|(n, _)| n != name);+                        self.aliases.push((name.to_string(), value.to_string()));+                        0+                    }+                    None => {+                        eprintln!("alias: use alias name=value");+                        2+                    }+                },+            },+            "history" => {+                for (i, line) in self.history.iter().enumerate() {+                    let _ = writeln!(out, "{:4}  {}", i + 1, line);+                }+                0+            }+            "jobs" => {+                self.reap_jobs();+                for job in &self.jobs {+                    let _ = writeln!(out, "[pid {}] running  {}", job.pid, job.command);+                }+                0+            }+            "wait" => {+                // `wait` waits for all background jobs, `wait 7` for pid 7.+                let only: Option<i64> = arg1.and_then(|s| s.parse().ok());+                let mut code = 0;+                self.jobs.retain(|job| {+                    if only.is_some_and(|pid| pid != job.pid) {+                        return true;+                    }+                    code = wait(job.pid);+                    let _ = writeln!(out, "[done] pid {} ({}), exit code {}", job.pid, job.command, code);+                    false+                });+                code+            }+            _ => return None,+        };+        Some(code)+    }+}++/// Erase `n` characters before the cursor (the console treats \x08 as+/// "back one column and blank it").+fn erase(n: usize) {+    for _ in 0..n {+        write(1, b"\x08");+    }+}++/// Reads a file (or pipe) line by line.+struct LineReader {+    fd: i64,+    buf: Vec<u8>,+    eof: bool,+}++impl LineReader {+    fn read_line(&mut self) -> Option<String> {+        loop {+            if let Some(i) = self.buf.iter().position(|&b| b == b'\n') {+                let line: Vec<u8> = self.buf.drain(..=i).collect();+                return Some(String::from_utf8_lossy(&line[..i]).trim_end_matches('\r').to_string());+            }+            if self.eof {+                // The last line may lack a final newline.+                if self.buf.is_empty() {+                    return None;+                }+                let line = String::from_utf8_lossy(&self.buf).into_owned();+                self.buf.clear();+                return Some(line);+            }+            let mut chunk = [0u8; 512];+            let n = read(self.fd, &mut chunk);+            if n <= 0 {+                self.eof = true;+            } else {+                self.buf.extend_from_slice(&chunk[..n as usize]);+            }+        }+    }+}
@@ -0,0 +1,236 @@+//! The shell's parser: command line -> tokens -> pipeline, and the drive+//! letter mapping (`D:/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),+//! where it's tested without starting any processes.+//!+//! Grammar (a small subset of what a Unix shell accepts):+//!+//!   line     = pipeline [ "&" ] [ "#" comment ]+//!   pipeline = command { "|" command }+//!   command  = { word | "<" word | ">" word | ">>" word }+//!   word     = { plain-char | 'single quoted' | "double quoted" }+//!+//! `|`, `<`, `>`, `>>` and `&` are operators even without spaces around+//! them (`ls|wc` works). Quotes just group: no `$variables` or escapes.++#![allow(dead_code)]++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:.+#[derive(Debug, Clone, PartialEq)]+pub struct Word {+    pub text: String,+    pub quoted: bool,+}++#[derive(Debug, Clone, PartialEq)]+pub enum Token {+    Word(Word),+    Pipe,       // |+    In,         // <+    Out,        // >+    Append,     // >>+    Background, // &+}++/// Split a line into tokens. Errors on an unterminated quote.+pub fn tokenize(line: &str) -> Result<Vec<Token>, String> {+    let mut tokens = Vec::new();+    let mut chars = line.chars().peekable();+    // The word being built, if any (a word can mix plain and quoted parts:+    // a"b c"d is the single word `ab cd`).+    let mut word: Option<Word> = None;++    while let Some(c) = chars.next() {+        let op = match c {+            '|' => Some(Token::Pipe),+            '<' => Some(Token::In),+            '&' => Some(Token::Background),+            '>' if chars.peek() == Some(&'>') => {+                chars.next();+                Some(Token::Append)+            }+            '>' => Some(Token::Out),+            _ => None,+        };+        if op.is_some() || c.is_whitespace() || (c == '#' && word.is_none()) {+            // Anything here ends the current word.+            if let Some(w) = word.take() {+                tokens.push(Token::Word(w));+            }+            if let Some(op) = op {+                tokens.push(op);+            } else if c == '#' {+                break; // comment: ignore the rest of the line+            }+            continue;+        }+        let w = word.get_or_insert(Word { text: String::new(), quoted: false });+        if c == '\'' || c == '"' {+            w.quoted = true;+            loop {+                match chars.next() {+                    Some(q) if q == c => break,+                    Some(other) => w.text.push(other),+                    None => return Err(format!("unterminated {} quote", c)),+                }+            }+        } else {+            w.text.push(c);+        }+    }+    if let Some(w) = word {+        tokens.push(Token::Word(w));+    }+    Ok(tokens)+}++/// One program (or builtin) with its arguments and redirections.+#[derive(Debug, Clone, Default, PartialEq)]+pub struct Command {+    pub words: Vec<Word>,           // words[0] is the command name+    pub stdin: Option<Word>,        // < file+    pub stdout: Option<(Word, bool)>, // > file (false) or >> file (true)+}++/// `a | b | c`, maybe with a trailing `&` (run in the background).+#[derive(Debug, Clone, PartialEq)]+pub struct Pipeline {+    pub commands: Vec<Command>,+    pub background: bool,+}++/// Parse a line. `Ok(None)` for an empty line or a comment.+pub fn parse(line: &str) -> Result<Option<Pipeline>, String> {+    let tokens = tokenize(line)?;+    if tokens.is_empty() {+        return Ok(None);+    }+    let mut commands = Vec::new();+    let mut current = Command::default();+    let mut background = false;+    let mut iter = tokens.into_iter().peekable();++    while let Some(token) = iter.next() {+        if background {+            return Err(String::from("`&` must be at the end of the line"));+        }+        match token {+            Token::Word(w) => current.words.push(w),+            Token::Pipe => {+                if current.words.is_empty() {+                    return Err(String::from("missing command before `|`"));+                }+                commands.push(core::mem::take(&mut current));+            }+            Token::Background => background = true,+            Token::In | Token::Out | Token::Append => {+                let target = match iter.next() {+                    Some(Token::Word(w)) => w,+                    _ => return Err(String::from("missing file name after redirection")),+                };+                match token {+                    Token::In => current.stdin = Some(target),+                    Token::Out => current.stdout = Some((target, false)),+                    _ => current.stdout = Some((target, true)),+                }+            }+        }+    }+    if current.words.is_empty() {+        // e.g. `> file`, `&` or `ls |`: nothing to run.+        return Err(String::from(if commands.is_empty() { "missing command" } else { "missing command after `|`" }));+    }+    commands.push(current);+    Ok(Some(Pipeline { commands, background }))+}++// --- drive letters -----------------------------------------------------------++/// A drive letter and the directory it stands for.+#[derive(Debug, Clone, PartialEq)]+pub struct Drive {+    pub letter: char, // always upper case+    pub path: String, // absolute, no trailing slash except "/"+    pub fs: String,   // file system type, e.g. "fat32"+}++/// 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+/// disks), in the order they were mounted. Other mounts such as /tmp are+/// reachable through C: (C:/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';+    for line in mount_table.lines() {+        if let Some((path, fs)) = line.split_once(' ') {+            if path.starts_with("/mnt/") && letter <= b'Z' {+                drives.push(Drive { letter: letter as char, path: String::from(path), fs: String::from(fs.trim()) });+                letter += 1;+            }+        }+    }+    drives+}++/// Does `word` start with a drive letter ("D:", "d:/x", "E: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':' {+        Some((b[0] as char).to_ascii_uppercase())+    } else {+        None+    }+}++/// `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+/// `word` doesn't start with a known drive letter.+pub fn translate(word: &str, drives: &[Drive]) -> Option<String> {+    let letter = drive_letter(word)?;+    let drive = drives.iter().find(|d| d.letter == letter)?;+    let rest = word[2..].trim_start_matches('/');+    Some(if rest.is_empty() {+        drive.path.clone()+    } else if drive.path == "/" {+        format!("/{}", rest)+    } else {+        format!("{}/{}", drive.path, rest)+    })+}++/// The other direction, for the prompt: `/mnt/disk0/bin` -> `D:/bin`,+/// `/tmp` -> `C:/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 {+        let rest = if d.path == "/" {+            path.strip_prefix('/')+        } else {+            // "/mnt/disk0" matches "/mnt/disk0" and "/mnt/disk0/x", not "/mnt/disk01".+            path.strip_prefix(d.path.as_str())+                .filter(|r| r.is_empty() || r.starts_with('/'))+                .map(|r| r.trim_start_matches('/'))+        };+        if let Some(rest) = rest {+            if best.map_or(true, |(b, _)| d.path.len() > b.path.len()) {+                best = Some((d, rest));+            }+        }+    }+    match best {+        Some((d, rest)) => format!("{}:/{}", d.letter, rest),+        None => String::from(path),+    }+}
@@ -0,0 +1,28 @@+//! sleep N -- wait N seconds (or `sleep 250ms`). The kernel blocks the+//! thread until the timer says it's time; no CPU is used meanwhile.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    let a = arg(1).unwrap_or("");+    let ms = match a.strip_suffix("ms") {+        Some(n) => n.parse::<u64>().ok(),+        None => a.parse::<u64>().ok().map(|s| s * 1000),+    };+    match ms {+        Some(ms) => {+            sleep_ms(ms);+            0+        }+        None => {+            eprintln!("usage: sleep seconds | sleep 250ms");+            2+        }+    }+}
@@ -0,0 +1,25 @@+//! touch file... -- create empty files (existing files are left alone;+//! there are no timestamps to update yet).+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++fn main() -> i64 {+    let mut status = 0;+    for i in 1..argc() {+        let path = arg(i).unwrap_or("");+        let fd = open(path, O_CREAT); // no O_TRUNC: keep the contents+        if fd < 0 {+            eprintln!("touch: {}: {}", path, strerror(fd));+            status = 1;+        } else {+            close(fd);+        }+    }+    status+}
@@ -1,5 +1,6 @@ //! A tiny "libc" for user programs: syscall wrappers, print!/println!,-//! the `_start` entry point and a panic handler.+//! eprintln!, a small heap (so `Vec` and `String` work), the `_start` entry+//! point and a panic handler. //! //! Each user program includes it with //!     #[macro_use] #[path = "ulib.rs"] mod ulib;@@ -9,9 +10,11 @@ //! SSE/floating point disabled. That matters: the kernel doesn't save SSE //! registers on a context switch. -#![allow(dead_code)]+#![allow(dead_code, unused_macros)] +use core::alloc::{GlobalAlloc, Layout}; use core::arch::asm;+use core::cell::UnsafeCell; use core::fmt;  pub const SYS_READ: u64 = 0;@@ -21,12 +24,14 @@ pub const SYS_CLOSE: u64 = 3; pub const SYS_STAT: u64 = 4; pub const SYS_FSTAT: u64 = 5; pub const SYS_LSEEK: u64 = 8;+pub const SYS_IOCTL: u64 = 16; pub const SYS_PIPE: u64 = 22; pub const SYS_SCHED_YIELD: u64 = 24; pub const SYS_SLEEP_MS: u64 = 35; pub const SYS_GETPID: u64 = 39; pub const SYS_EXIT: u64 = 60; pub const SYS_WAIT: u64 = 61;+pub const SYS_UNAME: u64 = 63; pub const SYS_GETCWD: u64 = 79; pub const SYS_CHDIR: u64 = 80; pub const SYS_MKDIR: u64 = 83;@@ -34,15 +39,20 @@ pub const SYS_RMDIR: u64 = 84; pub const SYS_UNLINK: u64 = 87; pub const SYS_GETDENTS64: u64 = 217; pub const SYS_SPAWN: u64 = 500;+pub const SYS_MOUNTS: u64 = 501;  pub const O_CREAT: u64 = 0o100; pub const O_TRUNC: u64 = 0o1000;+pub const O_APPEND: u64 = 0o2000;+pub const TTY_SET_RAW: u64 = 1;+pub const WNOHANG: u64 = 1; pub const SEEK_SET: u64 = 0; pub const SEEK_CUR: u64 = 1; pub const SEEK_END: u64 = 2; pub const SPAWN_TRACE: u64 = 1;  pub const ENOENT: i64 = 2;+pub const EAGAIN: i64 = 11; pub const EBADF: i64 = 9; pub const ECHILD: i64 = 10; pub const EFAULT: i64 = 14;@@ -163,6 +173,29 @@ pub fn exit(code: i64) -> ! { pub fn wait(pid: i64) -> i64 {     syscall3(SYS_WAIT, pid as u64, 0, 0) }+/// Like `wait`, but don't block: -EAGAIN while the child is still running.+pub fn try_wait(pid: i64) -> i64 {+    syscall3(SYS_WAIT, pid as u64, WNOHANG, 0)+}+/// Console raw mode on/off (keys arrive one by one, no echo). Returns the+/// old mode, or -ENOTTY if `fd` isn't the console.+pub fn set_raw(fd: i64, raw: bool) -> i64 {+    syscall3(SYS_IOCTL, fd as u64, TTY_SET_RAW, raw as u64)+}+/// Linux's `struct utsname`: sysname, nodename, release, version, machine,+/// domainname. Returns them as strings.+pub fn uname(buf: &mut [u8; 6 * 65]) -> [&str; 6] {+    syscall3(SYS_UNAME, buf.as_mut_ptr() as u64, 0, 0);+    core::array::from_fn(|i| {+        let field = &buf[i * 65..(i + 1) * 65];+        let len = field.iter().position(|&b| b == 0).unwrap_or(65);+        core::str::from_utf8(&field[..len]).unwrap_or("?")+    })+}+/// The mount table as text ("path fstype" lines); returns the length.+pub fn mounts(buf: &mut [u8]) -> i64 {+    syscall3(SYS_MOUNTS, buf.as_mut_ptr() as u64, buf.len() as u64, 0)+} /// Current directory into `buf`; returns its length (or -errno). pub fn getcwd(buf: &mut [u8]) -> i64 {     syscall3(SYS_GETCWD, buf.as_mut_ptr() as u64, buf.len() as u64, 0)@@ -229,6 +262,79 @@ pub fn arg(i: usize) -> Option<&'static str> {     } } +/// Write all of `buf` (pipes and files may take less than all at once).+pub fn write_all(fd: i64, mut buf: &[u8]) -> i64 {+    while !buf.is_empty() {+        let n = write(fd, buf);+        if n <= 0 {+            return n;+        }+        buf = &buf[n as usize..];+    }+    0+}++/// Call `f` for each line read from `fd` (without the '\n'). Lines longer+/// than 1 KiB come in pieces. Returns 0, or -errno if a read fails.+pub fn for_each_line(fd: i64, mut f: impl FnMut(&[u8])) -> i64 {+    let mut line = [0u8; 1024];+    let mut len = 0;+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n < 0 {+            return n;+        }+        if n == 0 {+            if len > 0 {+                f(&line[..len]); // last line without a newline+            }+            return 0;+        }+        for &b in &buf[..n as usize] {+            if b == b'\n' || len == line.len() {+                f(&line[..len]);+                len = 0;+                if b == b'\n' {+                    continue;+                }+            }+            line[len] = b;+            len += 1;+        }+    }+}++/// `ls`, `cat` etc. print errors like "cat: x: No such file or directory".+pub fn strerror(errno: i64) -> &'static str {+    match errno.abs() {+        2 => "No such file or directory",+        5 => "I/O error",+        7 => "Argument list too long",+        8 => "Exec format error",+        9 => "Bad file descriptor",+        10 => "No child processes",+        11 => "Try again",+        12 => "Out of memory",+        14 => "Bad address",+        16 => "Device or resource busy",+        17 => "File exists",+        20 => "Not a directory",+        21 => "Is a directory",+        22 => "Invalid argument",+        24 => "Too many open files",+        25 => "Not a terminal",+        28 => "No space left on device",+        29 => "Illegal seek",+        32 => "Broken pipe",+        34 => "Result too large",+        36 => "File name too long",+        38 => "Function not implemented",+        39 => "Directory not empty",+        _ => "Unknown error",+    }+}+ pub struct Stdout;  impl fmt::Write for Stdout {@@ -245,6 +351,32 @@ macro_rules! print {     }}; } +pub struct Stderr;++impl fmt::Write for Stderr {+    fn write_str(&mut self, s: &str) -> fmt::Result {+        write(2, s.as_bytes());+        Ok(())+    }+}++/// Write formatted text to any fd: `fprint!(fd, "...")`.+pub struct Fd(pub i64);++impl fmt::Write for Fd {+    fn write_str(&mut self, s: &str) -> fmt::Result {+        write_all(self.0, s.as_bytes());+        Ok(())+    }+}++macro_rules! eprintln {+    ($($arg:tt)*) => {{+        use core::fmt::Write;+        let _ = writeln!($crate::ulib::Stderr, $($arg)*);+    }};+}+ macro_rules! println {     ($($arg:tt)*) => {{         use core::fmt::Write;@@ -268,3 +400,75 @@ fn panic(info: &core::panic::PanicInfo) -> ! {     println!("[user panic] {}", info);     exit(101) }++// --- heap ----------------------------------------------------------------+//+// A tiny allocator so programs can use `Vec`, `String`, `format!` (with+// `extern crate alloc;`). Memory comes from a 64 KiB array in .bss, which+// the ELF loader maps as zeroed pages -- no `brk`/`mmap` syscall needed.+// Same idea as the kernel's fixed-size-block allocator: sizes are rounded up+// to a power of two (16 B .. 32 KiB) and freed blocks go on one free list+// per size, to be reused by the next allocation of that size. Programs are+// single-threaded, so no lock.++const HEAP_SIZE: usize = 64 * 1024;+const CLASSES: usize = 12; // 16 << 11 = 32 KiB++#[repr(C, align(4096))]+struct Heap {+    memory: UnsafeCell<[u8; HEAP_SIZE]>,+    used: UnsafeCell<usize>,               // bump pointer: bytes handed out so far+    free: UnsafeCell<[usize; CLASSES]>,    // free list heads (addresses, 0 = empty)+}++unsafe impl Sync for Heap {} // single-threaded programs++#[global_allocator]+static HEAP: Heap = Heap {+    memory: UnsafeCell::new([0; HEAP_SIZE]),+    used: UnsafeCell::new(0),+    free: UnsafeCell::new([0; CLASSES]),+};++/// Size class for a layout: 0 = 16 bytes, 1 = 32, ...+fn class_of(layout: Layout) -> usize {+    let size = layout.size().max(layout.align()).max(16).next_power_of_two();+    size.trailing_zeros() as usize - 4+}++unsafe impl GlobalAlloc for Heap {+    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {+        let class = class_of(layout);+        if class >= CLASSES {+            return core::ptr::null_mut(); // too big: "out of memory"+        }+        unsafe {+            let free = &mut *self.free.get();+            if free[class] != 0 {+                // Pop a freed block; its first 8 bytes hold the next one.+                let block = free[class] as *mut u8;+                free[class] = *(block as *const usize);+                return block;+            }+            // Carve a new block. Blocks are aligned to their size, which+            // covers any alignment up to that size.+            let size = 16 << class;+            let used = &mut *self.used.get();+            let start = (*used).next_multiple_of(size);+            if start + size > HEAP_SIZE {+                return core::ptr::null_mut();+            }+            *used = start + size;+            (self.memory.get() as *mut u8).add(start)+        }+    }++    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {+        let class = class_of(layout);+        unsafe {+            let free = &mut *self.free.get();+            *(ptr as *mut usize) = free[class];+            free[class] = ptr as usize;+        }+    }+}
@@ -0,0 +1,83 @@+//! wc [-l] [file...] -- count lines, words and bytes (stdin if no files).+//! -l: only lines.+#![no_std]+#![no_main]++#[macro_use]+#[path = "ulib.rs"]+mod ulib;++use ulib::*;++/// (lines, words, bytes) of everything readable from `fd`.+fn count(fd: i64) -> Result<[u64; 3], i64> {+    let mut c = [0u64; 3];+    let mut in_word = false;+    let mut buf = [0u8; 512];+    loop {+        let n = read(fd, &mut buf);+        if n < 0 {+            return Err(n);+        }+        if n == 0 {+            return Ok(c);+        }+        for &b in &buf[..n as usize] {+            c[2] += 1;+            if b == b'\n' {+                c[0] += 1;+            }+            let space = b == b' ' || b == b'\n' || b == b'\t' || b == b'\r';+            if !space && !in_word {+                c[1] += 1; // a word starts here+            }+            in_word = !space;+        }+    }+}++fn show(c: [u64; 3], lines_only: bool, name: &str) {+    let sep = if name.is_empty() { "" } else { " " };+    if lines_only {+        println!("{}{}{}", c[0], sep, name);+    } else {+        println!("{:>7} {:>7} {:>7}{}{}", c[0], c[1], c[2], sep, name);+    }+}++fn main() -> i64 {+    let lines_only = arg(1) == Some("-l");+    let first = if lines_only { 2 } else { 1 };+    if argc() <= first {+        return match count(0) {+            Ok(c) => {+                show(c, lines_only, "");+                0+            }+            Err(_) => 1,+        };+    }+    let (mut total, mut status) = ([0u64; 3], 0);+    for i in first..argc() {+        let name = arg(i).unwrap_or("");+        let fd = open(name, 0);+        let r = if fd < 0 { Err(fd) } else { count(fd) };+        if fd >= 0 {+            close(fd);+        }+        match r {+            Ok(c) => {+                show(c, lines_only, name);+                (0..3).for_each(|k| total[k] += c[k]);+            }+            Err(e) => {+                eprintln!("wc: {}: {}", name, strerror(e));+                status = 1;+            }+        }+    }+    if argc() - first > 1 {+        show(total, lines_only, "total");+    }+    status+}