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