Skip to content

Commit 6ed85f7

Browse files
ryanbreenclaude
andcommitted
feat(coreutils): add true, false, head, tail, wc utilities
Implements 5 new POSIX-compatible coreutils for the ext2 filesystem: - true: always exits with status 0 (16 bytes) - false: always exits with status 1 (17 bytes) - head: output first N lines of files (-n option, default 10) - tail: output last N lines of files (-n option, default 10) - wc: word, line, and byte count (-l/-w/-c options) All utilities: - Support reading from stdin when no files specified - Support multiple file arguments with headers - Use libbreenix syscall wrappers - Follow POSIX conventions Updated build.sh and Cargo.toml to include new binaries. Updated create_ext2_disk.sh to install in ext2 /bin/. Regenerated testdata/ext2.img with all 13 coreutils. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 0c3ecff commit 6ed85f7

File tree

9 files changed

+887
-5
lines changed

9 files changed

+887
-5
lines changed

scripts/create_ext2_disk.sh

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#
44
# This script creates a 4MB ext2 filesystem image with:
55
# - Test files for filesystem testing
6-
# - Coreutils binaries in /bin/ (cat, ls, echo, mkdir, rmdir, rm, cp, mv)
6+
# - Coreutils binaries in /bin/ (cat, ls, echo, mkdir, rmdir, rm, cp, mv, true, false, head, tail, wc)
77
# - hello_world binary for exec testing
88
#
99
# Requires Docker on macOS (or mke2fs on Linux).
@@ -25,7 +25,7 @@ TESTDATA_FILE="$PROJECT_ROOT/testdata/ext2.img"
2525
SIZE_MB=4
2626

2727
# Coreutils to install in /bin
28-
COREUTILS="cat ls echo mkdir rmdir rm cp mv"
28+
COREUTILS="cat ls echo mkdir rmdir rm cp mv true false head tail wc"
2929

3030
echo "Creating ext2 disk image..."
3131
echo " Output: $OUTPUT_FILE"
@@ -76,7 +76,7 @@ if [[ "$(uname)" == "Darwin" ]]; then
7676
7777
# Copy coreutils binaries
7878
echo "Installing coreutils in /bin..."
79-
for bin in cat ls echo mkdir rmdir rm cp mv; do
79+
for bin in cat ls echo mkdir rmdir rm cp mv true false head tail wc; do
8080
if [ -f /binaries/${bin}.elf ]; then
8181
cp /binaries/${bin}.elf /mnt/ext2/bin/${bin}
8282
chmod 755 /mnt/ext2/bin/${bin}
@@ -148,7 +148,7 @@ else
148148

149149
# Copy coreutils binaries
150150
echo "Installing coreutils in /bin..."
151-
for bin in cat ls echo mkdir rmdir rm cp mv; do
151+
for bin in cat ls echo mkdir rmdir rm cp mv true false head tail wc; do
152152
if [ -f "$USERSPACE_DIR/${bin}.elf" ]; then
153153
cp "$USERSPACE_DIR/${bin}.elf" "$MOUNT_DIR/bin/${bin}"
154154
chmod 755 "$MOUNT_DIR/bin/${bin}"
@@ -196,7 +196,9 @@ if [[ -f "$OUTPUT_FILE" ]]; then
196196
echo " Size: $SIZE"
197197
echo ""
198198
echo "Contents:"
199-
echo " /bin/cat, ls, echo, mkdir, rmdir, rm, cp, mv - coreutils"
199+
echo " /bin/cat, ls, echo, mkdir, rmdir, rm, cp, mv - file coreutils"
200+
echo " /bin/true, false - exit status coreutils"
201+
echo " /bin/head, tail, wc - text processing coreutils"
200202
echo " /bin/hello_world - exec test binary (exit code 42)"
201203
echo " /hello.txt - test file"
202204
echo " /test/nested.txt - nested test file"

testdata/ext2.img

0 Bytes
Binary file not shown.

userspace/tests/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,6 +275,26 @@ path = "cp.rs"
275275
name = "mv"
276276
path = "mv.rs"
277277

