foundry
Foundry OS: the operating system built on the Rustbelt kernel
| author | Tyler Fowler <tyler@localhost> | 2026-10-30 01:22:29 -0700 |
|---|---|---|
| commit | b10b990e384b578fd41000614e51b20a726c3103 | |
| parent | 519a3fb17204c73a6a4fa94ef6433ef32866c3bb | |
| tree | browse 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
| A | cat.rs | +41 -0 |
| A | cp.rs | +52 -0 |
| M | demo.rs | +1 -1 |
| D | echo.rs | +0 -23 |
| A | grep.rs | +85 -0 |
| M | link.ld | +6 -1 |
| A | ls.rs | +126 -0 |
| A | mkdir.rs | +26 -0 |
| A | rm.rs | +30 -0 |
| A | sh.rs | +554 -0 |
| A | shparse.rs | +236 -0 |
| A | sleep.rs | +28 -0 |
| A | touch.rs | +25 -0 |
| M | ulib.rs | +206 -2 |
| A | wc.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+ }+}
@@ -17,7 +17,12 @@ SECTIONS { . = 0x8000400000; .text : ALIGN(4K) { *(.text .text.* .ltext .ltext.*) } :text .rodata : ALIGN(4K) { *(.rodata .rodata.* .lrodata .lrodata.*) } :rodata- .data : ALIGN(4K) { *(.data .data.* .ldata .ldata.* .got .got.*) } :data+ /* Align the location counter itself, not just .data: a program with no+ .data at all (e.g. sleep) would otherwise get its .bss on the+ read-only .rodata page (the linker drops empty sections, alignment+ included) and crash on its first write to a static. */+ . = ALIGN(4K);+ .data : { *(.data .data.* .ldata .ldata.* .got .got.*) } :data .bss : ALIGN(8) { *(.bss .bss.* .lbss .lbss.* COMMON) } :data /DISCARD/ : { *(.comment) *(.eh_frame*) *(.note*) } }
@@ -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+}