Flash 156 lines
// grep — print the lines of its input that contain a literal pattern, for
// /bin/grep.
//
// grep [-i] PATTERN [FILE...]
//
// With no FILE it reads fd 0 (the `cat foo | grep bar` pipe case); with one or
// more FILEs it opens each and searches it in turn. Matching lines go to fd 1,
// each followed by a newline. `-i` folds ASCII case on both sides. The pattern
// is a literal substring — no regex (the product vision's first cut). An empty
// pattern matches every line (`grep '' FILE`), the GNU convention.
//
// The matcher itself is the pure, host-tested grep_match.lineContains; this
// file is only the driver: flag/argv parsing, open/read, and streaming line
// assembly. Streaming (rather than slurp-the-whole-file like /bin/less) is what
// lets the same code path serve an unseekable pipe and a regular file alike.
//
// Deliberate scope limits (hobby-coreutil grade, documented not hidden):
// * No filename prefix on matches, even with multiple FILEs — bare matching
// lines only (GNU grep prefixes "file:" once >1 file). The shell's use is
// single-file / pipe, which this matches exactly.
// * A line longer than LINE_MAX bytes is matched and printed truncated to its
// first LINE_MAX bytes; the overrun is scanned for the newline but dropped.
// Serial-console lines sit far below the cap.
// * Exit status is not distinguished (match vs no-match vs error); fsh has no
// `$?` yet. Errors still go to fd 2 so they are visible.
//
// Same coreutil recipe as cat / ls: flibc _start shim, flibc_mem, stack buffers
// only (rule 1 — no heap, no .bss). Kept out of the CI FSH_SCRIPT so the boot
// free-page baseline stays deterministic.
use flibc
use syscall_defs as defs
use grep_match
link "flibc_start"
link "flibc_mem"
// Read granularity from the source fd. One syscall per CHUNK bytes.
const CHUNK usize = 512
// Longest line we buffer for matching/printing. A longer line is scanned for
// its newline but only its first LINE_MAX bytes are tested and emitted.
const LINE_MAX usize = 1024
// Pattern copy bound. PATTERN comes in as a cstr argv slot; we copy it into a
// sized slice so the matcher takes a plain []u8 and the length is bounded.
const PAT_MAX usize = 256
fn diag(msg []u8) {
_ = flibc.sys.write_fd(2, msg.ptr, msg.len)
}
// Emit `ln` to fd 1 (plus the newline stripped during scanning) when it matches.
fn emitMatch(ln []u8, pat []u8, ignore_case bool) {
if grep_match.lineContains(ln, pat, ignore_case) {
_ = flibc.sys.write_fd(1, ln.ptr, ln.len)
const nl []u8 = "\n"
_ = flibc.sys.write_fd(1, nl.ptr, nl.len)
}
}
// Read `fd` to EOF, splitting on '\n' and testing each line. The newline is not
// stored; a final line with no trailing newline is still tested at EOF.
fn grepStream(fd i32, pat []u8, ignore_case bool) {
var chunk [CHUNK]u8 = undefined
var line [LINE_MAX]u8 = undefined
var line_len usize = 0
while true {
n := flibc.sys.read(fd, &chunk, chunk.len)
if n <= 0 {
break
}
var i usize = 0
const got usize = #intCast(n)
while i < got {
const c = chunk[i]
i += 1
if c == '\n' {
emitMatch(line[0..line_len], pat, ignore_case)
line_len = 0
} else if line_len < LINE_MAX {
line[line_len] = c
line_len += 1
}
// else: line past LINE_MAX — drop the byte, keep scanning for '\n'.
}
}
if line_len > 0 {
emitMatch(line[0..line_len], pat, ignore_case)
}
}
export fn main(argc usize, argv argv) noreturn {
var ai usize = 1
var ignore_case bool = false
// Leading flags: only -i (case-insensitive), bundled chars allowed (-i is
// the lone option, so this is forward room). Parsing stops at the first
// non-flag arg, at a bare "-", or at end of argv.
while ai < argc {
const arg = argv[ai] orelse break
if arg[0] != '-' || arg[1] == 0 {
break
}
var fi usize = 1
while arg[fi] != 0 {
if arg[fi] == 'i' {
ignore_case = true
} else {
diag("grep: unknown option\n")
flibc.exit()
}
fi += 1
}
ai += 1
}
// PATTERN is required. (Flash `orelse` takes a single expression, not a
// block — so guard the missing-arg case first, then unwrap.)
if ai >= argc {
diag("usage: grep [-i] PATTERN [FILE...]\n")
flibc.exit()
}
const pat_arg = argv[ai] orelse flibc.exit()
ai += 1
// Copy PATTERN into a sized slice (cstr -> []u8, bounded by PAT_MAX).
var pat_buf [PAT_MAX]u8 = undefined
var pat_len usize = 0
while pat_arg[pat_len] != 0 && pat_len < PAT_MAX {
pat_buf[pat_len] = pat_arg[pat_len]
pat_len += 1
}
const pat = pat_buf[0..pat_len]
if ai >= argc {
// No FILE — search stdin (the pipe case).
grepStream(0, pat, ignore_case)
} else {
while ai < argc {
const path = argv[ai] orelse break
ai += 1
const fd = flibc.sys.open(path)
if fd < 0 {
var msg []u8 = "grep: cannot open\n"
if fd == -defs.EACCES {
msg = "grep: Permission denied\n"
}
diag(msg)
continue
}
grepStream(fd, pat, ignore_case)
_ = flibc.sys.close(fd)
}
}
flibc.exit()
}