278+
[[bin]]
279+
name = "true"
280+
path = "true.rs"
281+
282+
[[bin]]
283+
name = "false"
284+
path = "false.rs"
285+
286+
[[bin]]
287+
name = "head"
288+
path = "head.rs"
289+
290+
[[bin]]
291+
name = "tail"
292+
path = "tail.rs"
293+
294+
[[bin]]
295+
name = "wc"
296+
path = "wc.rs"
297+
278298
[[bin]]
279299
name = "fork_memory_test"
280300
path = "fork_memory_test.rs"

userspace/tests/build.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,11 @@ BINARIES=(
114114
"rm"
115115
"cp"
116116
"mv"
117+
"true"
118+
"false"
119+
"head"
120+
"tail"
121+
"wc"
117122
)
118123

119124
echo "Building ${#BINARIES[@]} userspace binaries with libbreenix..."

userspace/tests/false.rs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
//! false - return unsuccessful exit status
2+
//!
3+
//! Usage: false
4+
//!
5+
//! Exit with a status code indicating failure (1).
6+
//! This command does nothing and always fails.
7+
8+
#![no_std]
9+
#![no_main]
10+
11+
use core::panic::PanicInfo;
12+
use libbreenix::process::exit;
13+
14+
#[no_mangle]
15+
pub extern "C" fn _start() -> ! {
16+
exit(1);
17+
}
18+
19+
#[panic_handler]
20+
fn panic(_info: &PanicInfo) -> ! {
21+
exit(1);
22+
}

