This commit is contained in:
Raven Scott
2026-04-03 03:19:48 -04:00
parent 7435f4b353
commit 8505077f8d
11 changed files with 979 additions and 43 deletions
+3 -1
View File
@@ -31,7 +31,9 @@ Or `node packages/bare-os-coreutils/build.mjs`.
## Commands (current)
`basename`, `cat`, `clear`, `crontab`, `date`, `dirname`, `echo`, `env`, `exit`, `false`, `head`, `hdms`, `help`, `hostname`, `id`, `login`, `logout`, `ls`, `nl`, `pathchk`, `printenv`, `pwd`, `rm`, `savevault`, `seq`, `sleep`, `sort`, `tail`, `test`, `touch`, `true`, `tty`, `uname`, `wc`, `which`, `whoami`
`basename`, `cat`, `clear`, `crontab`, `date`, `dirname`, `echo`, `env`, `exit`, `false`, `grep`, `head`, `hdms`, `help`, `hostname`, `id`, `login`, `logout`, `ls`, `nl`, `pathchk`, `printenv`, `pwd`, `rm`, `savevault`, `seq`, `sleep`, `sort`, `tail`, `test`, `touch`, `true`, `tty`, `uname`, `wc`, `which`, `whoami`
**`grep`** uses JavaScript `RegExp` (and `-F` fixed strings); it is a POSIX/GNU-like **subset**, not byte-identical to GNU grep.
See [handbook command reference](../../handbook/06-kernel-and-binaries.md) for behavior notes (e.g. `ls -a`, `crontab` identity gating).
+1
View File
@@ -18,6 +18,7 @@ const commands = [
'env',
'exit',
'false',
'grep',
'head',
'hdms',
'help',
+291
View File
@@ -0,0 +1,291 @@
/**
* Subset of POSIX/GNU grep behavior using JavaScript RegExp / string search.
* Not bit-identical to GNU grep (no PCRE, different escaping, UTF-16 strings).
*/
async function run(ctx, argv) {
const vfs = ctx.vfs
const patterns = []
const patternFiles = []
let fixed = false
let icase = false
let invert = false
let numbers = false
let countOnly = false
let listFiles = false
let quiet = false
let suppressErrors = false
let forceFilename = false
let noFilename = false
const args = argv.slice(1)
let i = 0
function usage() {
ctx.console.error(
'usage: grep [-E|-F] [-i] [-v] [-n] [-c] [-l] [-q] [-s] [-H|-h] [-e pat] ... [-f file] ... [pattern] [file...]'
)
ctx.exitCode = 2
}
while (i < args.length) {
const a = args[i]
if (a === '--') {
i++
break
}
if (a === '-' || !a.startsWith('-')) break
if (a.startsWith('--')) {
ctx.console.error('grep: unknown option ' + a)
ctx.exitCode = 2
return
}
if (a === '-e') {
if (i + 1 >= args.length) {
usage()
return
}
patterns.push(args[++i])
i++
continue
}
if (a.startsWith('-e') && a.length > 2) {
patterns.push(a.slice(2))
i++
continue
}
if (a === '-f') {
if (i + 1 >= args.length) {
usage()
return
}
patternFiles.push(args[++i])
i++
continue
}
if (a.startsWith('-f') && a.length > 2) {
patternFiles.push(a.slice(2))
i++
continue
}
const rest = a.slice(1)
for (let j = 0; j < rest.length; j++) {
const c = rest[j]
switch (c) {
case 'E':
/* Accepted for GNU compatibility; patterns use JS RegExp (ERE-like). */
break
case 'F':
fixed = true
break
case 'i':
icase = true
break
case 'v':
invert = true
break
case 'n':
numbers = true
break
case 'c':
countOnly = true
break
case 'l':
listFiles = true
break
case 'q':
quiet = true
break
case 's':
suppressErrors = true
break
case 'H':
forceFilename = true
break
case 'h':
noFilename = true
break
default:
ctx.console.error('grep: invalid option -- ' + c)
ctx.exitCode = 2
return
}
}
i++
}
for (const pf of patternFiles) {
try {
const buf = await vfs.readFile(pf)
if (!buf) {
if (!suppressErrors) ctx.console.error('grep: ' + pf + ': No such file')
ctx.exitCode = 2
return
}
const t = ctx.b4a.toString(buf)
for (const line of t.split(/\r?\n/)) {
const s = line.trimEnd()
if (s === '' || s.startsWith('#')) continue
patterns.push(s)
}
} catch (e) {
if (!suppressErrors)
ctx.console.error('grep: ' + pf + ': ' + (e.message || e))
ctx.exitCode = 2
return
}
}
const rest = args.slice(i)
if (patterns.length === 0) {
if (rest.length === 0) {
usage()
return
}
patterns.push(rest.shift())
}
const fileArgs = rest
const useStdin = fileArgs.length === 0
const inputs = useStdin
? [{ label: null, path: null }]
: fileArgs.map((p) => ({ label: p, path: p }))
let matchers
try {
matchers = buildMatchers(patterns, { fixed, icase })
} catch (e) {
ctx.console.error('grep: ' + (e.message || e))
ctx.exitCode = 2
return
}
const multiFile = inputs.length > 1
const showName =
!noFilename && (multiFile || forceFilename || (useStdin && forceFilename))
let anyMatch = false
let fatal = false
for (const { label, path } of inputs) {
let text
if (path == null) {
text = bareStdin(ctx)
} else {
try {
const buf = await vfs.readFile(path)
if (!buf) {
if (!suppressErrors)
ctx.console.error('grep: ' + path + ': No such file')
fatal = true
continue
}
text = ctx.b4a.toString(buf)
} catch (e) {
if (!suppressErrors)
ctx.console.error('grep: ' + path + ': ' + (e.message || e))
fatal = true
continue
}
}
const lines = splitLines(text)
let count = 0
let fileMatched = false
const out = []
for (let li = 0; li < lines.length; li++) {
const line = stripCr(lines[li])
const lineNum = li + 1
const matched = matchers.some((fn) => fn(line))
const hit = invert ? !matched : matched
if (hit) {
fileMatched = true
anyMatch = true
count++
if (!quiet && !countOnly && !listFiles) {
let chunk = line
if (numbers) chunk = lineNum + ':' + chunk
if (showName && label != null) chunk = label + ':' + chunk
else if (showName && label == null && useStdin)
chunk = '(standard input):' + chunk
out.push(chunk)
}
}
}
if (quiet) continue
if (listFiles && countOnly) {
if (fileMatched) {
const prefix = namePrefixForCount(showName, label, useStdin, multiFile)
ctx.console.log(prefix + count)
}
} else if (listFiles) {
if (fileMatched) {
if (label != null) ctx.console.log(label)
else if (useStdin) ctx.console.log('(standard input)')
}
} else if (countOnly) {
const prefix = namePrefixForCount(showName, label, useStdin, multiFile)
ctx.console.log(prefix + count)
} else {
for (const o of out) ctx.console.log(o)
}
}
if (fatal) ctx.exitCode = 2
else ctx.exitCode = anyMatch ? 0 : 1
}
/**
* @param {string[]} patterns
* @param {{ fixed: boolean, icase: boolean }} o
*/
function buildMatchers(patterns, o) {
if (patterns.length === 0) throw new Error('no pattern')
if (o.fixed) {
const pats = o.icase
? patterns.map((p) => p.toLowerCase())
: patterns.slice()
return pats.map(
(p) => (line) =>
o.icase ? line.toLowerCase().includes(p) : line.includes(p)
)
}
const flags = o.icase ? 'i' : ''
return patterns.map((p) => {
try {
const re = new RegExp(p, flags)
return (line) => re.test(line)
} catch (e) {
throw new Error('invalid regex: ' + (e.message || e))
}
})
}
/** @param {string} text */
function splitLines(text) {
if (text === '') return ['']
return text.split(/\n/)
}
/** @param {string} line */
function stripCr(line) {
return line.endsWith('\r') ? line.slice(0, -1) : line
}
/**
* @param {boolean} showName
* @param {string | null} label
* @param {boolean} useStdin
* @param {boolean} multiFile
*/
function namePrefixForCount(showName, label, useStdin, multiFile) {
const need = multiFile || (showName && (label != null || useStdin))
if (!need) return ''
if (label != null) return label + ':'
return '(standard input):'
}
+1 -1
View File
@@ -1,6 +1,6 @@
async function run(ctx, argv) {
ctx.console.log(
'Bare OS — default user: guest | builtins: cd, export, exit, login, logout | /bin: basename cat clear crontab date dirname echo env exit false head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
'Bare OS — default user: guest | builtins: cd, export, exit, login, logout | /bin: basename cat clear crontab date dirname echo env exit false grep head hdms help hostname id login logout ls nl pathchk printenv pwd rm savevault seq sleep sort tail test touch true tty uname wc which whoami'
)
ctx.console.log(
'Identity: login [--new] <passphrase> | logout [--save] | savevault (encrypt copy of personal drive under /.bare/vault/)'