fx 25.0.0 → 27.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +51 -11
  2. package/index.js +472 -41
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # fx
2
2
 
3
- The JavaScript version of the **fx**. Short for _Function eXecution_ or _f(x)_.
3
+ A non-interactive, JavaScript version of the [**fx**](https://fx.wtf).
4
+ Short for _Function eXecution_ or _f(x)_.
4
5
 
5
6
  ```sh
6
7
  npm i -g fx
@@ -9,19 +10,19 @@ npm i -g fx
9
10
  Or use **npx**:
10
11
 
11
12
  ```sh
12
- cat file.json | npx fx .param
13
+ cat file.json | npx fx .field
13
14
  ```
14
15
 
15
16
  Or use **node**:
16
17
 
17
18
  ```sh
18
- cat file.json | node <(curl -s https://fx.wtf) .param
19
+ cat file.json | node <(curl -s https://fx.wtf) .field
19
20
  ```
20
21
 
21
22
  Or use **deno**:
22
23
 
23
24
  ```sh
24
- cat file.json | deno run https://fx.wtf .param
25
+ cat file.json | deno run https://fx.wtf .field
25
26
  ```
26
27
 
27
28
  ## Usage
@@ -44,11 +45,56 @@ echo '{"name": "world"}' | fx '.name' '`Hello, ${this}!`'
44
45
  Use other JS functions to process the data.
45
46
 
46
47
  ```sh
47
- echo `{"name": "world"}` | fx 'Object.keys'
48
+ echo '{"name": "world"}' | fx 'Object.keys'
48
49
  ```
49
50
 
50
51
  ## Advanced Usage
51
52
 
53
+ Fx can process a stream of json objects. Fx will apply arguments to each object.
54
+
55
+ ```sh
56
+ echo '{"name": "hello"}\n{"name": "world"}' | fx '.name'
57
+ ```
58
+
59
+ If you want to process a stream of json objects as a single array,
60
+ use the **--slurp** or **-s** flag.
61
+
62
+ ```sh
63
+ echo '{"name": "hello"}\n{"name": "world"}' | fx --slurp '.map(x => x.name)' '.join(", ")'
64
+ ```
65
+
66
+ If you want to process non-JSON data, use the **--raw** or **-r** flag.
67
+
68
+ ```sh
69
+ ls | fx -r '[this, this.includes(".md")]'
70
+ ```
71
+
72
+ You can use **--raw** and **--slurp** (or **-rs**) together to get a single array of strings.
73
+
74
+ ```sh
75
+ ls | fx -rs '.filter(x => x.includes(".md"))'
76
+ ```
77
+
78
+ Fx has a special symbol **skip** for skipping the printing of the result.
79
+
80
+ ```sh
81
+ ls | fx -r '.includes(".md") ? this : skip'
82
+ ```
83
+
84
+ Fx comes with a set of useful functions: **uniq**, **sort**, **groupBy**.
85
+
86
+ ```sh
87
+ cat file.json | fx 'uniq' 'sort' 'groupBy(x => x.name)'
88
+ ```
89
+
90
+ Fx works with promises.
91
+
92
+ ```sh
93
+ echo '"https://medv.io/*"' | fx 'fetch' '.text()'
94
+ ```
95
+
96
+ ### Syntactic Sugar
97
+
52
98
  Fx has a shortcut for the map function. Fox example, `this.map(x => x.commit.message)`
53
99
  can be rewritten without leading dot and without `x => x` parts.
54
100
 
@@ -67,12 +113,6 @@ Fx has a special syntax for the flatMap function. Fox example,
67
113
  curl https://api.github.com/repos/kubernetes/kubernetes/issues | fx '.[].labels[].name'
68
114
  ```
69
115
 
70
- Fx works with promises.
71
-
72
- ```sh
73
- echo '"https://medv.io/*"' | fx 'fetch' '.text()'
74
- ```
75
-
76
116
  ## License
77
117
 
78
118
  [MIT](../LICENSE)
package/index.js CHANGED
@@ -1,19 +1,41 @@
1
+ #!/usr/bin/env node
2
+ 'use strict'
3
+
1
4
  void async function main() {
2
5
  const process = await import('node:process')
3
- const args = process.argv.slice(2)
4
- let input = ''
5
- process.stdin.setEncoding('utf8')
6
- for await (const chunk of process.stdin)
7
- input += chunk
8
-
9
- let json
10
- try {
11
- json = JSON.parse(input)
12
- } catch (err) {
13
- process.stderr.write(`Invalid JSON: ${err.message}\n`)
14
- process.exit(1)
6
+ let flagHelp = false
7
+ let flagRaw = false
8
+ let flagSlurp = false
9
+ const args = []
10
+ for (const arg of process.argv.slice(2)) {
11
+ if (arg === '--help' || arg === '-h') flagHelp = true
12
+ else if (arg === '--raw' || arg === '-r') flagRaw = true
13
+ else if (arg === '--slurp' || arg === '-s') flagSlurp = true
14
+ else if (arg === '-rs' || arg === '-sr') flagRaw = flagSlurp = true
15
+ else args.push(arg)
15
16
  }
17
+ if (flagHelp || (args.length === 0 && process.stdin.isTTY))
18
+ return printUsage()
16
19
 
20
+ const stdin = await readStdinGenerator()
21
+ const input = flagRaw ? readLine(stdin) : parseJson(stdin)
22
+ if (flagSlurp) {
23
+ const array = []
24
+ for (const json of input) {
25
+ array.push(json)
26
+ }
27
+ await runTransforms(array, args)
28
+ } else {
29
+ for (const json of input) {
30
+ await runTransforms(json, args)
31
+ }
32
+ }
33
+ }()
34
+
35
+ const skip = Symbol('skip')
36
+
37
+ async function runTransforms(json, args) {
38
+ const process = await import('node:process')
17
39
  let i, code, output = json
18
40
  for ([i, code] of args.entries()) try {
19
41
  output = await transform(output, code)
@@ -23,24 +45,26 @@ void async function main() {
23
45
  }
24
46
 
25
47
  if (typeof output === 'undefined')
26
- process.stderr.write('undefined\n')
48
+ console.error('undefined')
27
49
  else if (typeof output === 'string')
28
50
  console.log(output)
51
+ else if (output === skip)
52
+ return
29
53
  else
30
- console.log(JSON.stringify(output, null, 2))
54
+ console.log(stringify(output, process.stdout.isTTY))
31
55
 
32
56
  function printErr(err) {
33
57
  let pre = args.slice(0, i).join(' ')
34
58
  let post = args.slice(i + 1).join(' ')
35
59
  if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
36
60
  if (post.length > 20) post = post.substring(0, 20) + '...'
37
- process.stderr.write(
61
+ console.error(
38
62
  `\n ${pre} ${code} ${post}\n` +
39
63
  ` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
40
- `\n${err.stack || err}\n`
64
+ `\n${err.stack || err}`
41
65
  )
42
66
  }
43
- }()
67
+ }
44
68
 
45
69
  async function transform(json, code) {
46
70
  if ('.' === code)
@@ -77,40 +101,447 @@ async function transform(json, code) {
77
101
  })`).call(json)
78
102
 
79
103
  return apply(fn, json)
104
+
105
+ function apply(fn, ...args) {
106
+ if (typeof fn === 'function') return fn(...args)
107
+ return fn
108
+ }
109
+
110
+ function fold(s) {
111
+ if (s.length === 1)
112
+ return 'x => x' + s[0]
113
+ let obj = s.shift()
114
+ obj = obj === '.' ? 'x' : 'x' + obj
115
+ return `x => Object.values(${obj}).flatMap(${fold(s)})`
116
+ }
117
+
118
+ function uniq(array) {
119
+ return [...new Set(array)]
120
+ }
121
+
122
+ function sort(array) {
123
+ return array.sort()
124
+ }
125
+
126
+ function groupBy(keyOrFunction) {
127
+ return array => {
128
+ const grouped = {}
129
+ for (const item of array) {
130
+ const key = typeof keyOrFunction === 'function'
131
+ ? keyOrFunction(item)
132
+ : item[keyOrFunction]
133
+ if (!grouped.hasOwnProperty(key))
134
+ grouped[key] = []
135
+ grouped[key].push(item)
136
+ }
137
+ return grouped
138
+ }
139
+ }
80
140
  }
81
141
 
82
- function apply(fn, ...args) {
83
- if (typeof fn === 'function') return fn(...args)
84
- return fn
142
+ async function readStdinGenerator() {
143
+ const fs = await import('node:fs')
144
+ const {Buffer} = await import('node:buffer')
145
+ const {StringDecoder} = await import('node:string_decoder')
146
+ const decoder = new StringDecoder('utf8')
147
+ return function* () {
148
+ while (true) {
149
+ const buffer = Buffer.alloc(4_096)
150
+ let bytesRead
151
+ try {
152
+ bytesRead = fs.readSync(0, buffer, 0, buffer.length, null)
153
+ } catch (e) {
154
+ if (e.code === 'EAGAIN' || e.code === 'EWOULDBLOCK') {
155
+ sleepSync(10)
156
+ continue
157
+ }
158
+ if (e.code === 'EOF') break
159
+ throw e
160
+ }
161
+ if (bytesRead === 0) break
162
+ for (const ch of decoder.write(buffer.subarray(0, bytesRead)))
163
+ yield ch
164
+ }
165
+ for (const ch of decoder.end())
166
+ yield ch
167
+ }()
85
168
  }
86
169
 
87
- function fold(s) {
88
- if (s.length === 1)
89
- return 'x => x' + s[0]
90
- let obj = s.shift()
91
- obj = obj === '.' ? 'x' : 'x' + obj
92
- return `x => Object.values(${obj}).flatMap(${fold(s)})`
170
+ function sleepSync(ms) {
171
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
93
172
  }
94
173
 
95
- function uniq(array) {
96
- return [...new Set(array)]
174
+ function* readLine(stdin) {
175
+ let buffer = ''
176
+ for (const ch of stdin) {
177
+ if (ch === '\n') {
178
+ yield buffer
179
+ buffer = ''
180
+ } else {
181
+ buffer += ch
182
+ }
183
+ }
184
+ return buffer
97
185
  }
98
186
 
99
- function sort(array) {
100
- return array.sort()
187
+ function* parseJson(stdin) {
188
+ let lineNumber = 1, buffer = '', lastChar, done = false
189
+
190
+ function next() {
191
+ ({value: lastChar, done} = stdin.next())
192
+ if (lastChar === '\n') lineNumber++
193
+ buffer += (lastChar || '')
194
+ if (buffer.length > 100) buffer = buffer.slice(-40)
195
+ }
196
+
197
+ next()
198
+ while (!done) {
199
+ const value = parseValue()
200
+ expectValue(value)
201
+ yield value
202
+ }
203
+
204
+ function parseValue() {
205
+ skipWhitespace()
206
+ const value =
207
+ parseString() ??
208
+ parseNumber() ??
209
+ parseObject() ??
210
+ parseArray() ??
211
+ parseKeyword('true', true) ??
212
+ parseKeyword('false', false) ??
213
+ parseKeyword('null', null)
214
+ skipWhitespace()
215
+ return value
216
+ }
217
+
218
+ function parseString() {
219
+ if (lastChar !== '"') return
220
+ let str = ''
221
+ let escaped = false
222
+ while (true) {
223
+ next()
224
+ if (escaped) {
225
+ if (lastChar === 'u') {
226
+ let unicode = ''
227
+ for (let i = 0; i < 4; i++) {
228
+ next()
229
+ if (!isHexDigit(lastChar)) {
230
+ throw new SyntaxError(errorSnippet(`Invalid Unicode escape sequence '\\u${unicode}${lastChar}'`))
231
+ }
232
+ unicode += lastChar
233
+ }
234
+ str += String.fromCharCode(parseInt(unicode, 16))
235
+ } else {
236
+ const escapedChar = {
237
+ '"': '"',
238
+ '\\': '\\',
239
+ '/': '/',
240
+ 'b': '\b',
241
+ 'f': '\f',
242
+ 'n': '\n',
243
+ 'r': '\r',
244
+ 't': '\t'
245
+ }[lastChar]
246
+ if (!escapedChar) {
247
+ throw new SyntaxError(errorSnippet())
248
+ }
249
+ str += escapedChar
250
+ }
251
+ escaped = false
252
+ } else if (lastChar === '\\') {
253
+ escaped = true
254
+ } else if (lastChar === '"') {
255
+ break
256
+ } else if (lastChar === undefined) {
257
+ throw new SyntaxError(errorSnippet())
258
+ } else {
259
+ str += lastChar
260
+ }
261
+ }
262
+ next()
263
+ return str
264
+ }
265
+
266
+ function parseNumber() {
267
+ if (!isDigit(lastChar) && lastChar !== '-') return
268
+ let numStr = ''
269
+ if (lastChar === '-') {
270
+ numStr += lastChar
271
+ next()
272
+ if (!isDigit(lastChar)) {
273
+ throw new SyntaxError(errorSnippet())
274
+ }
275
+ }
276
+ if (lastChar === '0') {
277
+ numStr += lastChar
278
+ next()
279
+ } else {
280
+ while (isDigit(lastChar)) {
281
+ numStr += lastChar
282
+ next()
283
+ }
284
+ }
285
+ if (lastChar === '.') {
286
+ numStr += lastChar
287
+ next()
288
+ if (!isDigit(lastChar)) {
289
+ throw new SyntaxError(errorSnippet())
290
+ }
291
+ while (isDigit(lastChar)) {
292
+ numStr += lastChar
293
+ next()
294
+ }
295
+ }
296
+ if (lastChar === 'e' || lastChar === 'E') {
297
+ numStr += lastChar
298
+ next()
299
+ if (lastChar === '+' || lastChar === '-') {
300
+ numStr += lastChar
301
+ next()
302
+ }
303
+ if (!isDigit(lastChar)) {
304
+ throw new SyntaxError(errorSnippet())
305
+ }
306
+ while (isDigit(lastChar)) {
307
+ numStr += lastChar
308
+ next()
309
+ }
310
+ }
311
+ return isInteger(numStr) ? toSafeNumber(numStr) : parseFloat(numStr)
312
+ }
313
+
314
+ function parseObject() {
315
+ if (lastChar !== '{') return
316
+ next()
317
+ skipWhitespace()
318
+ const obj = {}
319
+ if (lastChar === '}') {
320
+ next()
321
+ return obj
322
+ }
323
+ while (true) {
324
+ if (lastChar !== '"') {
325
+ throw new SyntaxError(errorSnippet())
326
+ }
327
+ const key = parseString()
328
+ skipWhitespace()
329
+ if (lastChar !== ':') {
330
+ throw new SyntaxError(errorSnippet())
331
+ }
332
+ next()
333
+ const value = parseValue()
334
+ expectValue(value)
335
+ obj[key] = value
336
+ skipWhitespace()
337
+ if (lastChar === '}') {
338
+ next()
339
+ return obj
340
+ } else if (lastChar === ',') {
341
+ next()
342
+ skipWhitespace()
343
+ if (lastChar === '}') {
344
+ next()
345
+ return obj
346
+ }
347
+ } else {
348
+ throw new SyntaxError(errorSnippet())
349
+ }
350
+ }
351
+ }
352
+
353
+ function parseArray() {
354
+ if (lastChar !== '[') return
355
+ next()
356
+ skipWhitespace()
357
+ const array = []
358
+ if (lastChar === ']') {
359
+ next()
360
+ return array
361
+ }
362
+ while (true) {
363
+ const value = parseValue()
364
+ expectValue(value)
365
+ array.push(value)
366
+ skipWhitespace()
367
+ if (lastChar === ']') {
368
+ next()
369
+ return array
370
+ } else if (lastChar === ',') {
371
+ next()
372
+ skipWhitespace()
373
+ if (lastChar === ']') {
374
+ next()
375
+ return array
376
+ }
377
+ } else {
378
+ throw new SyntaxError(errorSnippet())
379
+ }
380
+ }
381
+ }
382
+
383
+ function parseKeyword(name, value) {
384
+ if (lastChar !== name[0]) return
385
+ for (let i = 1; i < name.length; i++) {
386
+ next()
387
+ if (lastChar !== name[i]) {
388
+ throw new SyntaxError(errorSnippet())
389
+ }
390
+ }
391
+ next()
392
+ if (isWhitespace(lastChar) || lastChar === ',' || lastChar === '}' || lastChar === ']' || lastChar === undefined) {
393
+ return value
394
+ }
395
+ throw new SyntaxError(errorSnippet())
396
+ }
397
+
398
+ function skipWhitespace() {
399
+ while (isWhitespace(lastChar)) {
400
+ next()
401
+ }
402
+ skipComment()
403
+ }
404
+
405
+ function skipComment() {
406
+ if (lastChar === '/') {
407
+ next()
408
+ if (lastChar === '/') {
409
+ while (!done && lastChar !== '\n') {
410
+ next()
411
+ }
412
+ skipWhitespace()
413
+ } else if (lastChar === '*') {
414
+ while (!done) {
415
+ next()
416
+ if (lastChar === '*') {
417
+ next()
418
+ if (lastChar === '/') {
419
+ next()
420
+ break
421
+ }
422
+ }
423
+ }
424
+ skipWhitespace()
425
+ } else {
426
+ throw new SyntaxError(errorSnippet())
427
+ }
428
+ }
429
+ }
430
+
431
+ function isWhitespace(ch) {
432
+ return ch === ' ' || ch === '\n' || ch === '\t' || ch === '\r'
433
+ }
434
+
435
+ function isHexDigit(ch) {
436
+ return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')
437
+ }
438
+
439
+ function isDigit(ch) {
440
+ return ch >= '0' && ch <= '9'
441
+ }
442
+
443
+ function isInteger(value) {
444
+ return /^-?[0-9]+$/.test(value)
445
+ }
446
+
447
+ function toSafeNumber(str) {
448
+ const maxSafeInteger = Number.MAX_SAFE_INTEGER
449
+ const minSafeInteger = Number.MIN_SAFE_INTEGER
450
+ const num = BigInt(str)
451
+ return num >= minSafeInteger && num <= maxSafeInteger ? Number(num) : num
452
+ }
453
+
454
+ function expectValue(value) {
455
+ if (value === undefined) {
456
+ throw new SyntaxError(errorSnippet(`JSON value expected`))
457
+ }
458
+ }
459
+
460
+ function errorSnippet(message = `Unexpected character '${lastChar}'`) {
461
+ if (!lastChar) {
462
+ message = 'Unexpected end of input'
463
+ }
464
+ const lines = buffer.slice(-40).split('\n')
465
+ const lastLine = lines.pop()
466
+ const source =
467
+ lines.map(line => ` ${line}\n`).join('')
468
+ + ` ${lastLine}${readEOL()}\n`
469
+ const p = ` ${'.'.repeat(Math.max(0, lastLine.length - 1))}^\n`
470
+ return `${message} on line ${lineNumber}.\n\n${source}${p}`
471
+ }
472
+
473
+ function readEOL() {
474
+ let line = ''
475
+ for (const ch of stdin) {
476
+ if (!ch || ch === '\n' || line.length >= 60) break
477
+ line += ch
478
+ }
479
+ return line
480
+ }
101
481
  }
102
482
 
103
- function groupBy(keyOrFunction) {
104
- return array => {
105
- const grouped = {}
106
- for (const item of array) {
107
- const key = typeof keyOrFunction === 'function'
108
- ? keyOrFunction(item)
109
- : item[keyOrFunction]
110
- if (!grouped.hasOwnProperty(key))
111
- grouped[key] = []
112
- grouped[key].push(item)
483
+ function stringify(value, isPretty = false) {
484
+ const colors = {
485
+ key: isPretty ? '\x1b[1;34m' : '',
486
+ string: isPretty ? '\x1b[32m' : '',
487
+ number: isPretty ? '\x1b[36m' : '',
488
+ boolean: isPretty ? '\x1b[35m' : '',
489
+ null: isPretty ? '\x1b[2m' : '',
490
+ reset: isPretty ? '\x1b[0m' : '',
491
+ }
492
+
493
+ function getIndent(level) {
494
+ return ' '.repeat(2 * level)
495
+ }
496
+
497
+ function stringifyValue(value, level = 0) {
498
+ if (typeof value === 'string') {
499
+ return `${colors.string}"${value}"${colors.reset}`
500
+ } else if (typeof value === 'number') {
501
+ return `${colors.number}${value}${colors.reset}`
502
+ } else if (typeof value === 'bigint') {
503
+ return `${colors.number}${value}${colors.reset}`
504
+ } else if (typeof value === 'boolean') {
505
+ return `${colors.boolean}${value}${colors.reset}`
506
+ } else if (value === null || typeof value === 'undefined') {
507
+ return `${colors.null}null${colors.reset}`
508
+ } else if (Array.isArray(value)) {
509
+ if (value.length === 0) {
510
+ return `[]`
511
+ }
512
+ const items = value
513
+ .map((v) => `${getIndent(level + 1)}${stringifyValue(v, level + 1)}`)
514
+ .join(',\n')
515
+ return `[\n${items}\n${getIndent(level)}]`
516
+ } else if (typeof value === 'object') {
517
+ const keys = Object.keys(value)
518
+ if (keys.length === 0) {
519
+ return `{}`
520
+ }
521
+ const entries = keys
522
+ .map(
523
+ (key) =>
524
+ `${getIndent(level + 1)}${colors.key}"${key}"${colors.reset}: ${stringifyValue(
525
+ value[key],
526
+ level + 1
527
+ )}`
528
+ )
529
+ .join(',\n')
530
+ return `{\n${entries}\n${getIndent(level)}}`
113
531
  }
114
- return grouped
532
+ throw new Error(`Unsupported value type: ${typeof value}`)
115
533
  }
534
+
535
+ return stringifyValue(value)
536
+ }
537
+
538
+ function printUsage() {
539
+ const usage = `Usage
540
+ fx [flags] [code...]
541
+
542
+ Flags
543
+ -h, --help Display this help message
544
+ -r, --raw Treat input as a raw string
545
+ -s, --slurp Read all inputs into an array`
546
+ console.log(usage)
116
547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fx",
3
- "version": "25.0.0",
3
+ "version": "27.0.0",
4
4
  "bin": {
5
5
  "fx": "index.js"
6
6
  },