userspace/tests/head.rs

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
//! head - output the first part of files
2+
//!
3+
//! Usage: head [-n NUM] [FILE...]
4+
//!
5+
//! Print the first 10 lines of each FILE to standard output.
6+
//! With more than one FILE, precede each with a header giving the file name.
7+
//!
8+
//! Options:
9+
//! -n NUM print the first NUM lines instead of the first 10
10+
11+
#![no_std]
12+
#![no_main]
13+
14+
use core::arch::naked_asm;
15+
use core::panic::PanicInfo;
16+
use libbreenix::argv;
17+
use libbreenix::errno::Errno;
18+
use libbreenix::fs::{close, open, read, O_RDONLY};
19+
use libbreenix::io::{stdout, stderr};
20+
use libbreenix::process::exit;
21+
22+
const DEFAULT_LINES: usize = 10;
23+
const BUF_SIZE: usize = 4096;
24+
25+
/// Parse a number from bytes
26+
fn parse_num(s: &[u8]) -> Option<usize> {
27+
let mut n: usize = 0;
28+
for &c in s {
29+
if c < b'0' || c > b'9' {
30+
return None;
31+
}
32+
n = n.checked_mul(10)?.checked_add((c - b'0') as usize)?;
33+
}
34+
Some(n)
35+
}
36+
37+
/// Read and output up to `max_lines` lines from stdin
38+
fn head_stdin(max_lines: usize) -> Result<(), Errno> {
39+
let mut buf = [0u8; BUF_SIZE];
40+
let mut lines_output = 0;
41+
42+
'outer: loop {
43+
let n = libbreenix::io::read(0, &mut buf);
44+
if n < 0 {
45+
return Err(Errno::from_raw(-n));
46+
}
47+
if n == 0 {
48+
break; // EOF
49+
}
50+
51+
// Output bytes, counting newlines
52+
for i in 0..n as usize {
53+
let _ = stdout().write(&buf[i..i + 1]);
54+
if buf[i] == b'\n' {
55+
lines_output += 1;
56+
if lines_output >= max_lines {
57+
break 'outer;
58+
}
59+
}
60+
}
61+
}
62+
Ok(())
63+
}
64+
65+
/// Read and output up to `max_lines` lines from a file
66+
fn head_file(path: &[u8], max_lines: usize) -> Result<(), Errno> {
67+
let mut path_buf = [0u8; 256];
68+
let len = path.len().min(255);
69+
path_buf[..len].copy_from_slice(&path[..len]);
70+
path_buf[len] = 0;
71+
72+
let path_str = match core::str::from_utf8(&path_buf[..len + 1]) {
73+
Ok(s) => s,
74+
Err(_) => return Err(Errno::EINVAL),
75+
};
76+
77+
let fd = open(path_str, O_RDONLY)?;
78+
79+
let mut buf = [0u8; BUF_SIZE];
80+
let mut lines_output = 0;
81+
82+
'outer: loop {
83+
let n = read(fd, &mut buf)?;
84+
if n == 0 {
85+
break; // EOF
86+
}
87+
88+
for i in 0..n {
89+
let _ = stdout().write(&buf[i..i + 1]);
90+
if buf[i] == b'\n' {
91+
lines_output += 1;
92+
if lines_output >= max_lines {
93+
break 'outer;
94+
}
95+
}
96+
}
97+
}
98+
99+
let _ = close(fd);
100+
Ok(())
101+
}
102+
103+
fn print_header(path: &[u8]) {
104+
let _ = stdout().write_str("==> ");
105+
let _ = stdout().write(path);
106+
let _ = stdout().write_str(" <==\n");
107+
}
108+
109+
fn print_error_bytes(path: &[u8], e: Errno) {
110+
let _ = stderr().write_str("head: ");
111+
let _ = stderr().write(path);
112+
let _ = stderr().write_str(": ");
113+
let _ = stderr().write_str(match e {
114+
Errno::ENOENT => "No such file or directory",
115+
Errno::EACCES => "Permission denied",
116+
Errno::EISDIR => "Is a directory",
117+
Errno::EINVAL => "Invalid argument",
118+
_ => "Error",
119+
});
120+
let _ = stderr().write(b"\n");
121+
}
122+
123+
#[unsafe(naked)]
124+
#[no_mangle]
125+
pub extern "C" fn _start() -> ! {
126+
naked_asm!(
127+
"mov rdi, rsp",
128+
"and rsp, -16",
129+
"call {main}",
130+
"ud2",
131+
main = sym rust_main,
132+
)
133+
}
134+
135+
extern "C" fn rust_main(stack_ptr: *const u64) -> ! {
136+
let args = unsafe { argv::get_args_from_stack(stack_ptr) };
137+
138+
let mut max_lines = DEFAULT_LINES;
139+
let mut file_start_idx = 1usize;
140+
141+
// Parse -n option
142+
if args.argc >= 2 {
143+
if let Some(arg) = args.argv(1) {
144+
if arg.len() >= 2 && arg[0] == b'-' && arg[1] == b'n' {
145+
// -nNUM or -n NUM
146+
if arg.len() > 2 {
147+
// -nNUM format
148+
if let Some(n) = parse_num(&arg[2..]) {
149+
max_lines = n;
150+
file_start_idx = 2;
151+
} else {
152+
let _ = stderr().write_str("head: invalid number of lines\n");
153+
exit(1);
154+
}
155+
} else if args.argc >= 3 {
156+
// -n NUM format
157+
if let Some(num_arg) = args.argv(2) {
158+
if let Some(n) = parse_num(num_arg) {
159+
max_lines = n;
160+
file_start_idx = 3;
161+
} else {
162+
let _ = stderr().write_str("head: invalid number of lines\n");
163+
exit(1);
164+
}
165+
}
166+
}
167+
}
168+
}
169+
}
170+
171+
// If no files, read from stdin
172+
if file_start_idx >= args.argc {
173+
if let Err(_e) = head_stdin(max_lines) {
174+
let _ = stderr().write_str("head: error reading stdin\n");
175+
exit(1);
176+
}
177+
exit(0);
178+
}
179+
180+
// Process files
181+
let mut exit_code = 0;
182+
let file_count = args.argc - file_start_idx;
183+
let mut first_file = true;
184+
185+
for i in file_start_idx..args.argc {
186+
if let Some(path) = args.argv(i) {
187+
// Print header if multiple files
188+
if file_count > 1 {
189+
if !first_file {
190+
let _ = stdout().write(b"\n");
191+
}
192+
print_header(path);
193+
}
194+
first_file = false;
195+
196+
if let Err(e) = head_file(path, max_lines) {
197+
print_error_bytes(path, e);
198+
exit_code = 1;
199+
}
200+
}
201+
}
202+
203+
exit(exit_code);
204+
}
205+
206+
#[panic_handler]
207+
fn panic(_info: &PanicInfo) -> ! {
208+
let _ = stderr().write_str("head: panic!\n");
209+
exit(2);
210+
}

0 commit comments

Comments
 (0)