2405 lines
66 KiB
Plaintext
2405 lines
66 KiB
Plaintext
/* BARE_OS_BIN_API 1.0.0 — bump when staged /bin script semantics change (see developer guide). */
|
|
/** Shared helpers for drive-resident /bin scripts (prepended before each command). */
|
|
function bareStdin(ctx) {
|
|
return typeof ctx.shellStdin === 'string' ? ctx.shellStdin : ''
|
|
}
|
|
|
|
/** @param {number} mode @param {'file' | 'directory' | 'symlink'} type */
|
|
function bareFormatModeString(mode, type) {
|
|
const typeChar = type === 'directory' ? 'd' : type === 'symlink' ? 'l' : '-'
|
|
const perm = mode & 0o777
|
|
const r = (bit) => (perm & bit ? 'r' : '-')
|
|
const w = (bit) => (perm & bit ? 'w' : '-')
|
|
const x = (bit) => (perm & bit ? 'x' : '-')
|
|
return (
|
|
typeChar +
|
|
r(0o400) +
|
|
w(0o200) +
|
|
x(0o100) +
|
|
r(0o040) +
|
|
w(0o020) +
|
|
x(0o010) +
|
|
r(0o004) +
|
|
w(0o002) +
|
|
x(0o001)
|
|
)
|
|
}
|
|
|
|
/** @param {number} mtimeMs @param {number} [nowMs] */
|
|
function bareFormatLsMtime(mtimeMs, nowMs) {
|
|
const now = nowMs != null ? nowMs : Date.now()
|
|
const d = new Date(mtimeMs)
|
|
const months = [
|
|
'Jan',
|
|
'Feb',
|
|
'Mar',
|
|
'Apr',
|
|
'May',
|
|
'Jun',
|
|
'Jul',
|
|
'Aug',
|
|
'Sep',
|
|
'Oct',
|
|
'Nov',
|
|
'Dec'
|
|
]
|
|
const mon = months[d.getMonth()]
|
|
const day = String(d.getDate()).padStart(2, ' ')
|
|
const sixMo = 180 * 24 * 3600 * 1000
|
|
if (Math.abs(now - mtimeMs) > sixMo) {
|
|
const yr = String(d.getFullYear()).padStart(4, ' ')
|
|
return mon + ' ' + day + ' ' + yr
|
|
}
|
|
const hh = String(d.getHours()).padStart(2, '0')
|
|
const mm = String(d.getMinutes()).padStart(2, '0')
|
|
return mon + ' ' + day + ' ' + hh + ':' + mm
|
|
}
|
|
|
|
/** @param {number} size */
|
|
function barePosixBlocks(size) {
|
|
return Math.ceil(Number(size) / 512) || 0
|
|
}
|
|
|
|
/**
|
|
* Raw stdout for NUL/binary when **`process.stdout.write`** is missing.
|
|
* If **`ctx.bareOsBinWrite(Uint8Array|string)`** is set (tests / host), use it.
|
|
* @param {Record<string, unknown>} ctx
|
|
* @param {string | Uint8Array} chunk
|
|
* @returns {boolean}
|
|
*/
|
|
function bareOsEmitRaw(ctx, chunk) {
|
|
if (typeof ctx.bareOsBinWrite === 'function') {
|
|
const b4 = ctx.b4a
|
|
const u8 =
|
|
typeof chunk === 'string'
|
|
? b4 && typeof b4.from === 'function'
|
|
? b4.from(chunk)
|
|
: new TextEncoder().encode(chunk)
|
|
: chunk
|
|
ctx.bareOsBinWrite(u8 instanceof Uint8Array ? u8 : new Uint8Array(u8))
|
|
return true
|
|
}
|
|
const w = globalThis.process?.stdout?.write
|
|
if (typeof w === 'function') {
|
|
w.call(globalThis.process.stdout, chunk)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Bare OS: vendored from @sscots/[email protected] (https://github.com/sscots/jqjs). ISC License. Top-level compile() and prettyPrint(); not the C jq binary (https://github.com/jqlang/jq).
|
|
// Triage: treat bug reports as jqjs semantic gaps; prefer golden tests before expanding builtins. Large JSON is bounded by host memory and stdin string caps (see handbook §3).
|
|
// jqjs - jq JSON query language in JavaScript
|
|
// Copyright (C) 2018-2023 Michael Homer
|
|
/*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
* of this software and associated documentation files (the "Software"), to deal
|
|
* in the Software without restriction, including without limitation the rights
|
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the Software is
|
|
* furnished to do so, subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
* SOFTWARE.
|
|
*/
|
|
|
|
function compile(prog) {
|
|
let filter = parse(tokenise(prog).tokens)
|
|
return (input) =>
|
|
filter.node.apply(input, {
|
|
userFuncs: {},
|
|
userFuncArgs: {},
|
|
variables: {}
|
|
})
|
|
}
|
|
|
|
function compileNode(prog) {
|
|
return parse(tokenise(prog).tokens).node
|
|
}
|
|
|
|
function isAlpha(c) {
|
|
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c === '_'
|
|
}
|
|
|
|
function isDigit(c) {
|
|
return c >= '0' && c <= '9'
|
|
}
|
|
|
|
function isQuote(c) {
|
|
return c === '"' || c === "'"
|
|
}
|
|
|
|
function prettyPrint(val, indent = '', step = ' ', LF = '\n') {
|
|
let SP = step ? ' ' : ''
|
|
if (typeof val == 'undefined') return val
|
|
if (val === null) {
|
|
return 'null'
|
|
} else if (val instanceof Array) {
|
|
let ret = '['
|
|
let first = true
|
|
for (let v of Object.values(val)) {
|
|
ret +=
|
|
(first ? '' : ',') +
|
|
LF +
|
|
indent +
|
|
step +
|
|
prettyPrint(v, indent + step, step, LF)
|
|
first = false
|
|
}
|
|
ret += LF + indent + ']'
|
|
return ret
|
|
} else if (typeof val == 'object') {
|
|
let ret = '{'
|
|
let first = true
|
|
for (let k of Object.keys(val)) {
|
|
ret +=
|
|
(first ? '' : ',') +
|
|
LF +
|
|
indent +
|
|
step +
|
|
'"' +
|
|
k +
|
|
'":' +
|
|
SP +
|
|
prettyPrint(val[k], indent + step, step, LF)
|
|
first = false
|
|
}
|
|
ret += LF + indent + '}'
|
|
return ret
|
|
} else if (typeof val == 'string') {
|
|
return '"' + escapeString(val) + '"'
|
|
} else if (typeof val == 'number') {
|
|
return '' + val
|
|
} else if (typeof val == 'boolean') {
|
|
return val ? 'true' : 'false'
|
|
}
|
|
}
|
|
|
|
function escapeString(s) {
|
|
s = s.replace(/\\/g, '\\\\')
|
|
s = s.replace(/"/g, '\\"')
|
|
s = s.replace(/\n/g, '\\n')
|
|
s = s.replace(
|
|
/[\x00-\x1f]/g,
|
|
(x) => '\\u00' + x.charCodeAt(0).toString(16).padStart(2, '0')
|
|
)
|
|
return s
|
|
}
|
|
|
|
function* zip(a, b) {
|
|
let aa = a[Symbol.iterator]()
|
|
let bb = b[Symbol.iterator]()
|
|
let v1 = aa.next()
|
|
let v2 = bb.next()
|
|
while (!v1.done && !v2.done) {
|
|
yield [v1.value, v2.value]
|
|
v1 = aa.next()
|
|
v2 = bb.next()
|
|
}
|
|
}
|
|
|
|
// Implements the jq ordering algorithm, which is terrible.
|
|
function compareValues(a, b) {
|
|
let at = nameType(a)
|
|
let bt = nameType(b)
|
|
if (at != bt) {
|
|
return (
|
|
compareValues.typeOrder.indexOf(at) - compareValues.typeOrder.indexOf(bt)
|
|
)
|
|
}
|
|
if (at == 'boolean') {
|
|
if (a && !b) return 1
|
|
if (!a && b) return -1
|
|
} else if (at == 'number') {
|
|
return a - b
|
|
} else if (at == 'string') {
|
|
if (a < b) return -1
|
|
if (b < a) return 1
|
|
} else if (at == 'array') {
|
|
for (let i = 0; i < a.length; i++) {
|
|
let v1 = a[i]
|
|
let v2 = b[i]
|
|
if (typeof v1 == 'undefined' && typeof v2 != 'undefined') return -1
|
|
else if (typeof v1 != 'undefined' && typeof v2 == 'undefined') return 1
|
|
else if (typeof v1 == 'undefined') return 0
|
|
let c = compareValues(v1, v2)
|
|
if (c != 0) return c
|
|
}
|
|
return 0
|
|
} else if (at == 'object') {
|
|
let ka = Object.keys(a).sort()
|
|
let kb = Object.keys(b).sort()
|
|
let c = compareValues(ka, kb)
|
|
if (c) return c
|
|
for (let k of ka) {
|
|
c = compareValues(a[k], b[k])
|
|
if (c) return c
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
compareValues.typeOrder = [
|
|
'null',
|
|
'boolean',
|
|
'number',
|
|
'string',
|
|
'array',
|
|
'object'
|
|
]
|
|
|
|
// Create a function from a program string.
|
|
//
|
|
// params is an array of parameter names
|
|
// body is a jq program as a string, which may use the parameters
|
|
//
|
|
// For example:
|
|
// makeFunc(['f'], '[.[] | f]')
|
|
// defines the map function.
|
|
function makeFunc(params, body, pathFunc = false) {
|
|
let c = compileNode(body)
|
|
let f = (x, conf) => c.apply(x, conf)
|
|
if (pathFunc) f = (x, conf) => c.paths(x, conf)
|
|
return function* (input, conf, args) {
|
|
let origArgs = conf.userFuncArgs
|
|
conf.userFuncArgs = Object.create(origArgs)
|
|
for (let i = 0; i < params.length; i++) {
|
|
let pn = params[i]
|
|
let pv = args[i]
|
|
conf.userFuncArgs[pn + '/0'] = pv
|
|
}
|
|
yield* f(input, conf)
|
|
conf.userFuncArgs = origArgs
|
|
}
|
|
}
|
|
|
|
// Define and save a function that is shorthand for a longer expression
|
|
//
|
|
// name is the name of the function
|
|
// params is an array of parameters, or a string of one-character names
|
|
// body is a jq program as a string
|
|
function defineShorthandFunction(name, params, body) {
|
|
let fname = name + '/' + params.length
|
|
functions[fname] = makeFunc(params, body)
|
|
functions[fname + '-paths'] = makeFunc(params, body, true)
|
|
}
|
|
|
|
// Recursive-descent parser for JQ query language
|
|
|
|
// Split input program into tokens. Tokens are:
|
|
// quote, number, identifier-index, dot-square, dot, left-square,
|
|
// right-square, left-paren, right-paren, pipe, comma,
|
|
// identifier, colon, left-brace, right-brace, semicolon, at,
|
|
// variable, as, reduce, foreach, def, import, include, question
|
|
// if, then, else, end, elif
|
|
function tokenise(str, startAt = 0, parenDepth) {
|
|
let ret = []
|
|
function error(msg) {
|
|
throw msg
|
|
}
|
|
let i
|
|
toplevel: for (i = startAt; i < str.length; i++) {
|
|
let location = i
|
|
let c = str[i]
|
|
if (c == ' ') continue
|
|
if (c == '"' || c == "'") {
|
|
let st = c
|
|
let tok = ''
|
|
let escaped = false
|
|
let uniEsc
|
|
let cu = 0
|
|
for (i++; i < str.length; i++) {
|
|
if (uniEsc) {
|
|
uniEsc--
|
|
cu *= 16
|
|
cu += Number.parseInt(str[i], 16)
|
|
if (uniEsc == 0) {
|
|
tok += String.fromCharCode(cu)
|
|
cu = 0
|
|
}
|
|
} else if (escaped) {
|
|
let q = str[i]
|
|
if (q == '"' || q == "'") tok += q
|
|
else if (q == 'n') tok += '\n'
|
|
else if (q == 't') tok += '\t'
|
|
else if (q == 'r') tok += '\r'
|
|
else if (q == 'b') tok += '\b'
|
|
else if (q == 'f') tok += '\f'
|
|
else if (q == '/') tok += '/'
|
|
else if (q == '\\') tok += '\\'
|
|
else if (q == 'u') uniEsc = 4
|
|
else if (q == '(') {
|
|
// Interpolation
|
|
let r = tokenise(str, i + 1, 0)
|
|
ret.push({ type: 'quote-interp', value: tok, location })
|
|
tok = ''
|
|
ret = ret.concat(r.tokens)
|
|
i = r.i
|
|
} else throw 'invalid escape ' + q
|
|
escaped = false
|
|
} else if (str[i] == '\\') {
|
|
escaped = true
|
|
} else if (str[i] == st) {
|
|
ret.push({ type: 'quote', value: tok, location })
|
|
continue toplevel
|
|
} else {
|
|
escaped = false
|
|
tok += str[i]
|
|
}
|
|
}
|
|
error('unterminated string literal')
|
|
} else if (isDigit(c)) {
|
|
let tok = ''
|
|
while (isDigit(str[i]) || str[i] == '.') tok += str[i++]
|
|
ret.push({ type: 'number', value: Number.parseFloat(tok), location })
|
|
i--
|
|
} else if (c == '.') {
|
|
let d = str[i + 1]
|
|
if (isAlpha(d) || isQuote(d)) {
|
|
i++
|
|
let tok = ''
|
|
while (isAlpha(str[i]) || isDigit(str[i]) || isQuote(str[i]))
|
|
tok += str[i++]
|
|
tok = tok.split('"').join('').split("'").join('') // Remove quotes
|
|
ret.push({ type: 'identifier-index', value: tok, location })
|
|
i--
|
|
} else if (d == '[') {
|
|
i++
|
|
ret.push({ type: 'dot-square', location })
|
|
} else if (d == '.') {
|
|
i++
|
|
ret.push({ type: 'dot-dot', location })
|
|
} else {
|
|
ret.push({ type: 'dot', location })
|
|
}
|
|
} else if (c == '$') {
|
|
let d = str[i + 1]
|
|
i++
|
|
let tok = ''
|
|
while (isAlpha(str[i]) || isDigit(str[i])) {
|
|
tok += str[i]
|
|
i++
|
|
}
|
|
ret.push({ type: 'variable', name: tok, location })
|
|
i--
|
|
} else if (c == '[') {
|
|
ret.push({ type: 'left-square', location })
|
|
} else if (c == ']') {
|
|
ret.push({ type: 'right-square', location })
|
|
} else if (c == '(') {
|
|
ret.push({ type: 'left-paren', location })
|
|
parenDepth++
|
|
} else if (c == ')') {
|
|
ret.push({ type: 'right-paren', location })
|
|
parenDepth--
|
|
if (parenDepth < 0) return { tokens: ret, i }
|
|
} else if (c == '{') {
|
|
ret.push({ type: 'left-brace', location })
|
|
} else if (c == '}') {
|
|
ret.push({ type: 'right-brace', location })
|
|
} else if (c == ',') {
|
|
ret.push({ type: 'comma', location })
|
|
} else if (c == ';') {
|
|
ret.push({ type: 'semicolon', location })
|
|
} else if (c == '@') {
|
|
ret.push({ type: 'at', location })
|
|
} else if (c == '?') {
|
|
ret.push({ type: 'question', location })
|
|
} else if (c == '|') {
|
|
let d = str[i + 1]
|
|
if (d == '=') {
|
|
ret.push({ type: 'pipe-equals', location })
|
|
i++
|
|
} else ret.push({ type: 'pipe', location })
|
|
// Infix operators
|
|
} else if (
|
|
c == '+' ||
|
|
c == '*' ||
|
|
c == '-' ||
|
|
c == '/' ||
|
|
c == '%' ||
|
|
c == '<' ||
|
|
c == '>'
|
|
) {
|
|
if (c == '/' && str[i + 1] == '/') {
|
|
c = '//'
|
|
i++
|
|
}
|
|
if (str[i + 1] == '=') {
|
|
if (c == '<' || c == '>')
|
|
ret.push({ type: 'op', op: c + '=', location })
|
|
else ret.push({ type: 'op-equals', op: c, location })
|
|
i++
|
|
} else ret.push({ type: 'op', op: c })
|
|
} else if (c == '=') {
|
|
if (str[i + 1] != '=') throw 'plain assignment = is not supported'
|
|
i++
|
|
ret.push({ type: 'op', op: '==', location })
|
|
} else if (c == '!') {
|
|
if (str[i + 1] != '=') throw 'unexpected ! at ' + location
|
|
i++
|
|
ret.push({ type: 'op', op: '!=', location })
|
|
} else if (isAlpha(c)) {
|
|
let tok = ''
|
|
while (isAlpha(str[i]) || isDigit(str[i]) || str[i] == '_')
|
|
tok += str[i++]
|
|
if (
|
|
tok == 'as' ||
|
|
tok == 'reduce' ||
|
|
tok == 'foreach' ||
|
|
tok == 'import' ||
|
|
tok == 'include' ||
|
|
tok == 'def' ||
|
|
tok == 'if' ||
|
|
tok == 'then' ||
|
|
tok == 'else' ||
|
|
tok == 'end' ||
|
|
tok == 'elif'
|
|
) {
|
|
ret.push({ type: tok, location })
|
|
} else {
|
|
ret.push({ type: 'identifier', value: tok, location })
|
|
}
|
|
i--
|
|
} else if (c == ':') {
|
|
ret.push({ type: 'colon', location })
|
|
}
|
|
}
|
|
ret.push({ type: '<end-of-program>', location: i })
|
|
return { tokens: ret, i }
|
|
}
|
|
|
|
function describeLocation(token) {
|
|
if (token) {
|
|
return token.location
|
|
}
|
|
return '<end>'
|
|
}
|
|
|
|
// Parse a token stream by recursive descent.
|
|
//
|
|
// Returns {node: {*apply(input, conf)}, i}, where i is the position in the
|
|
// token stream and node is one of the filtering nodes defined below.
|
|
// Returns at end of stream or when a token of type until is found.
|
|
function parse(tokens, startAt = 0, until = 'none') {
|
|
let i = startAt
|
|
let t = tokens[i]
|
|
let ret = []
|
|
let commaAccum = []
|
|
while (t && until.indexOf(t.type) == -1) {
|
|
// Simple cases
|
|
if (t.type == 'identifier-index') {
|
|
ret.push(new IdentifierIndex(t.value))
|
|
} else if (t.type == 'number') {
|
|
ret.push(new NumberNode(t.value))
|
|
} else if (t.type == 'quote') {
|
|
ret.push(new StringNode(t.value))
|
|
} else if (t.type == 'dot') {
|
|
ret.push(new IdentityNode())
|
|
} else if (t.type == 'dot-dot') {
|
|
ret.push(new RecursiveDescent())
|
|
} else if (t.type == 'identifier') {
|
|
if (t.value == 'true' || t.value == 'false')
|
|
ret.push(new BooleanNode(t.value == 'true'))
|
|
else if (t.value == 'null') ret.push(new BooleanNode(null))
|
|
else {
|
|
// Named function
|
|
let fname = t.value
|
|
let args = []
|
|
if (tokens[i + 1] && tokens[i + 1].type == 'left-paren') {
|
|
i++
|
|
while (tokens[i].type != 'right-paren') {
|
|
let arg = parse(tokens, i + 1, ['semicolon', 'right-paren'])
|
|
args.push(arg.node)
|
|
i = arg.i
|
|
}
|
|
}
|
|
ret.push(new FunctionCall(fname + '/' + args.length, args))
|
|
}
|
|
// Recursive square bracket cases
|
|
} else if (t.type == 'dot-square') {
|
|
let r = parseDotSquare(tokens, i)
|
|
ret.push(r.node)
|
|
i = r.i
|
|
} else if (t.type == 'left-square') {
|
|
// Find the body of the brackets first
|
|
let r = parse(tokens, i + 1, ['right-square', 'colon'])
|
|
if (ret.length) {
|
|
let lhs = makeFilterNode(ret)
|
|
ret = []
|
|
if (tokens[r.i].type == 'colon') {
|
|
// Slice
|
|
if (r.node.length === 0) r.node = new NumberNode(0)
|
|
let e = parse(tokens, r.i + 1, ['right-square'])
|
|
if (e.node.length === 0) e.node = new NumberNode(-1)
|
|
ret.push(new SliceNode(lhs, r.node, e.node))
|
|
r = e
|
|
} else if (r.node.length === 0) ret.push(new SpecificValueIterator(lhs))
|
|
else ret.push(new IndexNode(lhs, r.node))
|
|
} else {
|
|
ret.push(new ArrayNode(r.node))
|
|
}
|
|
i = r.i
|
|
// Recursive parenthesis case
|
|
} else if (t.type == 'left-paren') {
|
|
// Find the body of the brackets first
|
|
let r = parse(tokens, i + 1, 'right-paren')
|
|
ret.push(r.node)
|
|
i = r.i
|
|
// Object literal
|
|
} else if (t.type == 'left-brace') {
|
|
let r = parseObject(tokens, i + 1)
|
|
ret.push(r.node)
|
|
i = r.i
|
|
// Format @x
|
|
} else if (t.type == 'at') {
|
|
let n = tokens[++i]
|
|
if (!n || n.type != 'identifier')
|
|
throw 'expected identifier after @ at ' + describeLocation(n)
|
|
let fmt = n.value
|
|
if (!formats[fmt]) throw 'not a valid format: ' + fmt
|
|
let q
|
|
if (tokens[i + 1] && tokens[i + 1].type == 'quote-interp') {
|
|
;({ q, i } = parseStringInterpolation(tokens, i + 1))
|
|
i = i
|
|
}
|
|
ret.push(new FormatNode(fmt, q))
|
|
// Comma consumes everything previous and splits in-place
|
|
// (parsing carries on in this method)
|
|
} else if (t.type == 'comma') {
|
|
commaAccum.push(makeFilterNode(ret))
|
|
ret = []
|
|
// Pipe consumes everything previous *including* commas
|
|
// and splits by recursing for the right-hand side.
|
|
// "as" indicates a variable binding.
|
|
} else if (t.type == 'pipe' || t.type == 'as') {
|
|
if (commaAccum.length) {
|
|
// .x,.y | .[1] is the same as (.x,.y) | .[1]
|
|
commaAccum.push(makeFilterNode(ret))
|
|
ret = [new CommaNode(commaAccum)]
|
|
commaAccum = []
|
|
}
|
|
let lhs = makeFilterNode(ret)
|
|
if (t.type == 'as') {
|
|
let nameTok = tokens[i + 1]
|
|
lhs = new VariableBinding(lhs, nameTok.name)
|
|
i += 2
|
|
}
|
|
let r = parse(tokens, i + 1, until)
|
|
let rhs = r.node
|
|
i = r.i
|
|
if (tokens[i] && until.indexOf(tokens[i].type) != -1) i--
|
|
ret = [new PipeNode(lhs, rhs)]
|
|
// Question mark suppresses errors on the preceding filter
|
|
} else if (t.type == 'question') {
|
|
let p = ret.pop()
|
|
if (!p)
|
|
throw 'unexpected ? without preceding filter at ' + describeLocation(t)
|
|
ret.push(new ErrorSuppression(p))
|
|
// Infix operators
|
|
} else if (t.type == 'op') {
|
|
if (ret.length == 0 && t.op == '-' && tokens[i + 1].type == 'number') {
|
|
tokens[i + 1].value = -tokens[i + 1].value
|
|
t = tokens[++i]
|
|
continue
|
|
}
|
|
let lhs = makeFilterNode(ret)
|
|
let op = t.op
|
|
let stream = [lhs, t]
|
|
let r = parse(
|
|
tokens,
|
|
i + 1,
|
|
[
|
|
'op',
|
|
'comma',
|
|
'pipe',
|
|
'right-paren',
|
|
'right-brace',
|
|
'right-square',
|
|
'<end-of-program>'
|
|
].concat(until)
|
|
)
|
|
i = r.i
|
|
stream.push(r.node)
|
|
while (i < tokens.length && tokens[i].type == 'op') {
|
|
stream.push(tokens[i])
|
|
let r = parse(
|
|
tokens,
|
|
i + 1,
|
|
[
|
|
'op',
|
|
'comma',
|
|
'pipe',
|
|
'right-paren',
|
|
'right-brace',
|
|
'right-square',
|
|
'<end-of-program>'
|
|
].concat(until)
|
|
)
|
|
i = r.i
|
|
stream.push(r.node)
|
|
}
|
|
ret = [shuntingYard(stream)]
|
|
if (tokens[i]) i--
|
|
// Update-assignment
|
|
} else if (t.type == 'pipe-equals') {
|
|
let lhs = makeFilterNode(ret)
|
|
let r = parse(
|
|
tokens,
|
|
i + 1,
|
|
[
|
|
'comma',
|
|
'pipe',
|
|
'right-paren',
|
|
'right-brace',
|
|
'right-square',
|
|
'<end-of-program>'
|
|
].concat(until)
|
|
)
|
|
i = r.i
|
|
let rhs = r.node
|
|
ret = [new UpdateAssignment(lhs, rhs)]
|
|
// Arithmetic update-assignment
|
|
} else if (t.type == 'op-equals') {
|
|
let lhs = makeFilterNode(ret)
|
|
let r = parse(
|
|
tokens,
|
|
i + 1,
|
|
[
|
|
'comma',
|
|
'pipe',
|
|
'right-paren',
|
|
'right-brace',
|
|
'right-square',
|
|
'<end-of-program>'
|
|
].concat(until)
|
|
)
|
|
i = r.i
|
|
let rhs = r.node
|
|
rhs = shuntingYard([new IdentityNode(), { type: 'op', op: t.op }, rhs])
|
|
ret = [new UpdateAssignment(lhs, rhs)]
|
|
// reduce .[] as $item (0, . + $item)
|
|
} else if (t.type == 'reduce') {
|
|
let r = parse(tokens, i + 1, ['as'])
|
|
i = r.i
|
|
let generator = r.node
|
|
i++ // 'as'
|
|
let name = tokens[i].name
|
|
i++
|
|
if (tokens[i].type != 'left-paren')
|
|
throw 'expected left-paren in reduce at ' + describeLocation(tokens[i])
|
|
r = parse(tokens, i + 1, ['semicolon'])
|
|
i = r.i
|
|
let init = r.node
|
|
r = parse(tokens, i + 1, ['right-paren'])
|
|
i = r.i
|
|
let expr = r.node
|
|
ret.push(new ReduceNode(generator, name, init, expr))
|
|
// Interpolated string literal
|
|
} else if (t.type == 'quote-interp') {
|
|
let q
|
|
;({ q, i } = parseStringInterpolation(tokens, i))
|
|
ret.push(q)
|
|
// Variable reference
|
|
} else if (t.type == 'variable') {
|
|
ret.push(new VariableReference(t.name))
|
|
// Conditional if-then-(elif-then)*-else?-end
|
|
} else if (t.type == 'if') {
|
|
let conds = []
|
|
let trueExprs = []
|
|
let falseExpr = null
|
|
while (
|
|
tokens[i] &&
|
|
(tokens[i].type == 'if' || tokens[i].type == 'elif')
|
|
) {
|
|
let cond = parse(tokens, i + 1, ['then'])
|
|
if (!tokens[cond.i] || tokens[cond.i].type != 'then')
|
|
throw (
|
|
'expected then at ' +
|
|
describeLocation(tokens[cond.i]) +
|
|
', from ' +
|
|
tokens[i].type +
|
|
' at ' +
|
|
tokens[i].location
|
|
)
|
|
let trueExpr = parse(tokens, cond.i + 1, ['else', 'elif', 'end'])
|
|
if (trueExpr.i == cond.i + 1)
|
|
throw (
|
|
'expected expression after then at ' +
|
|
describeLocation(tokens[cond.i + 1]) +
|
|
', not ' +
|
|
tokens[cond.i + 1].type
|
|
)
|
|
i = trueExpr.i
|
|
conds.push(cond.node)
|
|
trueExprs.push(trueExpr.node)
|
|
}
|
|
if (tokens[i] && tokens[i].type == 'else') {
|
|
let elseCase = parse(tokens, i + 1, ['end'])
|
|
i = elseCase.i
|
|
falseExpr = elseCase.node
|
|
}
|
|
if (!tokens[i] || tokens[i].type != 'end')
|
|
throw (
|
|
'expected end at ' +
|
|
describeLocation(tokens[i]) +
|
|
' from if at ' +
|
|
t.location
|
|
)
|
|
ret.push(new IfNode(conds, trueExprs, falseExpr))
|
|
} else if (t.type == '<end-of-program>' && until == 'none') {
|
|
break
|
|
} else {
|
|
throw (
|
|
'could not handle token ' +
|
|
t.type +
|
|
' at ' +
|
|
describeLocation(t) +
|
|
(until != 'none' ? ', expected ' + until.join('/') : '')
|
|
)
|
|
}
|
|
t = tokens[++i]
|
|
}
|
|
// If a comma appeared this array is non-empty and contains all
|
|
// previous branches.
|
|
if (commaAccum.length) {
|
|
commaAccum.push(makeFilterNode(ret))
|
|
return { node: new CommaNode(commaAccum), i }
|
|
}
|
|
return { node: makeFilterNode(ret), i }
|
|
}
|
|
|
|
function makeFilterNode(ret) {
|
|
if (ret.length == 1) return ret[0]
|
|
return new FilterNode(ret)
|
|
}
|
|
|
|
// Consumes pairs (quote-interp, expression up to rparen)* followed by
|
|
// a bare string and returns a StringLiteral node with the interleaving
|
|
// lists.
|
|
function parseStringInterpolation(tokens, i) {
|
|
let t = tokens[i]
|
|
let strings = []
|
|
let interps = []
|
|
strings.push(t.value)
|
|
// Always followed by a paren expression afterwards
|
|
let inner = parse(tokens, i + 1, ['right-paren'])
|
|
i = inner.i + 1
|
|
interps.push(inner.node)
|
|
while (tokens[i].type == 'quote-interp') {
|
|
strings.push(tokens[i].value)
|
|
inner = parse(tokens, i + 1, ['right-paren'])
|
|
i = inner.i + 1
|
|
interps.push(inner.node)
|
|
}
|
|
// Must be the ending quote now
|
|
strings.push(tokens[i].value)
|
|
return { q: new StringLiteral(strings, interps), i }
|
|
}
|
|
|
|
function parseDotSquare(tokens, startAt = 0) {
|
|
let i = startAt
|
|
let ds = tokens[i]
|
|
i++
|
|
if (tokens[i].type == 'right-square')
|
|
return { node: new GenericValueIterator(), i }
|
|
let r = parse(tokens, i, ['right-square', 'colon'])
|
|
if (tokens[r.i].type == 'colon') {
|
|
// Slice
|
|
let fr = r
|
|
if (fr.length === 0) fr.node = new NumberNode(0)
|
|
r = parse(tokens, r.i + 1, ['right-square'])
|
|
if (r.length === 0) r.node = new NumberNode(-1)
|
|
return { node: new GenericSlice(fr.node, r.node), i: r.i }
|
|
}
|
|
return { node: new GenericIndex(r.node), i: r.i }
|
|
}
|
|
|
|
// Parse an object literal, expecting to start immediately inside the
|
|
// left brace and to consume up to and including the right brace.
|
|
function parseObject(tokens, startAt = 0) {
|
|
let i = startAt
|
|
let fields = []
|
|
while (tokens[i].type != 'right-brace') {
|
|
if (tokens[i].type == 'identifier') {
|
|
// bare name x
|
|
let ident = tokens[i++]
|
|
if (tokens[i].type == 'colon') {
|
|
// with value x: val
|
|
let r = parse(tokens, i + 1, ['comma', 'right-brace'])
|
|
i = r.i
|
|
fields.push({
|
|
key: new StringNode(ident.value),
|
|
value: r.node
|
|
})
|
|
i--
|
|
} else if (tokens[i].type == 'comma') {
|
|
// no value: equivalent to x : .x
|
|
fields.push({
|
|
key: new StringNode(ident.value),
|
|
value: new IdentifierIndex(ident.value)
|
|
})
|
|
} else if (tokens[i].type == 'right-brace') {
|
|
// ditto, last field: equivalent to x : .x
|
|
fields.push({
|
|
key: new StringNode(ident.value),
|
|
value: new IdentifierIndex(ident.value)
|
|
})
|
|
i--
|
|
}
|
|
} else if (tokens[i].type == 'quote') {
|
|
// quoted-string key: "x" : val
|
|
let ident = tokens[i++]
|
|
if (tokens[i].type == 'colon') {
|
|
let r = parse(tokens, i + 1, ['comma', 'right-brace'])
|
|
i = r.i
|
|
fields.push({
|
|
key: new StringNode(ident.value),
|
|
value: r.node
|
|
})
|
|
i--
|
|
} else {
|
|
throw (
|
|
'unexpected ' +
|
|
tokens[i].type +
|
|
', expected colon at ' +
|
|
describeLocation(tokens[i])
|
|
)
|
|
}
|
|
} else if (tokens[i].type == 'left-paren') {
|
|
// computed key: (.x | .y) : val
|
|
let kr = parse(tokens, i + 1, 'right-paren')
|
|
i = kr.i + 1
|
|
if (tokens[i].type == 'colon') {
|
|
let r = parse(tokens, i + 1, ['comma', 'right-brace'])
|
|
i = r.i
|
|
fields.push({
|
|
key: kr.node,
|
|
value: r.node
|
|
})
|
|
i--
|
|
} else {
|
|
throw (
|
|
'unexpected ' +
|
|
tokens[i].type +
|
|
', expected colon at ' +
|
|
describeLocation(tokens[i])
|
|
)
|
|
}
|
|
} else {
|
|
throw (
|
|
'unexpected ' +
|
|
tokens[i].type +
|
|
' at ' +
|
|
describeLocation(tokens[i]) +
|
|
' in object at ' +
|
|
describeLocation(tokens[startAt - 1])
|
|
)
|
|
}
|
|
i++
|
|
// Consume a comma after a field
|
|
if (tokens[i].type == 'comma') i++
|
|
}
|
|
return {
|
|
node: new ObjectNode(fields),
|
|
i
|
|
}
|
|
}
|
|
|
|
function shuntingYard(stream) {
|
|
const prec = {
|
|
'+': 5,
|
|
'-': 5,
|
|
'*': 10,
|
|
'/': 10,
|
|
'%': 10,
|
|
'//': 2,
|
|
'==': 3,
|
|
'!=': 3,
|
|
'>': 3,
|
|
'<': 3,
|
|
'>=': 3,
|
|
'<=': 3
|
|
}
|
|
let output = []
|
|
let operators = []
|
|
for (let x of stream) {
|
|
if (x.type == 'op') {
|
|
while (operators.length && prec[operators[0].op] >= prec[x.op])
|
|
output.push(operators.shift())
|
|
operators.unshift(x)
|
|
} else {
|
|
output.push(x)
|
|
}
|
|
}
|
|
for (let o of operators) output.push(o)
|
|
let constructors = {
|
|
'+': AdditionOperator,
|
|
'*': MultiplicationOperator,
|
|
'-': SubtractionOperator,
|
|
'/': DivisionOperator,
|
|
'%': ModuloOperator,
|
|
'//': AlternativeOperator,
|
|
'==': EqualsOperator,
|
|
'!=': NotEqualsOperator,
|
|
'<': LessThanOperator,
|
|
'>': GreaterThanOperator,
|
|
'<=': LessEqualsOperator,
|
|
'>=': GreaterEqualsOperator
|
|
}
|
|
let stack = []
|
|
for (let o of output) {
|
|
if (o.type == 'op') {
|
|
let r = stack.pop()
|
|
let l = stack.pop()
|
|
stack.push(new constructors[o.op](l, r))
|
|
} else {
|
|
stack.push(o)
|
|
}
|
|
}
|
|
return stack[0]
|
|
}
|
|
|
|
// Convert a value to a consistent type name, addressing the issue
|
|
// that arrays are objects.
|
|
function nameType(o) {
|
|
if (o === null) return 'null'
|
|
if (typeof o == 'number') return 'number'
|
|
if (typeof o == 'string') return 'string'
|
|
if (typeof o == 'boolean') return 'boolean'
|
|
if (o instanceof Array) return 'array'
|
|
if (typeof o == 'object') return 'object'
|
|
}
|
|
|
|
// Parse node classes follow. Parse nodes are:
|
|
// FilterNode, generic juxtaposition combination
|
|
// IndexNode, lhs[rhs]
|
|
// SliceNode, lhs[from:to]
|
|
// GenericIndex, .[index]
|
|
// IdentifierIndex .index (delegates to GenericIndex("index"))
|
|
// GenericSlice, .[from:to]
|
|
// IdentityNode, .
|
|
// ValueNode, parent of string/number/boolean
|
|
// StringNode, "abc"
|
|
// NumberNode, 123.45
|
|
// BooleanNode, true/false
|
|
// SpecificValueIterator, lhs[] (yields values from lhs)
|
|
// GenericValueIterator, .[] (yields values from input)
|
|
// CommaNode, .x, .y, .z
|
|
// ArrayNode, [...]
|
|
// PipeNode, a | b | c
|
|
// ObjectNode { x : y, z, "a b" : 12, (.x.y) : .z }
|
|
// RecursiveDescent, ..
|
|
// OperatorNode, a binary infix operator
|
|
// AdditionOperator, a + b
|
|
// MultiplicationOperator, a * b
|
|
// SubtractionOperator, a - b
|
|
// DivisionOperator, a / b
|
|
// ModuloOperator, a % b
|
|
// EqualsOperator, a == b
|
|
// NotEqualsOperator, a != b
|
|
// AlternativeOperator, a // b
|
|
// UpdateAssignment, .x.y |= .z
|
|
// FunctionCall, fname(arg1, arg2)
|
|
// FormatNode, @format, @format "a\(...)"
|
|
// ErrorSuppression, foo?
|
|
// VariableBinding, ... as $x (not the pipe)
|
|
// VariableReference, $x
|
|
// ReduceNode, reduce .[] as $x (0; . + $x)
|
|
// IfNode, if a then b elif c then d else e end
|
|
class ParseNode {}
|
|
class FilterNode extends ParseNode {
|
|
constructor(nodes) {
|
|
super()
|
|
this.length = nodes.length
|
|
let p = nodes.pop()
|
|
if (p) {
|
|
this.filter = p
|
|
this.source = nodes.length == 1 ? nodes[0] : new FilterNode(nodes)
|
|
}
|
|
}
|
|
*apply(input, conf) {
|
|
if (!this.filter) return
|
|
for (let v of this.source.apply(input, conf)) {
|
|
yield* this.filter.apply(v, conf)
|
|
}
|
|
}
|
|
*paths(input, conf) {
|
|
if (!this.filter) {
|
|
return []
|
|
}
|
|
for (let v of this.source.paths(input, conf)) {
|
|
for (let w of this.filter.paths(input, conf)) {
|
|
yield v.concat(w)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class IndexNode extends ParseNode {
|
|
constructor(lhs, index) {
|
|
super()
|
|
this.lhs = lhs
|
|
this.index = index
|
|
}
|
|
*apply(input, conf) {
|
|
for (let l of this.lhs.apply(input, conf)) {
|
|
let t = nameType(l)
|
|
for (let i of this.index.apply(input, conf)) {
|
|
if (t == 'array' && nameType(i) != 'number')
|
|
throw (
|
|
'Cannot index array with ' + nameType(i) + ' ' + JSON.stringify(i)
|
|
)
|
|
else if (t == 'object' && nameType(i) != 'string')
|
|
throw (
|
|
'Cannot index object with ' + nameType(i) + ' ' + JSON.stringify(i)
|
|
)
|
|
if (typeof i == 'number' && i < 0 && nameType(l) == 'array')
|
|
yield l[l.length + i]
|
|
else yield l[i]
|
|
yield typeof l[i] == 'undefined' ? null : l[i]
|
|
}
|
|
}
|
|
}
|
|
*paths(input, conf) {
|
|
for (let l of this.lhs.paths(input, conf))
|
|
for (let a of this.index.apply(input, conf)) yield l.concat([a])
|
|
}
|
|
}
|
|
class SliceNode extends ParseNode {
|
|
constructor(lhs, from, to) {
|
|
super()
|
|
this.lhs = lhs
|
|
this.from = from
|
|
this.to = to
|
|
}
|
|
*apply(input, conf) {
|
|
for (let l of this.lhs.apply(input, conf))
|
|
for (let s of this.from.apply(input, conf)) {
|
|
if (s < 0) s += l.length
|
|
for (let e of this.to.apply(input, conf)) {
|
|
if (e < 0) e += l.length
|
|
yield l.slice(s, e)
|
|
}
|
|
}
|
|
}
|
|
*paths(input, conf) {
|
|
for (let l of this.lhs.paths(input, conf))
|
|
for (let a of this.from.apply(input, conf))
|
|
for (let b of this.to.apply(input, conf))
|
|
yield l.concat([{ start: a, end: b }])
|
|
}
|
|
}
|
|
class GenericIndex extends ParseNode {
|
|
constructor(innerNode) {
|
|
super()
|
|
this.index = innerNode
|
|
}
|
|
*apply(input, conf) {
|
|
let t = nameType(input)
|
|
for (let i of this.index.apply(input, conf)) {
|
|
if (t == 'array' && nameType(i) != 'number')
|
|
throw 'Cannot index array with ' + nameType(i) + ' ' + JSON.stringify(i)
|
|
else if (t == 'object' && nameType(i) != 'string')
|
|
throw (
|
|
'Cannot index object with ' + nameType(i) + ' ' + JSON.stringify(i)
|
|
)
|
|
if (typeof i == 'number' && i < 0 && nameType(input) == 'array')
|
|
yield input[input.length + i]
|
|
else yield typeof input[i] == 'undefined' ? null : input[i]
|
|
}
|
|
}
|
|
*paths(input, conf) {
|
|
for (let a of this.index.apply(input, conf)) yield [a]
|
|
}
|
|
}
|
|
class IdentifierIndex extends GenericIndex {
|
|
constructor(v) {
|
|
super(new StringNode(v))
|
|
}
|
|
}
|
|
class GenericSlice extends ParseNode {
|
|
constructor(fr, to) {
|
|
super()
|
|
this.from = fr
|
|
this.to = to
|
|
}
|
|
*apply(input, conf) {
|
|
for (let l of this.from.apply(input, conf)) {
|
|
if (l < 0) l += input.length
|
|
for (let r of this.to.apply(input, conf)) {
|
|
if (r < 0) r += input.length
|
|
yield input.slice(l, r)
|
|
}
|
|
}
|
|
}
|
|
*paths(input, conf) {
|
|
for (let l of this.from.apply(input, conf))
|
|
for (let r of this.to.apply(input, conf)) yield [{ start: l, end: r }]
|
|
}
|
|
}
|
|
class IdentityNode extends ParseNode {
|
|
constructor() {
|
|
super()
|
|
}
|
|
*apply(input, conf) {
|
|
yield input
|
|
}
|
|
*paths(input, conf) {
|
|
yield []
|
|
}
|
|
}
|
|
class ValueNode extends ParseNode {
|
|
constructor(v) {
|
|
super()
|
|
this.value = v
|
|
}
|
|
*apply() {
|
|
yield this.value
|
|
}
|
|
*paths(input, conf) {
|
|
yield this.value
|
|
}
|
|
}
|
|
class StringNode extends ValueNode {
|
|
constructor(v) {
|
|
super(v)
|
|
}
|
|
}
|
|
class StringLiteral extends ParseNode {
|
|
constructor(strings, interpolations) {
|
|
super()
|
|
this.strings = strings
|
|
this.interpolations = interpolations
|
|
}
|
|
*apply(input, conf) {
|
|
yield* this.applyEscape(input, formats.text, conf)
|
|
}
|
|
*applyEscape(input, esc, conf, startAt = 0) {
|
|
let s = this.strings[startAt]
|
|
let i = this.interpolations[startAt]
|
|
if (!i) return yield s
|
|
for (let v of this.interpolations[startAt].apply(input, conf)) {
|
|
for (let r of this.applyEscape(input, esc, conf, startAt + 1)) {
|
|
yield s + esc(v) + r
|
|
}
|
|
}
|
|
}
|
|
}
|
|
class NumberNode extends ValueNode {
|
|
constructor(v) {
|
|
super(v)
|
|
}
|
|
}
|
|
class BooleanNode extends ValueNode {
|
|
constructor(v) {
|
|
super(v)
|
|
}
|
|
}
|
|
class SpecificValueIterator extends ParseNode {
|
|
constructor(source) {
|
|
super()
|
|
this.source = source
|
|
}
|
|
*apply(input, conf) {
|
|
for (let o of this.source.apply(input, conf)) yield* Object.values(o)
|
|
}
|
|
*paths(input, conf) {
|
|
for (let [p, v] of this.zip(
|
|
this.source.paths(input, conf),
|
|
this.source.apply(input, conf)
|
|
)) {
|
|
if (nameType(v) == 'array')
|
|
for (let i = 0; i < v.length; i++) yield p.concat([i])
|
|
else
|
|
for (let i of Object.keys(v)) {
|
|
yield p.concat([i])
|
|
}
|
|
}
|
|
}
|
|
*zip(a, b) {
|
|
let aa = a[Symbol.iterator]()
|
|
let bb = b[Symbol.iterator]()
|
|
let v1 = aa.next()
|
|
let v2 = bb.next()
|
|
while (!v1.done && !v2.done) {
|
|
yield [v1.value, v2.value]
|
|
v1 = aa.next()
|
|
v2 = bb.next()
|
|
}
|
|
}
|
|
}
|
|
class GenericValueIterator extends ParseNode {
|
|
constructor() {
|
|
super()
|
|
}
|
|
*apply(input, conf) {
|
|
if (nameType(input) == 'array') yield* input
|
|
else yield* Object.values(input)
|
|
}
|
|
*paths(input, conf) {
|
|
if (nameType(input) == 'array')
|
|
for (let i = 0; i < input.length; i++) yield [i]
|
|
else for (let o of Object.keys(input)) yield [o]
|
|
}
|
|
}
|
|
class CommaNode extends ParseNode {
|
|
constructor(branches) {
|
|
super()
|
|
this.branches = branches
|
|
}
|
|
*apply(input, conf) {
|
|
for (let b of this.branches) yield* b.apply(input, conf)
|
|
}
|
|
*paths(input, conf) {
|
|
for (let b of this.branches) yield* b.paths(input, conf)
|
|
}
|
|
}
|
|
class ArrayNode extends ParseNode {
|
|
constructor(body) {
|
|
super()
|
|
this.body = body
|
|
}
|
|
*apply(input, conf) {
|
|
yield Array.from(this.body.apply(input, conf))
|
|
}
|
|
}
|
|
class PipeNode extends ParseNode {
|
|
constructor(lhs, rhs) {
|
|
super()
|
|
this.lhs = lhs
|
|
this.rhs = rhs
|
|
}
|
|
*apply(input, conf) {
|
|
for (let v of this.lhs.apply(input, conf))
|
|
for (let q of this.rhs.apply(v, conf)) yield q
|
|
}
|
|
*paths(input, conf) {
|
|
for (let [p, v] of this.zip(
|
|
this.lhs.paths(input, conf),
|
|
this.lhs.apply(input, conf)
|
|
)) {
|
|
for (let p2 of this.rhs.paths(v, conf)) {
|
|
yield p.concat(p2)
|
|
}
|
|
}
|
|
}
|
|
*zip(a, b) {
|
|
let aa = a[Symbol.iterator]()
|
|
let bb = b[Symbol.iterator]()
|
|
let v1 = aa.next()
|
|
let v2 = bb.next()
|
|
while (!v1.done && !v2.done) {
|
|
yield [v1.value, v2.value]
|
|
v1 = aa.next()
|
|
v2 = bb.next()
|
|
}
|
|
}
|
|
}
|
|
class ObjectNode extends ParseNode {
|
|
constructor(fields) {
|
|
super()
|
|
this.fields = fields
|
|
}
|
|
*apply(input, conf) {
|
|
let obj = {}
|
|
let values = {}
|
|
let keys = []
|
|
for (let { key, value } of this.fields) {
|
|
for (let k of key.apply(input, conf)) {
|
|
keys.push(k)
|
|
values[k] = []
|
|
for (let v of value.apply(input, conf)) values[k].push(v)
|
|
}
|
|
}
|
|
yield* this.helper(keys, values, 0, {})
|
|
}
|
|
*helper(keys, values, startAt, obj) {
|
|
if (startAt >= keys.length) {
|
|
yield Object.assign({}, obj)
|
|
return
|
|
}
|
|
let k = keys[startAt]
|
|
for (let v of values[k]) {
|
|
obj[k] = v
|
|
yield* this.helper(keys, values, startAt + 1, obj)
|
|
}
|
|
}
|
|
}
|
|
class RecursiveDescent extends ParseNode {
|
|
constructor() {
|
|
super()
|
|
}
|
|
*apply(input, conf) {
|
|
yield* this.recurse(input)
|
|
}
|
|
*recurse(s) {
|
|
yield s
|
|
let t = nameType(s)
|
|
if (t == 'array' || t == 'object')
|
|
for (let v of Object.values(s)) yield* this.recurse(v)
|
|
}
|
|
*paths(input, conf) {
|
|
yield* this.recursePaths(input, [])
|
|
}
|
|
*recursePaths(s, prefix) {
|
|
yield prefix
|
|
let t = nameType(s)
|
|
if (t == 'array')
|
|
for (let i = 0; i < s.length; i++)
|
|
yield* this.recursePaths(s[i], prefix.concat([i]))
|
|
else if (t == 'object')
|
|
for (let [k, v] of Object.entries(s))
|
|
yield* this.recursePaths(v, prefix.concat([k]))
|
|
}
|
|
}
|
|
class OperatorNode extends ParseNode {
|
|
constructor(l, r) {
|
|
super()
|
|
this.l = l
|
|
this.r = r
|
|
}
|
|
*apply(input, conf) {
|
|
for (let rr of this.r.apply(input, conf))
|
|
for (let ll of this.l.apply(input, conf))
|
|
yield this.combine(ll, rr, nameType(ll), nameType(rr))
|
|
}
|
|
}
|
|
class AdditionOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt == 'number' && rt == 'number') return l + r
|
|
if (l === null) return r
|
|
if (r === null) return l
|
|
if (lt == 'string' && rt == 'string') return l + r
|
|
if (lt == 'array' && rt == 'array') return l.concat(r)
|
|
if (lt == 'object' && rt == 'object')
|
|
return Object.assign(Object.assign({}, l), r)
|
|
throw 'type mismatch in +:' + lt + ' and ' + rt + ' cannot be added'
|
|
}
|
|
}
|
|
class MultiplicationOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt == 'number' && rt == 'number') return l * r
|
|
if (lt == 'number' && rt == 'string') return this.repeat(r, l)
|
|
if (lt == 'string' && rt == 'number') return this.repeat(l, r)
|
|
if (lt == 'object' && rt == 'object')
|
|
return this.merge(Object.assign({}, l), r)
|
|
throw 'type mismatch in *:' + lt + ' and ' + rt + ' cannot be multiplied'
|
|
}
|
|
repeat(s, n) {
|
|
if (n == 0) return null
|
|
let r = []
|
|
for (let i = 0; i < n; i++) r.push(s)
|
|
return r.join('')
|
|
}
|
|
merge(l, r) {
|
|
for (let k of Object.keys(r)) {
|
|
if (!l.hasOwnProperty(k)) l[k] = r[k]
|
|
else if (nameType(l[k]) != 'object' || nameType(r[k]) != 'object')
|
|
l[k] = r[k]
|
|
else this.merge(l[k], r[k])
|
|
}
|
|
return l
|
|
}
|
|
}
|
|
class SubtractionOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt == 'number' && rt == 'number') return l - r
|
|
if (l == null || r == null) throw 'type mismatch in -'
|
|
if (lt == 'array' && rt == 'array')
|
|
return l.filter((x) => r.indexOf(x) == -1)
|
|
throw 'type mismatch in -:' + lt + ' and ' + rt + ' cannot be subtracted'
|
|
}
|
|
}
|
|
class DivisionOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt == 'number' && rt == 'number') return l / r
|
|
if (lt == 'string' && rt == 'string') return l.split(r)
|
|
throw 'type mismatch in -:' + lt + ' and ' + rt + ' cannot be divided'
|
|
}
|
|
}
|
|
class ModuloOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt == 'number' && rt == 'number') return l % r
|
|
throw (
|
|
'type mismatch in -:' +
|
|
lt +
|
|
' and ' +
|
|
rt +
|
|
' cannot be divided (remainder)'
|
|
)
|
|
}
|
|
}
|
|
class LessThanOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
return compareValues(l, r) < 0
|
|
}
|
|
}
|
|
class GreaterThanOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
return compareValues(l, r) > 0
|
|
}
|
|
}
|
|
class LessEqualsOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
return compareValues(l, r) <= 0
|
|
}
|
|
}
|
|
class GreaterEqualsOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
return compareValues(l, r) >= 0
|
|
}
|
|
}
|
|
class EqualsOperator extends OperatorNode {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
if (lt != rt) return false
|
|
if (lt == 'number' || lt == 'string' || lt == 'boolean' || lt == 'null')
|
|
return l == r
|
|
if (lt == 'array') {
|
|
if (l.length != r.length) return false
|
|
for (let i = 0; i < l.length; i++)
|
|
if (!this.combine(l[i], r[i], nameType(l[i]), nameType(r[i])))
|
|
return false
|
|
return true
|
|
}
|
|
let lk = Object.keys(l)
|
|
let rk = Object.keys(r)
|
|
if (lk.length != rk.length) return false
|
|
for (let k of lk) {
|
|
if (!r.hasOwnProperty(k)) return false
|
|
if (!this.combine(l[k], r[k], nameType(l[k]), nameType(r[k])))
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
class NotEqualsOperator extends EqualsOperator {
|
|
constructor(l, r) {
|
|
super(l, r)
|
|
}
|
|
combine(l, r, lt, rt) {
|
|
return !super.combine(l, r, lt, rt)
|
|
}
|
|
}
|
|
class AlternativeOperator extends ParseNode {
|
|
constructor(l, r) {
|
|
super()
|
|
this.lhs = l
|
|
this.rhs = r
|
|
}
|
|
*apply(input, conf) {
|
|
let found = false
|
|
for (let v of this.lhs.apply(input, conf)) {
|
|
if (v !== null) found = true
|
|
yield v
|
|
}
|
|
if (!found) yield* this.rhs.apply(input, conf)
|
|
}
|
|
}
|
|
class UpdateAssignment extends ParseNode {
|
|
constructor(l, r) {
|
|
super()
|
|
this.l = l
|
|
this.r = r
|
|
}
|
|
*apply(input, conf) {
|
|
input = JSON.parse(JSON.stringify(input))
|
|
for (let p of this.l.paths(input, conf)) {
|
|
let it = this.r.apply(this.get(input, p), conf).next()
|
|
if (it.done) input = this.update(input, p, null, true)
|
|
else input = this.update(input, p, it.value)
|
|
}
|
|
yield input
|
|
}
|
|
// Pluck the value at a path out of an object
|
|
get(obj, p) {
|
|
let o = obj
|
|
for (let i of p) o = o[i]
|
|
return o
|
|
}
|
|
// Set the value at path p to v in obj,
|
|
// or delete the key if del is true.
|
|
update(obj, p, v, del = false) {
|
|
let o = obj
|
|
let last = p.pop()
|
|
for (let i of p) o = o[i]
|
|
if (typeof last == 'undefined') return v
|
|
o[last] = v
|
|
if (del) delete o[last]
|
|
return obj
|
|
}
|
|
}
|
|
class FunctionCall extends ParseNode {
|
|
constructor(fname, args) {
|
|
super()
|
|
this.name = fname
|
|
this.args = args
|
|
}
|
|
apply(input, conf) {
|
|
let func
|
|
let ufa = conf.userFuncArgs[this.name]
|
|
if (ufa)
|
|
func = function (input, conf, args) {
|
|
return ufa.apply(input, conf)
|
|
}
|
|
else if (!func) func = functions[this.name]
|
|
if (!func) throw 'no such function ' + this.name
|
|
let argStack = []
|
|
return func(input, conf, this.args)
|
|
}
|
|
paths(input, conf) {
|
|
let ufa = conf.userFuncArgs[this.name]
|
|
if (ufa) return ufa.paths(input, conf)
|
|
let func = functions[this.name + '-paths']
|
|
if (!func) throw 'no paths for ' + this.name
|
|
return func(input, conf, this.args)
|
|
}
|
|
}
|
|
class FormatNode extends ParseNode {
|
|
constructor(fname, quote) {
|
|
super()
|
|
this.name = fname
|
|
this.string = quote
|
|
}
|
|
*apply(input, conf) {
|
|
if (!this.string) return yield formats[this.name](input)
|
|
yield* this.string.applyEscape(input, formats[this.name], conf)
|
|
}
|
|
}
|
|
class ErrorSuppression extends ParseNode {
|
|
constructor(inner) {
|
|
super()
|
|
this.inner = inner
|
|
}
|
|
*apply(input, conf) {
|
|
try {
|
|
for (let o of this.inner.apply(input, conf)) if (o !== null) yield o
|
|
} catch {}
|
|
}
|
|
*paths(input, conf) {
|
|
try {
|
|
for (let [o, p] of zip(
|
|
this.inner.apply(input, conf),
|
|
this.inner.paths(input, conf)
|
|
))
|
|
if (o !== null) yield p
|
|
} catch {}
|
|
}
|
|
}
|
|
class VariableBinding extends ParseNode {
|
|
constructor(lhs, name) {
|
|
super()
|
|
this.value = lhs
|
|
this.name = name
|
|
}
|
|
*apply(input, conf) {
|
|
for (let v of this.value.apply(input, conf)) {
|
|
conf.variables[this.name] = v
|
|
yield input
|
|
}
|
|
delete conf.variables[this.name]
|
|
}
|
|
}
|
|
class VariableReference extends ParseNode {
|
|
constructor(name) {
|
|
super()
|
|
this.name = name
|
|
}
|
|
*apply(input, conf) {
|
|
yield conf.variables[this.name]
|
|
}
|
|
}
|
|
class ReduceNode extends ParseNode {
|
|
constructor(generator, name, init, expr) {
|
|
super()
|
|
this.generator = generator
|
|
this.name = name
|
|
this.init = init
|
|
this.expr = expr
|
|
}
|
|
*apply(input, conf) {
|
|
// This uses all values of the initialiser, but only the
|
|
// last value of the reduction expression is retained. This
|
|
// seems to match jq proper's behaviour, but jq has odd
|
|
// errors in mixed cases that seem unnecessary.
|
|
for (let accum of this.init.apply(input, conf)) {
|
|
for (let v of this.generator.apply(input, conf)) {
|
|
conf.variables[this.name] = v
|
|
for (let a of this.expr.apply(accum, conf)) accum = a
|
|
}
|
|
delete conf.variables[this.name]
|
|
yield accum
|
|
}
|
|
}
|
|
}
|
|
class IfNode extends ParseNode {
|
|
constructor(conditions, thens, elseBranch) {
|
|
super()
|
|
this.conditions = conditions
|
|
this.thens = thens
|
|
this.elseBranch = elseBranch
|
|
}
|
|
*apply(input, conf) {
|
|
for (let [c, t] of zip(this.conditions, this.thens)) {
|
|
for (let cond of c.apply(input, conf)) {
|
|
if (cond) {
|
|
for (let o of t.apply(input, conf)) yield o
|
|
return
|
|
}
|
|
}
|
|
}
|
|
if (this.elseBranch) {
|
|
yield* this.elseBranch.apply(input, conf)
|
|
return
|
|
}
|
|
yield input
|
|
}
|
|
}
|
|
|
|
const formats = {
|
|
text(v) {
|
|
if (typeof v == 'string') return v
|
|
return prettyPrint(v, '', '', '')
|
|
},
|
|
json(v) {
|
|
return prettyPrint(v, '', '', '')
|
|
},
|
|
html(v) {
|
|
if (typeof v != 'string') v = prettyPrint(v, '', '', '')
|
|
return v
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/&/g, '&')
|
|
.replace(/'/g, ''')
|
|
.replace(/"/g, '"')
|
|
},
|
|
uri(v) {
|
|
if (typeof v != 'string') v = prettyPrint(v, '', '', '')
|
|
return escape(v)
|
|
},
|
|
csv(v) {
|
|
if (nameType(v) != 'array')
|
|
throw 'cannot csv-format ' + nameType(v) + ', only array'
|
|
return v
|
|
.map((x) => {
|
|
if (typeof x == 'string') return '"' + x.replace(/"/g, '""') + '"'
|
|
else if (typeof x == 'number') return '' + x
|
|
else if (x === null) return ''
|
|
else throw 'type ' + nameType(x) + ' not valid in a csv row'
|
|
})
|
|
.join(',')
|
|
},
|
|
tsv(v) {
|
|
if (nameType(v) != 'array')
|
|
throw 'cannot tsv-format ' + nameType(v) + ', only array'
|
|
return v
|
|
.map((x) => {
|
|
if (typeof x == 'string') return escapeString(x)
|
|
else if (typeof x == 'number') return '' + x
|
|
else if (x === null) return ''
|
|
else throw 'type ' + nameType(x) + ' not valid in a tsv row'
|
|
})
|
|
.join('\t')
|
|
},
|
|
base64(v) {
|
|
if (typeof v != 'string') v = prettyPrint(v, '', '', '')
|
|
return btoa(v)
|
|
},
|
|
base64d(v) {
|
|
if (typeof v != 'string') throw 'can only base64-decode strings'
|
|
return atob(v)
|
|
},
|
|
sh(v) {
|
|
let t = nameType(v)
|
|
if (t == 'string') return "'" + t.replace(/'/g, "'\\''") + "'"
|
|
else if (t == 'number') return '' + v
|
|
else if (t == 'boolean') return '' + v
|
|
else if (v === null) return 'null'
|
|
else if (t === 'array') {
|
|
return v
|
|
.map((v) => {
|
|
let t = nameType(v)
|
|
if (t == 'string') return "'" + v.replace(/'/g, "'\\''") + "'"
|
|
else if (t == 'number') return '' + v
|
|
else if (t == 'boolean') return '' + v
|
|
else if (v === null) return 'null'
|
|
else throw t + ' cannot be escaped for shell'
|
|
})
|
|
.join(' ')
|
|
} else throw t + ' cannot be escaped for shell'
|
|
}
|
|
}
|
|
|
|
const functions = {
|
|
'tostring/0': function* (input) {
|
|
yield formats.text(input)
|
|
},
|
|
'empty/0': function* (input) {},
|
|
'path/1': function* (input, conf, args) {
|
|
let f = args[0]
|
|
yield* f.paths(input, conf)
|
|
},
|
|
'select/1': function* (input, conf, args) {
|
|
let selector = args[0]
|
|
for (let b of selector.apply(input, conf)) if (b) yield input
|
|
},
|
|
'select/1-paths': function* (input, conf, args) {
|
|
let selector = args[0]
|
|
for (let b of selector.apply(input, conf)) if (b) yield []
|
|
},
|
|
'length/0': function* (input) {
|
|
let t = nameType(input)
|
|
if (t == 'string' || t == 'array') return yield input.length
|
|
if (t == 'null') return yield 0
|
|
if (t == 'object') return yield Object.keys(input).length
|
|
throw 'cannot compute length of ' + t
|
|
},
|
|
'keys/0': function* (input) {
|
|
yield* Object.keys(input).sort()
|
|
},
|
|
'has/1': function* (input, conf, args) {
|
|
let f = args[0]
|
|
for (let k of f.apply(input, conf)) yield input.hasOwnProperty(k)
|
|
},
|
|
'has/1-paths': function* (input, conf, args) {
|
|
let f = args[0]
|
|
for (let k of f.apply(input, conf)) if (input.hasOwnProperty(k)) yield []
|
|
},
|
|
'in/1': function* (input, conf, args) {
|
|
let f = args[0]
|
|
for (let o of f.apply(input, conf)) yield o.hasOwnProperty(input)
|
|
},
|
|
'in/1-paths': function* (input, conf, args) {
|
|
let f = args[0]
|
|
for (let o of f.apply(input, conf)) if (o.hasOwnProperty(input)) yield []
|
|
},
|
|
'contains/1': function* (input, conf, args) {
|
|
let f = args[0]
|
|
let t = nameType(input)
|
|
for (let o of f.apply(input, conf)) {
|
|
let ot = nameType(o)
|
|
if (t != ot) {
|
|
throw t + ' and ' + ot + ' cannot have their containment checked'
|
|
} else yield containsHelper(input, o)
|
|
}
|
|
},
|
|
'inside/1': function* (input, conf, args) {
|
|
let f = args[0]
|
|
let t = nameType(input)
|
|
for (let o of f.apply(input, conf)) {
|
|
let ot = nameType(o)
|
|
if (t != ot) {
|
|
throw t + ' and ' + ot + ' cannot have their containment checked'
|
|
} else yield containsHelper(o, input)
|
|
}
|
|
},
|
|
'to_entries/0': function* (input, conf) {
|
|
let t = nameType(input)
|
|
if (t == 'array') {
|
|
let ret = []
|
|
for (let i = 0; i < input.length; i++)
|
|
ret.push({ key: i, value: input[i] })
|
|
yield ret
|
|
} else if (t == 'object')
|
|
yield Object.entries(input).map((a) => ({ key: a[0], value: a[1] }))
|
|
else throw 'cannot make entries from ' + t
|
|
},
|
|
'from_entries/0': function* (input, conf) {
|
|
let t = nameType(input)
|
|
if (t == 'array') {
|
|
let obj = {}
|
|
for (let { key, value } of input) obj[key] = value
|
|
yield obj
|
|
} else throw 'cannot use entries from ' + t
|
|
},
|
|
'type/0': function* (input) {
|
|
yield nameType(input)
|
|
},
|
|
'range/1': function* (input, conf, args) {
|
|
for (let m of args[0].apply(input, conf))
|
|
for (let i = 0; i < m; i++) yield i
|
|
},
|
|
'range/2': function* (input, conf, args) {
|
|
for (let min of args[0].apply(input, conf))
|
|
for (let max of args[1].apply(input, conf))
|
|
for (let i = min; i < max; i++) yield i
|
|
},
|
|
'range/3': function* (input, conf, args) {
|
|
for (let min of args[0].apply(input, conf))
|
|
for (let max of args[1].apply(input, conf))
|
|
for (let step of args[2].apply(input, conf))
|
|
for (let i = min; i < max; i += step) yield i
|
|
},
|
|
'any/0': function* (input, conf) {
|
|
if (nameType(input) != 'array')
|
|
throw 'any/0 requires array as input, not ' + nameType(input)
|
|
for (let b of input) if (b) return yield true
|
|
yield false
|
|
},
|
|
'any/1': function* (input, conf, args) {
|
|
if (nameType(input) != 'array')
|
|
throw 'any/1 requires array as input, not ' + nameType(input)
|
|
for (let v of input)
|
|
for (let b of args[0].apply(v, conf)) if (b) return yield true
|
|
yield false
|
|
},
|
|
'any/2': function* (input, conf, args) {
|
|
let gen = args[0]
|
|
let cond = args[1]
|
|
for (let v of gen.apply(input, conf))
|
|
for (let b of cond.apply(v, conf)) if (b) return yield true
|
|
yield false
|
|
},
|
|
'all/0': function* (input, conf) {
|
|
if (nameType(input) != 'array')
|
|
throw 'all/0 requires array as input, not ' + nameType(input)
|
|
for (let b of input) if (!b) return yield false
|
|
yield true
|
|
},
|
|
'all/1': function* (input, conf, args) {
|
|
if (nameType(input) != 'array')
|
|
throw 'all/1 requires array as input, not ' + nameType(input)
|
|
for (let v of input)
|
|
for (let b of args[0].apply(v, conf)) if (!b) return yield false
|
|
yield true
|
|
},
|
|
'all/2': function* (input, conf, args) {
|
|
let gen = args[0]
|
|
let cond = args[1]
|
|
for (let v of gen.apply(input, conf))
|
|
for (let b of cond.apply(v, conf)) if (!b) return yield false
|
|
yield true
|
|
},
|
|
'add/0': function* (input, conf) {
|
|
if (nameType(input) != 'array') throw 'can only add up arrays'
|
|
if (input.length == 0) return yield null
|
|
if (input.length == 1) return yield input[0]
|
|
let ret = AdditionOperator.prototype.combine(
|
|
input[0],
|
|
input[1],
|
|
nameType(input[0]),
|
|
nameType(input[1])
|
|
)
|
|
for (let i = 2; i < input.length; i++)
|
|
ret = AdditionOperator.prototype.combine(
|
|
ret,
|
|
input[i],
|
|
nameType(ret),
|
|
nameType(input[i])
|
|
)
|
|
yield ret
|
|
},
|
|
'round/0': function* (input) {
|
|
yield Math.round(input)
|
|
},
|
|
'round/1': function* (input, conf, args) {
|
|
yield input.toFixed(args[0].value)
|
|
},
|
|
'tonumber/0': function* (input) {
|
|
yield Number.parseFloat(input)
|
|
},
|
|
'sort/0': function* (input, conf) {
|
|
if (nameType(input) != 'array')
|
|
throw 'can only sort arrays, not ' + nameType(input)
|
|
let r = Array.from(input)
|
|
yield r.sort(compareValues)
|
|
},
|
|
'sort_by/1': function* (input, conf, args) {
|
|
if (nameType(input) != 'array')
|
|
throw 'can only sort arrays, not ' + nameType(input)
|
|
let key = args[0]
|
|
let r = input.map((v) => ({
|
|
key: key.apply(v, conf).next().value,
|
|
value: v
|
|
}))
|
|
r.sort((a, b) => compareValues(a.key, b.key))
|
|
yield r.map((a) => a.value)
|
|
},
|
|
'explode/0': function* (input, conf) {
|
|
if (nameType(input) != 'string')
|
|
throw 'can only explode string, not ' + nameType(input)
|
|
let ret = []
|
|
for (let i = 0; i < input.length; i++) {
|
|
let c = input.charCodeAt(i)
|
|
ret.push(c)
|
|
if (c > 0xffff) i++
|
|
}
|
|
yield ret
|
|
},
|
|
'implode/0': function* (input, conf) {
|
|
if (nameType(input) != 'array')
|
|
throw 'can only implode array, not ' + nameType(input)
|
|
yield input.map((x) => String.fromCodePoint(x)).join('')
|
|
},
|
|
'split/1': function* (input, conf, args) {
|
|
if (nameType(input) != 'string')
|
|
throw 'can only split string, not ' + nameType(input)
|
|
for (let s of args[0].apply(input, conf)) {
|
|
yield input.split(s)
|
|
}
|
|
},
|
|
'join/1': function* (input, conf, args) {
|
|
if (nameType(input) != 'array')
|
|
throw 'can only join array, not ' + nameType(input)
|
|
let a = input.map((x) => {
|
|
if (typeof x == 'number') return '' + x
|
|
if (typeof x == 'string') return x
|
|
if (typeof x == 'boolean') return '' + x
|
|
if (x === null) return ''
|
|
throw 'cannot join ' + nameType(x)
|
|
})
|
|
for (let s of args[0].apply(input, conf)) yield a.join(s)
|
|
},
|
|
'test/1': function* (input, conf, args) {
|
|
const regex = new RegExp(args[0].value)
|
|
const isValid = regex.test(input)
|
|
|
|
if (isValid) yield [input]
|
|
}
|
|
}
|
|
|
|
// Implements the containment algorithm, returning whether haystack
|
|
// contains needle:
|
|
// * Strings are contained if they are substrings
|
|
// * Arrays if each element is contained in some element of other
|
|
// * Object if values contained by values in matching key
|
|
// * All others, if they are equal.
|
|
// This helper function is necessary because the recursive case
|
|
// has different error behaviour to the user-exposed function.
|
|
function containsHelper(haystack, needle) {
|
|
let haystackType = nameType(haystack)
|
|
let needleType = nameType(needle)
|
|
if (haystackType != needleType) {
|
|
return false
|
|
} else if (haystackType == 'string') {
|
|
return haystack.indexOf(needle) != -1
|
|
} else if (haystackType == 'array') {
|
|
for (let b of needle) {
|
|
let found = false
|
|
for (let a of haystack) {
|
|
if (containsHelper(a, b)) {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if (!found) return false
|
|
}
|
|
return true
|
|
} else if (haystackType == 'object') {
|
|
for (let k of Object.keys(needle)) {
|
|
if (!haystack.hasOwnProperty(k)) return false
|
|
if (!containsHelper(haystack[k], needle[k])) return false
|
|
}
|
|
return true
|
|
} else {
|
|
return haystack === needle
|
|
}
|
|
}
|
|
|
|
defineShorthandFunction('map', 'f', '[.[] | f]')
|
|
defineShorthandFunction('map_values', 'f', '.[] |= f')
|
|
defineShorthandFunction('del', 'p', 'p |= empty')
|
|
defineShorthandFunction(
|
|
'with_entries',
|
|
'w',
|
|
'to_entries | map(w) | from_entries'
|
|
)
|
|
defineShorthandFunction('arrays', '', 'select(type == "array")')
|
|
defineShorthandFunction('objects', '', 'select(type == "object")')
|
|
defineShorthandFunction('booleans', '', 'select(type == "boolean")')
|
|
defineShorthandFunction('strings', '', 'select(type == "string")')
|
|
defineShorthandFunction('numbers', '', 'select(type == "number")')
|
|
defineShorthandFunction('nulls', '', 'select(type == "null")')
|
|
|
|
/**
|
|
* JSON filter command using the vendored jqjs engine (jq language subset).
|
|
* Not bit-identical to https://github.com/jqlang/jq — see man jq bareOsNotes.
|
|
*/
|
|
async function run(ctx, argv) {
|
|
const vfs = ctx.vfs
|
|
|
|
let nullInput = false
|
|
let slurp = false
|
|
let rawInput = false
|
|
let compact = false
|
|
let rawOut = false
|
|
let exitStatus = false
|
|
let programPath = null
|
|
|
|
const args = argv.slice(1)
|
|
let i = 0
|
|
|
|
function usage() {
|
|
ctx.console.error(
|
|
'usage: jq [-n] [-R] [-s] [-c] [-r] [-e] [-f file] filter [file...]'
|
|
)
|
|
ctx.exitCode = 2
|
|
}
|
|
|
|
while (i < args.length) {
|
|
const a = args[i]
|
|
if (a === '--') {
|
|
i++
|
|
break
|
|
}
|
|
if (a === '-' || !a.startsWith('-')) break
|
|
|
|
if (a === '-n' || a === '--null-input') {
|
|
nullInput = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-R' || a === '--raw-input') {
|
|
rawInput = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-s' || a === '--slurp') {
|
|
slurp = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-c' || a === '--compact-output') {
|
|
compact = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-r' || a === '--raw-output') {
|
|
rawOut = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-e' || a === '--exit-status') {
|
|
exitStatus = true
|
|
i++
|
|
continue
|
|
}
|
|
if (a === '-f' || a === '--from-file') {
|
|
if (i + 1 >= args.length) {
|
|
usage()
|
|
return
|
|
}
|
|
programPath = args[i + 1]
|
|
i += 2
|
|
continue
|
|
}
|
|
if (a.startsWith('--')) {
|
|
ctx.console.error('jq: unknown option ' + a)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
|
|
const rest = a.slice(1)
|
|
let consumeNextArg = 0
|
|
for (let j = 0; j < rest.length; j++) {
|
|
const c = rest[j]
|
|
switch (c) {
|
|
case 'n':
|
|
nullInput = true
|
|
break
|
|
case 'R':
|
|
rawInput = true
|
|
break
|
|
case 's':
|
|
slurp = true
|
|
break
|
|
case 'c':
|
|
compact = true
|
|
break
|
|
case 'r':
|
|
rawOut = true
|
|
break
|
|
case 'e':
|
|
exitStatus = true
|
|
break
|
|
case 'f':
|
|
if (j + 1 < rest.length) {
|
|
programPath = rest.slice(j + 1)
|
|
j = rest.length
|
|
} else {
|
|
if (i + 1 >= args.length) {
|
|
usage()
|
|
return
|
|
}
|
|
programPath = args[i + 1]
|
|
consumeNextArg = 1
|
|
j = rest.length
|
|
}
|
|
break
|
|
default:
|
|
ctx.console.error('jq: invalid option -- ' + c)
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
}
|
|
i += 1 + consumeNextArg
|
|
}
|
|
|
|
let program
|
|
if (programPath) {
|
|
const buf = await vfs.readFile(programPath)
|
|
if (!buf) {
|
|
ctx.console.error('jq: Could not open ' + programPath)
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
program = ctx.b4a.toString(buf).replace(/\r\n/g, '\n')
|
|
} else {
|
|
if (i >= args.length) {
|
|
usage()
|
|
return
|
|
}
|
|
program = args[i++]
|
|
}
|
|
|
|
const fileArgs = args.slice(i)
|
|
|
|
if (nullInput && rawInput) {
|
|
ctx.console.error('jq: -n and -R are mutually exclusive')
|
|
ctx.exitCode = 2
|
|
return
|
|
}
|
|
|
|
let runFn
|
|
try {
|
|
runFn = compile(program)
|
|
} catch (e) {
|
|
ctx.console.error('jq: compile error: ' + (e && e.message ? e.message : e))
|
|
ctx.exitCode = 3
|
|
return
|
|
}
|
|
|
|
/** @type {unknown[]} */
|
|
let inputs
|
|
try {
|
|
inputs = await bareJqBuildInputs(ctx, vfs, fileArgs, {
|
|
nullInput,
|
|
slurp,
|
|
rawInput
|
|
})
|
|
} catch (e) {
|
|
ctx.console.error('jq: ' + (e && e.message ? e.message : e))
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
|
|
let lastOut
|
|
let anyOut = false
|
|
|
|
function formatOne(val) {
|
|
if (typeof val === 'undefined') return null
|
|
if (rawOut && typeof val === 'string') return val
|
|
if (compact) return JSON.stringify(val)
|
|
return prettyPrint(val)
|
|
}
|
|
|
|
for (const input of inputs) {
|
|
let iter
|
|
try {
|
|
iter = runFn(input)
|
|
} catch (e) {
|
|
ctx.console.error('jq: error: ' + (e && e.message ? e.message : e))
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
try {
|
|
for (const val of iter) {
|
|
const line = formatOne(val)
|
|
if (line !== null) {
|
|
ctx.console.log(line)
|
|
anyOut = true
|
|
lastOut = val
|
|
}
|
|
}
|
|
} catch (e) {
|
|
ctx.console.error('jq: error: ' + (e && e.message ? e.message : e))
|
|
ctx.exitCode = 1
|
|
return
|
|
}
|
|
}
|
|
|
|
if (exitStatus) {
|
|
if (!anyOut) ctx.exitCode = 4
|
|
else if (lastOut === false || lastOut === null) ctx.exitCode = 1
|
|
}
|
|
}
|
|
|
|
function bareJqSkipWs(str, i) {
|
|
while (i < str.length && /\s/.test(str[i])) i++
|
|
return i
|
|
}
|
|
|
|
function bareJqEndOfJsonValue(str, start) {
|
|
let i = bareJqSkipWs(str, start)
|
|
if (i >= str.length) throw new SyntaxError('Unexpected end of JSON input')
|
|
const c = str[i]
|
|
if (c === '"') {
|
|
let j = i + 1
|
|
while (j < str.length) {
|
|
if (str[j] === '\\') {
|
|
j += 2
|
|
continue
|
|
}
|
|
if (str[j] === '"') return j + 1
|
|
j++
|
|
}
|
|
throw new SyntaxError('Unclosed string in JSON')
|
|
}
|
|
if (c === '{' || c === '[') {
|
|
const stack = [c === '{' ? '}' : ']']
|
|
let j = i + 1
|
|
let inStr = false
|
|
let esc = false
|
|
while (j < str.length) {
|
|
const ch = str[j]
|
|
if (inStr) {
|
|
if (esc) esc = false
|
|
else if (ch === '\\') esc = true
|
|
else if (ch === '"') inStr = false
|
|
} else {
|
|
if (ch === '"') inStr = true
|
|
else if (ch === '{') stack.push('}')
|
|
else if (ch === '[') stack.push(']')
|
|
else if (ch === '}' || ch === ']') {
|
|
const want = stack.pop()
|
|
if (ch !== want) throw new SyntaxError('Mismatched bracket in JSON')
|
|
if (stack.length === 0) return j + 1
|
|
}
|
|
}
|
|
j++
|
|
}
|
|
throw new SyntaxError('Unclosed array or object in JSON')
|
|
}
|
|
if (c === '-' || (c >= '0' && c <= '9')) {
|
|
let j = i
|
|
if (str[j] === '-') {
|
|
j++
|
|
if (j >= str.length) throw new SyntaxError('Invalid number')
|
|
}
|
|
if (str[j] === '0') {
|
|
j++
|
|
if (j < str.length && str[j] >= '1' && str[j] <= '9')
|
|
throw new SyntaxError('Invalid number')
|
|
} else if (str[j] >= '1' && str[j] <= '9') {
|
|
while (j < str.length && str[j] >= '0' && str[j] <= '9') j++
|
|
} else if (str[j] === '0') {
|
|
j++
|
|
} else {
|
|
throw new SyntaxError('Invalid number')
|
|
}
|
|
if (j < str.length && str[j] === '.') {
|
|
j++
|
|
if (j >= str.length || str[j] < '0' || str[j] > '9')
|
|
throw new SyntaxError('Invalid number')
|
|
while (j < str.length && str[j] >= '0' && str[j] <= '9') j++
|
|
}
|
|
if (j < str.length && (str[j] === 'e' || str[j] === 'E')) {
|
|
j++
|
|
if (j < str.length && (str[j] === '+' || str[j] === '-')) j++
|
|
if (j >= str.length || str[j] < '0' || str[j] > '9')
|
|
throw new SyntaxError('Invalid number')
|
|
while (j < str.length && str[j] >= '0' && str[j] <= '9') j++
|
|
}
|
|
return j
|
|
}
|
|
if (str.startsWith('null', i)) return i + 4
|
|
if (str.startsWith('true', i)) return i + 4
|
|
if (str.startsWith('false', i)) return i + 5
|
|
throw new SyntaxError('Unexpected token in JSON at position ' + i)
|
|
}
|
|
|
|
function bareJqParseJsonValues(text) {
|
|
const values = []
|
|
let i = 0
|
|
while (true) {
|
|
i = bareJqSkipWs(text, i)
|
|
if (i >= text.length) break
|
|
const end = bareJqEndOfJsonValue(text, i)
|
|
values.push(JSON.parse(text.slice(i, end)))
|
|
i = end
|
|
}
|
|
return values
|
|
}
|
|
|
|
function bareJqRawLines(text) {
|
|
const lines = text.replace(/\r\n/g, '\n').split('\n')
|
|
if (lines.length && lines[lines.length - 1] === '') lines.pop()
|
|
return lines
|
|
}
|
|
|
|
/**
|
|
* @param {unknown} ctx
|
|
* @param {unknown} vfs
|
|
* @param {string[]} fileArgs
|
|
* @param {{ nullInput: boolean, slurp: boolean, rawInput: boolean }} opts
|
|
*/
|
|
async function bareJqBuildInputs(ctx, vfs, fileArgs, opts) {
|
|
if (opts.nullInput) return [null]
|
|
|
|
const parts = []
|
|
if (fileArgs.length === 0) {
|
|
const stdin = bareStdin(ctx)
|
|
parts.push(stdin == null ? '' : String(stdin))
|
|
} else {
|
|
for (const name of fileArgs) {
|
|
if (name === '-') {
|
|
parts.push(String(bareStdin(ctx) ?? ''))
|
|
} else {
|
|
const buf = await vfs.readFile(name)
|
|
if (!buf) throw new Error(name + ': No such file or directory')
|
|
parts.push(ctx.b4a.toString(buf))
|
|
}
|
|
}
|
|
}
|
|
|
|
if (opts.rawInput) {
|
|
const lines = []
|
|
for (const p of parts) {
|
|
for (const line of bareJqRawLines(p)) lines.push(line)
|
|
}
|
|
if (opts.slurp) return [lines]
|
|
return lines
|
|
}
|
|
|
|
const merged = parts.join('')
|
|
const values = bareJqParseJsonValues(merged)
|
|
if (opts.slurp) return [values]
|
|
return values
|
|
}
|