+291
@@ -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):'
|
||||
}
|
||||
+1
-1
@@ -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