grep
This commit is contained in:
@@ -25,10 +25,7 @@ import {
|
||||
parseCronLine,
|
||||
jobMatchesDate
|
||||
} from './lib/bare-cron.js'
|
||||
import {
|
||||
registerBareInitdDisposer,
|
||||
stopBareInitd
|
||||
} from './lib/bare-initd.js'
|
||||
import { registerBareInitdDisposer, stopBareInitd } from './lib/bare-initd.js'
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
function testCorestoreDir(name) {
|
||||
@@ -148,10 +145,7 @@ test('runBinCommand user script error is caught and logged', async (t) => {
|
||||
const personal = new Hyperdrive(store.namespace('ptj'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await personal.put(
|
||||
'/bad.js',
|
||||
b4a.from(`async function run() { test() }`)
|
||||
)
|
||||
await personal.put('/bad.js', b4a.from(`async function run() { test() }`))
|
||||
const errs = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.console = {
|
||||
@@ -297,10 +291,14 @@ test('vfs lists virtual /home at root; /home shows only active session dir', asy
|
||||
t.ok(rootG.includes('home'))
|
||||
t.ok(rootG.includes('bin') || rootG.includes('boot') || rootG.length >= 1)
|
||||
t.alike(await vfsG.readdir('/home'), ['guest'])
|
||||
const envUser = { HOME: '/home/eeb18de988e9', PWD: '/home/eeb18de988e9', PATH: '/bin' }
|
||||
const envUser = {
|
||||
HOME: '/home/eeb18de988e9',
|
||||
PWD: '/home/eeb18de988e9',
|
||||
PATH: '/bin'
|
||||
}
|
||||
const vfsU = createVfs(sys, personal, envUser)
|
||||
t.alike(await vfsU.readdir('/home'), ['eeb18de988e9'])
|
||||
t.is((await vfsU.stat('/home/guest')), null)
|
||||
t.is(await vfsU.stat('/home/guest'), null)
|
||||
await vfsU.writeFile('/home/eeb18de988e9/x', b4a.from('1'))
|
||||
const buf = await personal.get('/x')
|
||||
t.ok(buf)
|
||||
@@ -319,8 +317,7 @@ test('vfs /mnt lists HDMS mounts and allows writable put', async (t) => {
|
||||
const extra = new Hyperdrive(store.namespace('exm', { writable: true }))
|
||||
await extra.ready()
|
||||
const mntRef = {
|
||||
getMounts: () =>
|
||||
new Map([['vault', { drive: extra, writable: true }]])
|
||||
getMounts: () => new Map([['vault', { drive: extra, writable: true }]])
|
||||
}
|
||||
const vfs = createVfs(sys, personal, env, mntRef)
|
||||
const root = await vfs.readdir('/')
|
||||
@@ -460,6 +457,68 @@ test('tier-1 cat from system drive', async (t) => {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
test('tier-1 grep from system drive', async (t) => {
|
||||
const dir = testCorestoreDir('grep')
|
||||
const store = new Corestore(dir)
|
||||
const drive = new Hyperdrive(store)
|
||||
const personal = new Hyperdrive(store.namespace('pg'))
|
||||
await drive.ready()
|
||||
await personal.ready()
|
||||
await drive.put('/bin/grep', b4a.from(await readBuiltBin('grep')))
|
||||
const lines = []
|
||||
const ctx = testCtx(drive, personal)
|
||||
ctx.exitCode = 0
|
||||
ctx.console = {
|
||||
log(s) {
|
||||
lines.push(String(s))
|
||||
},
|
||||
error() {}
|
||||
}
|
||||
await ctx.vfs.writeFile(
|
||||
'w.txt',
|
||||
b4a.from('aaa\nneedle line\nbbb\nneedle line two')
|
||||
)
|
||||
await runBinCommand(ctx, ['grep', '-F', 'needle', 'w.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.length, 2)
|
||||
t.ok(lines[0].includes('needle line'))
|
||||
t.ok(lines[1].includes('needle line two'))
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['grep', '-F', 'nomatch', 'w.txt'])
|
||||
t.is(ctx.exitCode, 1)
|
||||
t.is(lines.length, 0)
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['grep', '-v', '-F', 'needle', 'w.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines.join('\n'), 'aaa\nbbb')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['grep', '-c', '-F', 'needle', 'w.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines[0], '2')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
await runBinCommand(ctx, ['grep', '-n', '-F', 'bbb', 'w.txt'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines[0], '3:bbb')
|
||||
|
||||
lines.length = 0
|
||||
ctx.exitCode = 0
|
||||
ctx.shellStdin = 'alpha\nbeta\n'
|
||||
await runBinCommand(ctx, ['grep', '-F', 'beta'])
|
||||
t.is(ctx.exitCode, 0)
|
||||
t.is(lines[0], 'beta')
|
||||
|
||||
await store.close()
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
async function readBuiltBin(name) {
|
||||
const fs = await import('node:fs/promises')
|
||||
const p = path.join(__dirname, '../../kernel/bin', name)
|
||||
|
||||
@@ -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).
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ const commands = [
|
||||
'env',
|
||||
'exit',
|
||||
'false',
|
||||
'grep',
|
||||
'head',
|
||||
'hdms',
|
||||
'help',
|
||||
|
||||
@@ -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,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/)'
|
||||
|
||||
@@ -0,0 +1,291 @@
|
||||
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
||||
function bareStdin(ctx) {
|
||||
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 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):'
|
||||
}
|
||||
@@ -5,7 +5,7 @@ function bareStdin(ctx) {
|
||||
|
||||
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/)'
|
||||
|
||||
Reference in New Issue
Block a user