fx 26.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 +45 -13
  2. package/index.js +460 -49
  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
@@ -44,27 +45,46 @@ 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
 
52
- Fx has a shortcut for the map function. Fox example, `this.map(x => x.commit.message)`
53
- can be rewritten without leading dot and without `x => x` parts.
53
+ Fx can process a stream of json objects. Fx will apply arguments to each object.
54
54
 
55
55
  ```sh
56
- curl https://api.github.com/repos/antonmedv/fx/commits | fx 'map(.commit.message)'
56
+ echo '{"name": "hello"}\n{"name": "world"}' | fx '.name'
57
57
  ```
58
58
 
59
+ If you want to process a stream of json objects as a single array,
60
+ use the **--slurp** or **-s** flag.
61
+
59
62
  ```sh
60
- echo '[{"name": "world"}]' | fx 'map(`Hello, ${x.name}!`)'
63
+ echo '{"name": "hello"}\n{"name": "world"}' | fx --slurp '.map(x => x.name)' '.join(", ")'
61
64
  ```
62
65
 
63
- Fx has a special syntax for the flatMap function. Fox example,
64
- `.flatMap(x => x.labels.flatMap(x => x.name))` can be rewritten in the next way.
66
+ If you want to process non-JSON data, use the **--raw** or **-r** flag.
65
67
 
66
68
  ```sh
67
- curl https://api.github.com/repos/kubernetes/kubernetes/issues | fx '.[].labels[].name'
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)'
68
88
  ```
69
89
 
70
90
  Fx works with promises.
@@ -73,12 +93,24 @@ Fx works with promises.
73
93
  echo '"https://medv.io/*"' | fx 'fetch' '.text()'
74
94
  ```
75
95
 
76
- When using the **-r** or **--raw** flag, the input will be treated as a raw string
77
- instead of JSON. This can be useful when working with non-JSON input data, such as
78
- plain text or CSV data.
96
+ ### Syntactic Sugar
97
+
98
+ Fx has a shortcut for the map function. Fox example, `this.map(x => x.commit.message)`
99
+ can be rewritten without leading dot and without `x => x` parts.
100
+
101
+ ```sh
102
+ curl https://api.github.com/repos/antonmedv/fx/commits | fx 'map(.commit.message)'
103
+ ```
79
104
 
80
105
  ```sh
81
- ls | fx -r '.trim().split("\n")'
106
+ echo '[{"name": "world"}]' | fx 'map(`Hello, ${x.name}!`)'
107
+ ```
108
+
109
+ Fx has a special syntax for the flatMap function. Fox example,
110
+ `.flatMap(x => x.labels.flatMap(x => x.name))` can be rewritten in the next way.
111
+
112
+ ```sh
113
+ curl https://api.github.com/repos/kubernetes/kubernetes/issues | fx '.[].labels[].name'
82
114
  ```
83
115
 
84
116
  ## License
package/index.js CHANGED
@@ -1,29 +1,41 @@
1
1
  #!/usr/bin/env node
2
+ 'use strict'
3
+
2
4
  void async function main() {
3
5
  const process = await import('node:process')
4
- const args = process.argv.slice(2)
5
- if ((args.length === 0 && process.stdin.isTTY)
6
- || (args.length === 1 && ['-h', '--help'].includes(args[0]))) {
7
- printUsage()
8
- return
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)
9
16
  }
17
+ if (flagHelp || (args.length === 0 && process.stdin.isTTY))
18
+ return printUsage()
10
19
 
11
- let input = ''
12
- process.stdin.setEncoding('utf8')
13
- for await (const chunk of process.stdin)
14
- input += chunk
15
-
16
- let json
17
- if (['-r', '--raw'].includes(args[0])) {
18
- args.shift()
19
- json = input
20
- } else try {
21
- json = JSON.parse(input)
22
- } catch (err) {
23
- process.stderr.write(`Invalid JSON: ${err.message}\n`)
24
- process.exit(1)
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
+ }
25
32
  }
33
+ }()
34
+
35
+ const skip = Symbol('skip')
26
36
 
37
+ async function runTransforms(json, args) {
38
+ const process = await import('node:process')
27
39
  let i, code, output = json
28
40
  for ([i, code] of args.entries()) try {
29
41
  output = await transform(output, code)
@@ -33,24 +45,26 @@ void async function main() {
33
45
  }
34
46
 
35
47
  if (typeof output === 'undefined')
36
- process.stderr.write('undefined\n')
48
+ console.error('undefined')
37
49
  else if (typeof output === 'string')
38
50
  console.log(output)
51
+ else if (output === skip)
52
+ return
39
53
  else
40
- console.log(JSON.stringify(output, null, 2))
54
+ console.log(stringify(output, process.stdout.isTTY))
41
55
 
42
56
  function printErr(err) {
43
57
  let pre = args.slice(0, i).join(' ')
44
58
  let post = args.slice(i + 1).join(' ')
45
59
  if (pre.length > 20) pre = '...' + pre.substring(pre.length - 20)
46
60
  if (post.length > 20) post = post.substring(0, 20) + '...'
47
- process.stderr.write(
61
+ console.error(
48
62
  `\n ${pre} ${code} ${post}\n` +
49
63
  ` ${' '.repeat(pre.length + 1)}${'^'.repeat(code.length)}\n` +
50
- `\n${err.stack || err}\n`
64
+ `\n${err.stack || err}`
51
65
  )
52
66
  }
53
- }()
67
+ }
54
68
 
55
69
  async function transform(json, code) {
56
70
  if ('.' === code)
@@ -87,42 +101,438 @@ async function transform(json, code) {
87
101
  })`).call(json)
88
102
 
89
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
+ }
90
140
  }
91
141
 
92
- function apply(fn, ...args) {
93
- if (typeof fn === 'function') return fn(...args)
94
- 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
+ }()
95
168
  }
96
169
 
97
- function fold(s) {
98
- if (s.length === 1)
99
- return 'x => x' + s[0]
100
- let obj = s.shift()
101
- obj = obj === '.' ? 'x' : 'x' + obj
102
- return `x => Object.values(${obj}).flatMap(${fold(s)})`
170
+ function sleepSync(ms) {
171
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms)
103
172
  }
104
173
 
105
- function uniq(array) {
106
- 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
107
185
  }
108
186
 
109
- function sort(array) {
110
- 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
+ }
111
481
  }
112
482
 
113
- function groupBy(keyOrFunction) {
114
- return array => {
115
- const grouped = {}
116
- for (const item of array) {
117
- const key = typeof keyOrFunction === 'function'
118
- ? keyOrFunction(item)
119
- : item[keyOrFunction]
120
- if (!grouped.hasOwnProperty(key))
121
- grouped[key] = []
122
- 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)}}`
123
531
  }
124
- return grouped
532
+ throw new Error(`Unsupported value type: ${typeof value}`)
125
533
  }
534
+
535
+ return stringifyValue(value)
126
536
  }
127
537
 
128
538
  function printUsage() {
@@ -131,6 +541,7 @@ function printUsage() {
131
541
 
132
542
  Flags
133
543
  -h, --help Display this help message
134
- -r, --raw Treat input as raw string`
544
+ -r, --raw Treat input as a raw string
545
+ -s, --slurp Read all inputs into an array`
135
546
  console.log(usage)
136
547
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fx",
3
- "version": "26.0.0",
3
+ "version": "27.0.0",
4
4
  "bin": {
5
5
  "fx": "index.js"
6
6
  },