-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathindex.js
More file actions
225 lines (203 loc) · 6.2 KB
/
Copy pathindex.js
File metadata and controls
225 lines (203 loc) · 6.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
#!/usr/bin/env node
import fs from 'node:fs'
import process from 'node:process'
import { parse, print } from 'maml-ast'
const NULL = { type: 'Null', value: null }
async function readStdin() {
const chunks = []
for await (const chunk of process.stdin) chunks.push(chunk)
return Buffer.concat(chunks).toString('utf8')
}
void (async function main() {
let flagHelp = false
const args = []
for (const arg of process.argv.slice(2)) {
if (arg === '--help' || arg === '-h') flagHelp = true
else args.push(arg)
}
if (flagHelp || (args.length === 0 && process.stdin.isTTY)) {
return printUsage()
}
const theme = process.stdout.isTTY
? ['', '1;34', '32', '36', '35', '38;5;243']
: ['', '', '', '', '', '']
const color = (x) => (str) => {
if (theme[x] === '') return str
return `\x1b[${theme[x]}m${str}\x1b[0m`
}
const colors = {
string: color(2),
number: color(3),
boolean: color(4),
null: color(5),
key: color(1),
comment: color(5),
bracket: color(0),
colon: color(0),
}
let input
let filename
if (args.length > 0) {
filename = isFile(fs, args[0])
? args.shift()
: isFile(fs, args.at(-1))
? args.pop()
: undefined
if (filename) {
input = fs.readFileSync(filename, 'utf8')
}
}
if (input === undefined) {
input = await readStdin()
}
const doc = parse(input)
reduce(doc, args, colors, filename)
})()
function parseSteps(path) {
const steps = []
const re = /\.([a-zA-Z_][a-zA-Z_0-9]*)|\.?\[(\d+)]|\.?\[]/g
let m
while ((m = re.exec(path)) !== null) {
if (m[1] !== undefined) steps.push({ type: 'prop', name: m[1] })
else if (m[2] !== undefined)
steps.push({ type: 'index', index: Number(m[2]) })
else steps.push({ type: 'iter' })
}
return steps
}
// Wrap value nodes into a synthetic Array node so output stays valid MAML.
function arrayNode(values) {
return {
type: 'Array',
elements: values.map((value) => ({
value,
leadingComments: [],
trailingComment: null,
emptyLineBefore: false,
})),
danglingComments: [],
}
}
// Descend one step into a node, returning the child's value node. When
// `lenient` is set (during iteration), missing properties or indices yield
// null instead of throwing, matching jq's behaviour.
function descend(current, step, lenient) {
if (current.type === 'Document') current = current.value
if (lenient && current.type === 'Null') return NULL
if (step.type === 'prop') {
if (current.type !== 'Object')
throw new Error(`Cannot access .${step.name} on ${current.type}`)
const prop = current.properties.find((p) => p.key.value === step.name)
if (!prop) {
if (lenient) return NULL
throw new Error(`Property "${step.name}" not found`)
}
return prop.value
} else {
if (current.type !== 'Array')
throw new Error(`Cannot access [${step.index}] on ${current.type}`)
const el = current.elements[step.index]
if (!el) {
if (lenient) return NULL
throw new Error(`Index ${step.index} out of bounds`)
}
return el.value
}
}
function query(node, path) {
let nodes = [node]
let iterated = false
for (const step of parseSteps(path)) {
if (step.type === 'iter') {
const next = []
for (let current of nodes) {
if (current.type === 'Document') current = current.value
if (current.type !== 'Array')
throw new Error(`Cannot iterate [] on ${current.type}`)
for (const el of current.elements) next.push(el.value)
}
nodes = next
iterated = true
} else {
nodes = nodes.map((current) => descend(current, step, iterated))
}
}
return iterated ? arrayNode(nodes) : nodes[0]
}
// Assign a parsed maml value to the node at `path`, mutating in place.
function assign(node, path, valueText) {
const steps = parseSteps(path)
if (steps.length === 0) throw new Error('Cannot assign to root')
if (steps.some((s) => s.type === 'iter'))
throw new Error('Cannot assign through [] iteration')
let value = parse(valueText)
if (value.type === 'Document') value = value.value
let current = node
for (let k = 0; k < steps.length - 1; k++)
current = descend(current, steps[k])
if (current.type === 'Document') current = current.value
const last = steps[steps.length - 1]
if (last.type === 'prop') {
if (current.type !== 'Object')
throw new Error(`Cannot access .${last.name} on ${current.type}`)
const prop = current.properties.find((p) => p.key.value === last.name)
if (!prop) throw new Error(`Property "${last.name}" not found`)
prop.value = value
} else {
if (current.type !== 'Array')
throw new Error(`Cannot access [${last.index}] on ${current.type}`)
const el = current.elements[last.index]
if (!el) throw new Error(`Index ${last.index} out of bounds`)
el.value = value
}
return node
}
function reduce(json, args, colors, filename) {
let i,
code,
output = json
for ([i, code] of args.entries())
try {
if (code === 'save') {
if (!filename)
throw new Error(
'Specify a file as the first argument to be able to save: mx file.json ...',
)
fs.writeFileSync(filename, print(output) + '\n')
} else {
const m = code.match(/^(\.[^=\s]*)\s*=\s*([\s\S]+)$/)
if (m) output = assign(output, m[1], m[2])
else output = query(output, code)
}
} catch (err) {
printErr(err)
}
console.log(print(output, { colors }))
function printErr(err) {
let pre = args.slice(0, i).join(' ')
let post = args.slice(i + 1).join(' ')
if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
if (post.length > 20) post = post.substring(0, 20) + '...'
console.error(
`\n ${pre} ${code} ${post}\n` +
` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
`\n${err.stack || err}`,
)
process.exit(1)
}
}
function isFile(fs, path) {
try {
const stat = fs.statSync(path, { throwIfNoEntry: false })
return stat !== undefined && stat.isFile()
} catch (err) {
return false
}
}
function printUsage() {
const usage = `Usage
mx [flags] [code...]
Flags
-h, --help print help`
console.log(usage)
}