Delete js version

sleep-stdin-bug
Anton Medvedev 2 years ago
parent 8a9e1f39d5
commit 65a1608462

3
.gitignore vendored

@ -1,3 +0,0 @@
/node_modules/
/dist/
package-lock.json

@ -1,4 +0,0 @@
language: node_js
node_js:
- "node"
- "11"

@ -1,331 +0,0 @@
# Documentation
* [Getting started](#getting-started)
* [Usage](#usage)
+ [Anonymous function](#anonymous-function)
+ [Binding](#binding)
+ [Dot](#dot)
+ [Map](#map)
+ [Chaining](#chaining)
+ [Updating](#updating)
+ [Edit-in-place](#edit-in-place)
+ [Using packages](#using-packages)
* [Using .fxrc](#using-fxrc)
+ [Query language](#query-language)
* [Formatting](#formatting)
* [Other examples](#other-examples)
* [Streaming mode](#streaming-mode)
+ [Filtering](#filtering)
* [Interactive mode](#interactive-mode)
+ [Searching](#searching)
+ [Selecting text](#selecting-text)
* [Memory Usage](#memory-usage)
## Getting started
`fx` can work in two modes: cli and interactive. To start interactive mode pipe any JSON into `fx`:
```bash
$ curl ... | fx
```
Or you can pass a filename as the first parameter:
```bash
$ fx my.json
```
If any argument was passed, `fx` will apply it and prints to stdout.
## Usage
### Anonymous function
Use an anonymous function as reducer which gets JSON and processes it:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo[0].bar'
value
```
### Binding
If you don't pass anonymous function `param => ...`, code will be automatically transformed into anonymous function.
And you can get access to JSON by `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'this.foo[0].bar'
value
```
### Dot
It is possible to omit `this` keyword:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx .foo[0].bar
value
```
If a single dot is passed, the input JSON will be formatted but otherwise unaltered:
```bash
$ echo '{"foo": "bar"}' | fx .
{
"foo": "bar"
}
```
### Map
One of the frequent operations is mapping some function on an array. For example, to extract some values.
```
[
{
"author": {
"name": "antonmedv"
}
},
{...},
...
]
```
And we want to collect names of each object in the array. We can do this by mapping anonymous function:
```bash
$ cat ... | fx '.map(x => x.author.name)'
```
Or we can do the same by using jq-like syntax:
```bash
$ cat ... | fx .[].author.name
[
"antonmedv",
...
]
```
> Note what `[]` can be applied to map object values.
> ```bash
> $ echo '{"foo": 1, "bar": 2}' | fx .[]
> [1, 2]
> ```
### Chaining
You can pass any number of anonymous functions for reducing JSON:
```bash
$ echo '{"foo": [{"bar": "value"}]}' | fx 'x => x.foo' 'this[0]' 'this.bar'
value
```
### Updating
You can update existing JSON using the spread operator:
```bash
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
### Edit-in-place
`fx` provides a function `save` which will save everything in place and return saved object.
This function can be only used with filename as first argument to `fx` command.
Usage:
```bash
fx data.json '{...this, count: this.count+1}' save .count
```
### Using packages
Use any npm package by installing it globally:
```bash
$ npm install -g lodash
$ cat package.json | fx 'require("lodash").keys(this.dependencies)'
```
## Using .fxrc
Create _.fxrc_ file in `$HOME` directory, and require any packages or define global functions.
For example, access all lodash methods without `_` prefix. Put in your `.fxrc` file:
```js
Object.assign(global, require('lodash/fp'))
```
And now you will be able to call all lodash methods. For example, see who's been committing to react recently:
```bash
curl 'https://api.github.com/repos/facebook/react/commits?per_page=100' \
| fx 'groupBy("commit.author.name")' 'mapValues(size)' toPairs 'sortBy(1)' reverse 'take(10)' fromPairs
```
> To be able require global modules make sure you have correct `NODE_PATH` env variable.
> ```bash
> export NODE_PATH=`npm root -g`
> ```
### Query language
If you want to use query language, for example [jsonata](http://jsonata.org/) you can use helper function like this:
```js
global.jsonata = expr => require('jsonata')(expr).evaluate
```
And use it like this:
```bash
curl ... | fx 'jsonata("$sum(Order.Product.(Price * Quantity))")'
```
Instead you can create next alias in _.bashrc_ file:
```bash
alias jsonata='FX_APPLY=jsonata fx'
```
And now all code arguments to `jsonata` will be passed through `jsonata` helper. And now you can use it like this:
```bash
curl ... | jsonata '$sum(Order.Product.(Price * Quantity))'
```
## Formatting
If you need output other than JSON (for example arguments for xargs), do not return anything from the reducer.
`undefined` value is printed into stderr by default.
```bash
echo '[]' | fx 'void 0'
undefined
```
```bash
echo '[1,2,3]' | fx 'this.forEach(x => console.log(+x))' 2>/dev/null | xargs echo
1 2 3
```
## Other examples
Convert object to array:
```bash
$ cat package.json | fx 'Object.keys(this.dependencies)'
```
Or by two functions:
```bash
$ cat package.json | fx .dependencies Object.keys
```
By the way, fx has shortcut for `Object.keys`. Previous example can be rewritten as:
```bash
$ cat package.json | fx .dependencies ?
```
## Streaming mode
`fx` supports line-delimited JSON and concatenated JSON streaming.
```bash
$ kubectl logs ... | fx .message
```
> Note what is object lacks `message` field, _undefined_ will be printed to stderr.
> This is useful to see if you are skipping some objects. But if you want to hide them,
> redirect stderr to `/dev/null`.
### Filtering
Sometimes it is necessary to omit some messages in JSON stream, or select only specified log messages.
For this purpose, `fx` has special helpers `select`/`filter`, pass function into it to select/filter JSON messages.
```bash
$ kubectl logs ... | fx 'select(x => x.status == 500)' .message
```
```bash
$ kubectl logs ... | fx 'filter(x => x.status < 499)' .message
```
> If `filter`/`select` overridden in _.fxrc_ you still able to access them with prefix:
> `std.select(cb)` or `std.filter(cd)`
## Interactive mode
Click on fields to expand or collapse JSON tree, use mouse wheel to scroll view.
Next commands available in interactive mode:
| Key | Command |
|-------------------------------|----------------------------------------------|
| `q` or `Esc` or `Ctrl`+`c` | Exit |
| `up` or `k` | Move cursor up |
| `down` or `j` | Move cursor down |
| `left` or `h` | Collapse |
| `right` or `l` | Expand |
| `Shift`+`right` or `L` | Expand all under cursor |
| `Shift`+`left` or `K` | Collapse all under cursor |
| `e` | Expand all |
| `E` | Collapse all |
| `g` | Scroll to top |
| `G` | Scroll to bottom |
| `.` | Edit filter |
| `/` | Search |
| `n` | Find next |
| `p` | Exit and print JSON to stdout |
| `P` | Exit and print fully expanded JSON to stdout |
These commands are available when editing the filter:
| Key | Command |
|-------------------------------|-------------------------|
| `Enter` | Apply filter |
| `Ctrl`+`u` | Clear filter |
| `Ctrl`+`w` | Delete last part |
| `up`/`down` | Select autocomplete |
### Searching
Press `/` and type regexp pattern to search in current JSON. Search work with currently applied filter.
Examples of pattern and corresponding regexp:
| Pattern | RegExp |
|------------|-------------|
| `/apple` | `/apple/ig` |
| `/apple/` | `/apple/` |
| `/apple/u` | `/apple/u` |
| `/\w+` | `/\w+/ig` |
### Selecting text
You may found what you can't just select text in fx. This is due the fact that all mouse events redirected to stdin. To be able select again you need instruct your terminal not to do it. This can be done by holding special keys while selecting:
| Key | Terminal |
|------------------|---------------|
| `Option`+`Mouse` | iTerm2, Hyper |
| `Fn`+`Mouse` | Terminal.app |
| `Shift`+`Mouse` | Linux |
> Note what you can press `p`/`P` to print everything to stdout and select if there.
## Memory Usage
You may find that sometimes, on really big JSON files, fx prints an error message like this:
```
FATAL ERROR: JavaScript heap out of memory
```
V8 limits memory usage to around 2 GB by default. You can increase the limit by putting this line in your _.profile_:
```bash
export NODE_OPTIONS='--max-old-space-size=8192'
```

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2019 Anton Medvedev
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.

@ -1,106 +0,0 @@
<p align="center"><a href="http://fx.wtf"><img src="https://medv.io/assets/fx-logo.png" height="100" alt="fx logo"></a></p>
<p align="center"><img src="https://medv.io/assets/fx.gif" width="562" alt="fx example"></p>
_* Function eXecution_
[![Build Status](https://travis-ci.org/antonmedv/fx.svg?branch=master)](https://travis-ci.org/antonmedv/fx)
[![Npm Version](https://img.shields.io/npm/v/fx.svg)](https://www.npmjs.com/package/fx)
[![Brew Version](https://img.shields.io/homebrew/v/fx.svg)](https://formulae.brew.sh/formula/fx)
Command-line JSON processing tool
## Features
* Easy to use
* Standalone binary
* Interactive mode 🎉
* Streaming support 🌊
## Install
```bash
npm install -g fx
```
Or via Homebrew
```bash
brew install fx
```
Or download standalone binary from [releases](https://github.com/antonmedv/fx/releases)
## Usage
Start [interactive mode](https://github.com/antonmedv/fx/blob/master/DOCS.md#interactive-mode) without passing any arguments.
```bash
$ curl ... | fx
```
Or by passing filename as first argument.
```bash
$ fx data.json
```
Pass a few JSON files.
```bash
cat foo.json bar.json baz.json | fx .message
```
Use full power of JavaScript.
```bash
$ curl ... | fx '.filter(x => x.startsWith("a"))'
```
Access all lodash (or ramda, etc) methods by using [.fxrc](https://github.com/antonmedv/fx/blob/master/DOCS.md#using-fxrc) file.
```bash
$ curl ... | fx '_.groupBy("commit.committer.name")' '_.mapValues(_.size)'
```
Update JSON using spread operator.
```bash
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{
"count": 1
}
```
Extract values from maps.
```bash
$ fx commits.json | fx .[].author.name
```
Print formatted JSON to stdout.
```bash
$ curl ... | fx .
```
Pipe JSON logs stream into fx.
```bash
$ kubectl logs ... -f | fx .message
```
And try this:
```bash
$ fx --life
```
## Documentation
See full [documentation](https://github.com/antonmedv/fx/blob/master/DOCS.md).
## Links
* [Discover how to use fx effectively](http://bit.ly/discover-how-to-use-fx-effectively)
* [Video tutorial](http://bit.ly/youtube-fx-tutorial)
## Related
* [gofx](https://github.com/antonmedv/gofx) fx-like JSON tool (*go*)
* [eat](https://github.com/antonmedv/eat) converts anything into JSON
* [ymlx](https://github.com/matthewadams/ymlx) fx-like YAML cli processor
* [fx-completion](https://github.com/antonmedv/fx-completion) bash completion for fx
* [fx-theme-monokai](https://github.com/antonmedv/fx-theme-monokai) monokai theme
* [fx-theme-night](https://github.com/antonmedv/fx-theme-night) night theme
## License
[MIT](https://github.com/antonmedv/fx/blob/master/LICENSE)

@ -1,79 +0,0 @@
'use strict'
// http://bit.ly/fx--life
const
run = f => setInterval(f, 16),
p = (s) => process.stdout.write(s),
esc = (...x) => x.map(i => p('\u001B[' + i)),
[upper, lower, full] = ['\u2580', '\u2584', '\u2588'],
{columns, rows} = process.stdout,
[w, h] = [columns, rows * 2],
a = Math.floor(w / 2) - 6, b = Math.floor(h / 2) - 7
let $ = Array(w * h).fill(false)
if (Date.now() % 3 === 0) {
$[1 + 5 * w] = $[1 + 6 * w] = $[2 + 5 * w] = $[2 + 6 * w] = $[12 + 5 * w] = $[12 + 6 * w] = $[12 + 7 * w] = $[13 + 4 * w] = $[13 + 8 * w] = $[14 + 3 * w] = $[14 + 9 * w] = $[15 + 4 * w] = $[15 + 8 * w] = $[16 + 5 * w] = $[16 + 6 * w] = $[16 + 7 * w] = $[17 + 5 * w] = $[17 + 6 * w] = $[17 + 7 * w] = $[22 + 3 * w] = $[22 + 4 * w] = $[22 + 5 * w] = $[23 + 2 * w] = $[23 + 3 * w] = $[23 + 5 * w] = $[23 + 6 * w] = $[24 + 2 * w] = $[24 + 3 * w] = $[24 + 5 * w] = $[24 + 6 * w] = $[25 + 2 * w] = $[25 + 3 * w] = $[25 + 4 * w] = $[25 + 5 * w] = $[25 + 6 * w] = $[26 + 1 * w] = $[26 + 2 * w] = $[26 + 6 * w] = $[26 + 7 * w] = $[35 + 3 * w] = $[35 + 4 * w] = $[36 + 3 * w] = $[36 + 4 * w] = true
} else if (Date.now() % 3 === 1) {
for (let i = 0; i < $.length; i-=-1)
if (Math.random() < 0.16) $[i] = true
} else {
$[a + 1 + (2 + b) * w] = $[a + 2 + (1 + b) * w] = $[a + 2 + (3 + b) * w] = $[a + 3 + (2 + b) * w] = $[a + 5 + (15 + b) * w] = $[a + 6 + (13 + b) * w] = $[a + 6 + (15 + b) * w] = $[a + 7 + (12 + b) * w] = $[a + 7 + (13 + b) * w] = $[a + 7 + (15 + b) * w] = $[a + 9 + (11 + b) * w] = $[a + 9 + (12 + b) * w] = $[a + 9 + (13 + b) * w] = true
}
function at(i, j) {
if (i < 0) i = h - 1
if (i >= h) i = 0
if (j < 0) j = w - 1
if (j >= w) j = 0
return $[i * w + j]
}
function neighbors(i, j) {
let c = 0
at(i - 1, j - 1) && c++
at(i - 1, j) && c++
at(i - 1, j + 1) && c++
at(i, j - 1) && c++
at(i, j + 1) && c++
at(i + 1, j - 1) && c++
at(i + 1, j) && c++
at(i + 1, j + 1) && c++
return c
}
run(() => {
esc('H')
let gen = Array(w * h).fill(false)
for (let i = 0; i < h; i-=-1) {
for (let j = 0; j < w; j-=-1) {
const n = neighbors(i, j)
const z = i * w + j
if ($[z]) {
if (n < 2) gen[z] = false
if (n === 2 || n === 3) gen[z] = true
if (n > 3) gen[z] = false
} else {
if (n === 3) gen[z] = true
}
}
}
$ = gen
for (let i = 0; i < rows; i-=-1) {
for (let j = 0; j < columns; j-=-1) {
if ($[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(full)
else if ($[i * 2 * w + j] && !$[(i * 2 + 1) * w + j]) p(upper)
else if (!$[i * 2 * w + j] && $[(i * 2 + 1) * w + j]) p(lower)
else p(' ')
}
if (i !== rows - 1) p('\n')
}
})
esc('2J', '?25l')
process.on('SIGINT', () => {
esc('?25h')
process.exit(2)
})

@ -1,26 +0,0 @@
'use strict'
const chalk = require('chalk')
const noop = x => x
const list = {
fg: 'black',
bg: 'cyan',
selected: {
bg: 'magenta'
}
}
module.exports = {
space: global.FX_STYLE_SPACE || 2,
null: global.FX_STYLE_NULL || chalk.grey.bold,
number: global.FX_STYLE_NUMBER || chalk.cyan.bold,
boolean: global.FX_STYLE_BOOLEAN || chalk.yellow.bold,
string: global.FX_STYLE_STRING || chalk.green.bold,
key: global.FX_STYLE_KEY || chalk.blue.bold,
bracket: global.FX_STYLE_BRACKET || noop,
comma: global.FX_STYLE_COMMA || noop,
colon: global.FX_STYLE_COLON || noop,
list: global.FX_STYLE_LIST || list,
highlight: global.FX_STYLE_HIGHLIGHT || chalk.black.bgYellow,
highlightCurrent: global.FX_STYLE_HIGHLIGHT_CURRENT || chalk.inverse,
statusBar: global.FX_STYLE_STATUS_BAR || chalk.inverse,
}

@ -1,35 +0,0 @@
'use strict'
function* find(v, regex, path = []) {
if (typeof v === 'undefined' || v === null) {
return
}
if (Array.isArray(v)) {
let i = 0
for (let value of v) {
yield* find(value, regex, path.concat(['[' + i++ + ']']))
}
return
}
if (typeof v === 'object' && v.constructor === Object) {
const entries = Object.entries(v)
for (let [key, value] of entries) {
const nextPath = path.concat(['.' + key])
if (regex.test(key)) {
yield nextPath
}
yield* find(value, regex, nextPath)
}
return
}
if (regex.test(v)) {
yield path
}
}
module.exports = find

740
fx.js

@ -1,740 +0,0 @@
'use strict'
const fs = require('fs')
const tty = require('tty')
const blessed = require('@medv/blessed')
const stringWidth = require('string-width')
const reduce = require('./reduce')
const print = require('./print')
const find = require('./find')
const config = require('./config')
module.exports = function start(filename, source, prev = {}) {
// Current rendered object on a screen.
let json = prev.json || source
// Contains map from row number to expand path.
// Example: {0: '', 1: '.foo', 2: '.foo[0]'}
let index = new Map()
// Contains expanded paths. Example: ['', '.foo']
// Empty string represents root path.
const expanded = prev.expanded || new Set()
expanded.add('')
// Current filter code.
let currentCode = null
// Current search regexp and generator.
let highlight = null
let findGen = null
let currentPath = null
let ttyReadStream, ttyWriteStream
// Reopen tty
if (process.platform === 'win32') {
const cfs = process.binding('fs')
ttyReadStream = tty.ReadStream(cfs.open('conin$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
ttyWriteStream = tty.WriteStream(cfs.open('conout$', fs.constants.O_RDWR | fs.constants.O_EXCL, 0o666))
} else {
const ttyFd = fs.openSync('/dev/tty', 'r+')
ttyReadStream = tty.ReadStream(ttyFd)
ttyWriteStream = tty.WriteStream(ttyFd)
}
const program = blessed.program({
input: ttyReadStream,
output: ttyWriteStream,
})
const screen = blessed.screen({
program: program,
smartCSR: true,
fullUnicode: true,
})
const box = blessed.box({
parent: screen,
tags: false,
left: 0,
top: 0,
width: '100%',
height: '100%',
mouse: true,
keys: true,
vi: true,
ignoreArrows: true,
alwaysScroll: true,
scrollable: true,
})
const input = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const search = blessed.textbox({
parent: screen,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const statusBar = blessed.box({
parent: screen,
tags: false,
bottom: 0,
left: 0,
height: 1,
width: '100%',
})
const autocomplete = blessed.list({
parent: screen,
width: 6,
height: 7,
left: 1,
bottom: 1,
style: config.list,
})
screen.title = filename
box.focus()
input.hide()
search.hide()
statusBar.hide()
autocomplete.hide()
process.stdout.on('resize', () => {
// Blessed has a bug with resizing the terminal. I tried my best to fix it but was not succeeded.
// For now exit and print seem like a reasonable alternative, as it not usable after resize.
// If anyone can fix this bug it will be cool.
printJson({expanded})
})
screen.key(['escape', 'q', 'C-c'], function () {
exit()
})
input.on('submit', function () {
if (autocomplete.hidden) {
const code = input.getValue()
if (/^\//.test(code)) {
// Forgive a mistake to the user. This looks like user wanted to search something.
apply('')
applyPattern(code)
} else {
apply(code)
}
} else {
// Autocomplete selected
let code = input.getValue()
let replace = autocomplete.getSelected()
if (/^[a-z]\w*$/i.test(replace)) {
replace = '.' + replace
} else {
replace = `["${replace}"]`
}
code = code.replace(/\.\w*$/, replace)
input.setValue(code)
autocomplete.hide()
update(code)
// Keep editing code
input.readInput()
}
})
input.on('cancel', function () {
if (autocomplete.hidden) {
const code = input.getValue()
apply(code)
} else {
// Autocomplete not selected
autocomplete.hide()
screen.render()
// Keep editing code
input.readInput()
}
})
input.on('update', function (code) {
if (currentCode === code) {
return
}
currentCode = code
if (index.size < 10000) { // Don't live update in we have a big JSON file.
update(code)
}
complete(code)
})
input.key('up', function () {
if (!autocomplete.hidden) {
autocomplete.up()
screen.render()
}
})
input.key('down', function () {
if (!autocomplete.hidden) {
autocomplete.down()
screen.render()
}
})
input.key('C-u', function () {
input.setValue('')
update('')
render()
})
input.key('C-w', function () {
let code = input.getValue()
code = code.replace(/[\.\[][^\.\[]*$/, '')
input.setValue(code)
update(code)
render()
})
search.on('submit', function (pattern) {
applyPattern(pattern)
})
search.on('cancel', function () {
highlight = null
currentPath = null
search.hide()
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
})
box.key('.', function () {
hideStatusBar()
box.height = '100%-1'
input.show()
if (input.getValue() === '') {
input.setValue('.')
complete('.')
}
input.readInput()
screen.render()
})
box.key('/', function () {
hideStatusBar()
box.height = '100%-1'
search.show()
search.setValue('/')
search.readInput()
screen.render()
})
box.key('e', function () {
hideStatusBar()
expanded.clear()
for (let path of dfs(json)) {
if (expanded.size < 1000) {
expanded.add(path)
} else {
break
}
}
render()
})
box.key('S-e', function () {
hideStatusBar()
expanded.clear()
expanded.add('')
render()
// Make sure cursor stay on JSON object.
const [n] = getLine(program.y)
if (typeof n === 'undefined' || !index.has(n)) {
// No line under cursor
let rest = [...index.keys()]
if (rest.length > 0) {
const next = Math.max(...rest)
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
}
})
box.key('n', function () {
hideStatusBar()
findNext()
})
box.key(['up', 'k'], function () {
hideStatusBar()
program.showCursor()
const [n] = getLine(program.y)
let next
for (let [i,] of index) {
if (i < n && (typeof next === 'undefined' || i > next)) {
next = i
}
}
if (typeof next !== 'undefined') {
let y = box.getScreenNumber(next) - box.childBase
if (y <= 0) {
box.scroll(-1)
screen.render()
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['down', 'j'], function () {
hideStatusBar()
program.showCursor()
const [n] = getLine(program.y)
let next
for (let [i,] of index) {
if (i > n && (typeof next === 'undefined' || i < next)) {
next = i
}
}
if (typeof next !== 'undefined') {
let y = box.getScreenNumber(next) - box.childBase
if (y >= box.height) {
box.scroll(1)
screen.render()
y = box.height - 1
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
})
box.key(['right', 'l'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (!expanded.has(path)) {
expanded.add(path)
render()
}
})
// Expand everything under cursor.
box.key(['S-right', 'S-l'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
const subJson = reduce(json, 'this' + path)
for (let p of dfs(subJson, path)) {
if (expanded.size < 1000) {
expanded.add(p)
} else {
break
}
}
render()
})
box.key(['S-left', 'S-k'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
const path = index.get(n)
if (!path) {
// collapse on top level (should render like after run)
expanded.clear()
expanded.add('')
} else {
const subJson = reduce(json, 'this' + path)
for (let p of dfs(subJson, path)) {
if (expanded.size < 1000) {
expanded.delete(p)
} else {
break
}
}
}
render()
})
box.key(['left', 'h'], function () {
hideStatusBar()
const [n, line] = getLine(program.y)
program.showCursor()
program.cursorPos(program.y, line.search(/\S/))
// Find path at current cursor position.
const path = index.get(n)
if (expanded.has(path)) {
// Collapse current path.
expanded.delete(path)
render()
} else {
// If there is no expanded paths on current line,
// collapse parent path of current location.
if (typeof path === 'string') {
// Trip last part (".foo", "[0]") to get parent path.
const parentPath = path.replace(/(\.[^\[\].]+|\[\d+\])$/, '')
if (expanded.has(parentPath)) {
expanded.delete(parentPath)
render()
// Find line number of parent path, and if we able to find it,
// move cursor to this position of just collapsed parent path.
for (let y = program.y; y >= 0; --y) {
const [n, line] = getLine(y)
if (index.get(n) === parentPath) {
program.cursorPos(y, line.search(/\S/))
break
}
}
}
}
}
})
box.on('click', function (mouse) {
hideStatusBar()
const [n, line] = getLine(mouse.y)
if (mouse.x >= stringWidth(line)) {
return
}
program.hideCursor()
program.cursorPos(mouse.y, line.search(/\S/))
autocomplete.hide()
const path = index.get(n)
if (expanded.has(path)) {
expanded.delete(path)
} else {
expanded.add(path)
}
render()
})
box.on('scroll', function () {
hideStatusBar()
})
box.key('p', function () {
printJson({expanded})
})
box.key('S-p', function () {
printJson()
})
function printJson(options = {}) {
screen.destroy()
program.disableMouse()
program.destroy()
setTimeout(() => {
const [text] = print(json, options)
console.log(text)
process.exit(0)
}, 10)
}
function getLine(y) {
const dy = box.childBase + y
const n = box.getNumber(dy)
const line = box.getScreenLine(dy)
if (typeof line === 'undefined') {
return [n, '']
}
return [n, line]
}
function apply(code) {
if (code && code.length !== 0) {
try {
json = reduce(source, code)
} catch (e) {
// pass
}
} else {
box.height = '100%'
input.hide()
json = source
}
box.focus()
program.cursorPos(0, 0)
render()
}
function complete(inputCode) {
const match = inputCode.match(/\.(\w*)$/)
const code = /^\.\w*$/.test(inputCode) ? '.' : inputCode.replace(/\.\w*$/, '')
let json
try {
json = reduce(source, code)
} catch (e) {
}
if (match) {
if (typeof json === 'object' && json.constructor === Object) {
const keys = Object.keys(json)
.filter(key => key.startsWith(match[1]))
.slice(0, 1000) // With lots of items, list takes forever to render.
// Hide if there is nothing to show or
// don't show if there is complete match.
if (keys.length === 0 || (keys.length === 1 && keys[0] === match[1])) {
autocomplete.hide()
return
}
autocomplete.width = Math.max(...keys.map(key => key.length)) + 1
autocomplete.height = Math.min(7, keys.length)
autocomplete.left = Math.min(
screen.width - autocomplete.width,
code.length === 1 ? 1 : code.length + 1
)
let selectFirst = autocomplete.items.length !== keys.length
autocomplete.setItems(keys)
if (selectFirst) {
autocomplete.select(autocomplete.items.length - 1)
}
if (autocomplete.hidden) {
autocomplete.show()
}
} else {
autocomplete.clearItems()
autocomplete.hide()
}
}
}
function update(code) {
if (code && code.length !== 0) {
try {
const pretender = reduce(source, code)
if (
typeof pretender !== 'undefined'
&& typeof pretender !== 'function'
&& !(pretender instanceof RegExp)
) {
json = pretender
}
} catch (e) {
// pass
}
}
if (code === '') {
json = source
}
if (highlight) {
findGen = find(json, highlight)
}
render()
}
function applyPattern(pattern) {
let regex
let m = pattern.match(/^\/(.*)\/([gimuy]*)$/)
if (m) {
try {
regex = new RegExp(m[1], m[2])
} catch (e) {
// Wrong regexp.
}
} else {
m = pattern.match(/^\/(.*)$/)
if (m) {
try {
regex = new RegExp(m[1], 'gi')
} catch (e) {
// Wrong regexp.
}
}
}
highlight = regex
search.hide()
if (highlight) {
findGen = find(json, highlight)
findNext()
} else {
findGen = null
currentPath = null
}
search.setValue('')
box.height = '100%'
box.focus()
program.cursorPos(0, 0)
render()
}
function findNext() {
if (!findGen) {
return
}
const {value: path, done} = findGen.next()
if (done) {
showStatusBar('Pattern not found')
} else {
currentPath = ''
for (let p of path) {
expanded.add(currentPath += p)
}
render()
for (let [k, v] of index) {
if (v === currentPath) {
let y = box.getScreenNumber(k)
// Scroll one line up for better view and make sure it's not negative.
if (--y < 0) {
y = 0
}
box.scrollTo(y)
screen.render()
}
}
// Set cursor to current path.
// We need timeout here to give our terminal some time.
// Without timeout first cursorPos call does not working,
// it looks like an ugly hack and it is an ugly hack.
setTimeout(() => {
for (let [k, v] of index) {
if (v === currentPath) {
let y = box.getScreenNumber(k) - box.childBase
if (y <= 0) {
y = 0
}
const line = box.getScreenLine(y + box.childBase)
program.cursorPos(y, line.search(/\S/))
}
}
}, 100)
}
}
function showStatusBar(status) {
statusBar.show()
statusBar.setContent(config.statusBar(` ${status} `))
screen.render()
}
function hideStatusBar() {
if (!statusBar.hidden) {
statusBar.hide()
statusBar.setContent('')
screen.render()
}
}
function render() {
let content
[content, index] = print(json, {expanded, highlight, currentPath})
if (typeof content === 'undefined') {
content = 'undefined'
}
box.setContent(content)
screen.render()
}
function exit() {
// If exit program immediately, stdin may still receive
// mouse events which will be printed in stdout.
program.disableMouse()
setTimeout(() => process.exit(0), 10)
}
render()
}
function* bfs(json) {
const queue = [[json, '']]
while (queue.length > 0) {
const [v, path] = queue.shift()
if (!v) {
continue
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
const p = path + '[' + (i++) + ']'
queue.push([item, p])
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
const p = path + '.' + key
queue.push([value, p])
}
}
}
}
function* dfs(v, path = '') {
if (!v) {
return
}
if (Array.isArray(v)) {
yield path
let i = 0
for (let item of v) {
yield* dfs(item, path + '[' + (i++) + ']')
}
}
if (typeof v === 'object' && v.constructor === Object) {
yield path
for (let [key, value] of Object.entries(v)) {
yield* dfs(value, path + '.' + key)
}
}
}

@ -1,177 +0,0 @@
#!/usr/bin/env node
'use strict'
const os = require('os')
const fs = require('fs')
const path = require('path')
const JSON = require('lossless-json')
JSON.config({circularRefs: false})
const std = require('./std')
try {
require(path.join(os.homedir(), '.fxrc')) // Should be required before config.js usage.
} catch (err) {
if (err.code !== 'MODULE_NOT_FOUND') {
throw err
}
}
const print = require('./print')
const reduce = require('./reduce')
const stream = require('./stream')
const usage = `
Usage
$ fx [code ...]
Examples
$ echo '{"key": "value"}' | fx 'x => x.key'
value
$ echo '{"key": "value"}' | fx .key
value
$ echo '[1,2,3]' | fx 'this.map(x => x * 2)'
[2, 4, 6]
$ echo '{"items": ["one", "two"]}' | fx 'this.items' 'this[1]'
two
$ echo '{"count": 0}' | fx '{...this, count: 1}'
{"count": 1}
$ echo '{"foo": 1, "bar": 2}' | fx ?
["foo", "bar"]
`
const {stdin, stdout, stderr} = process
const args = process.argv.slice(2)
void function main() {
stdin.setEncoding('utf8')
if (stdin.isTTY) {
handle('')
return
}
const reader = stream(stdin, apply)
stdin.on('readable', reader.read)
stdin.on('end', () => {
if (!reader.isStream()) {
handle(reader.value())
}
})
}()
function handle(input) {
let filename = 'fx'
if (input === '') {
if (args.length === 0 || (args.length === 1 && (args[0] === '-h' || args[0] === '--help'))) {
stderr.write(usage)
process.exit(2)
}
if (args.length === 1 && (args[0] === '-v' || args[0] === '--version')) {
stderr.write(require('./package.json').version + '\n')
process.exit(0)
}
if (args.length === 1 && args[0] === '--life') {
require('./bang')
return
}
input = fs.readFileSync(args[0]).toString('utf8')
filename = path.basename(args[0])
global.FX_FILENAME = filename
args.shift()
}
let json
try {
json = JSON.parse(input)
} catch (e) {
printError(e, input)
process.exit(1)
}
if (args.length === 0 && stdout.isTTY) {
require('./fx')(filename, json)
return
}
apply(json)
}
function apply(json) {
let output = json
for (let [i, code] of args.entries()) {
try {
output = reduce(output, code)
} catch (e) {
if (e === std.skip) {
return
}
stderr.write(`${snippet(i, code)}\n${e.stack || e}\n`)
process.exit(1)
}
}
if (typeof output === 'undefined') {
stderr.write('undefined\n')
} else if (typeof output === 'string') {
console.log(output)
} else {
const [text] = print(output)
console.log(text)
}
}
function snippet(i, code) {
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) + '...'
}
const chalk = require('chalk')
return `\n ${pre} ${chalk.red.underline(code)} ${post}\n`
}
function printError(e, input) {
if (e.char) {
let lineNumber = 1, start = e.char - 70, end = e.char + 50
if (start < 0) start = 0
if (end > input.length) end = input.length
for (let i = 0; i < input.length && i < start; i++) {
if (input[i] === '\n') lineNumber++
}
let lines = input
.substring(start, end)
.split('\n')
if (lines.length > 1) {
lines = lines.slice(1)
lineNumber++
}
const chalk = require('chalk')
process.stderr.write(`\n`)
for (let line of lines) {
process.stderr.write(` ${chalk.yellow(lineNumber)} ${line}\n`)
lineNumber++
}
process.stderr.write(`\n`)
}
process.stderr.write(e.toString() + '\n')
}

@ -1,53 +0,0 @@
{
"name": "fx",
"version": "20.0.2",
"description": "Command-line JSON viewer",
"repository": "antonmedv/fx",
"homepage": "https://fx.wtf",
"author": "Anton Medvedev <anton@medv.io>",
"license": "MIT",
"bin": {
"fx": "index.js"
},
"files": [
"*.js"
],
"scripts": {
"test": "ava",
"build": "pkg . --out-path dist",
"zip": "cd dist && find . -name 'fx-*' -exec zip '{}.zip' '{}' \\;",
"release": "release-it --github.release --github.assets=dist/*.zip",
"brew:release": "brew bump-formula-pr --url=https://registry.npmjs.org/fx/-/fx-`fx package.json .version`.tgz --no-audit fx"
},
"release-it": {
"hooks": {
"after:bump": "rm -rf ./dist && npm run build && npm run zip"
}
},
"keywords": [
"json",
"viewer",
"cli",
"terminal",
"term",
"console",
"ascii",
"unicode",
"blessed"
],
"engines": {
"node": ">=8"
},
"dependencies": {
"@medv/blessed": "^2.0.1",
"chalk": "^4.1.0",
"indent-string": "^4.0.0",
"lossless-json": "^1.0.4",
"string-width": "^4.2.0"
},
"devDependencies": {
"ava": "^3.12.1",
"pkg": "^4.4.9",
"release-it": "^14.0.1"
}
}

@ -1,117 +0,0 @@
'use strict'
const indent = require('indent-string')
const config = require('./config')
function format(value, style, highlightStyle, regexp, transform = x => x) {
if (!regexp) {
return style(transform(value))
}
const marked = value
.replace(regexp, s => '<highlight>' + s + '<highlight>')
return transform(marked)
.split(/<highlight>/g)
.map((s, i) => i % 2 !== 0 ? highlightStyle(s) : style(s))
.join('')
}
function print(input, options = {}) {
const {expanded, highlight, currentPath} = options
const index = new Map()
let row = 0
function doPrint(v, path = '') {
index.set(row, path)
// Code for highlighting parts become cumbersome.
// Maybe we should refactor this part.
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
const formatStyle = (v, style) => format(v, style, highlightStyle, highlight)
const formatText = (v, style, path) => {
const highlightStyle = (currentPath === path) ? config.highlightCurrent : config.highlight
return format(v, style, highlightStyle, highlight, JSON.stringify)
}
const eol = () => {
row++
return '\n'
}
if (typeof v === 'undefined') {
return void 0
}
if (v === null) {
return formatStyle(JSON.stringify(v), config.null)
}
if (typeof v === 'number' && Number.isFinite(v)) {
return formatStyle(JSON.stringify(v), config.number)
}
if (typeof v === 'object' && v.isLosslessNumber) {
return formatStyle(v.toString(), config.number)
}
if (typeof v === 'boolean') {
return formatStyle(JSON.stringify(v), config.boolean)
}
if (typeof v === 'string') {
return formatText(v, config.string, path)
}
if (Array.isArray(v)) {
let output = config.bracket('[')
const len = v.length
if (len > 0) {
if (expanded && !expanded.has(path)) {
output += '\u2026'
} else {
output += eol()
let i = 0
for (let item of v) {
const value = typeof item === 'undefined' ? null : item // JSON.stringify compatibility
output += indent(doPrint(value, path + '[' + i + ']'), config.space)
output += i++ < len - 1 ? config.comma(',') : ''
output += eol()
}
}
}
return output + config.bracket(']')
}
if (typeof v === 'object' && v.constructor === Object) {
let output = config.bracket('{')
const entries = Object.entries(v).filter(([key, value]) => typeof value !== 'undefined') // JSON.stringify compatibility
const len = entries.length
if (len > 0) {
if (expanded && !expanded.has(path)) {
output += '\u2026'
} else {
output += eol()
let i = 0
for (let [key, value] of entries) {
const part = formatText(key, config.key, path + '.' + key) + config.colon(':') + ' ' + doPrint(value, path + '.' + key)
output += indent(part, config.space)
output += i++ < len - 1 ? config.comma(',') : ''
output += eol()
}
}
}
return output + config.bracket('}')
}
return JSON.stringify(v, null, config.space)
}
return [doPrint(input), index]
}
module.exports = print

@ -1,49 +0,0 @@
'use strict'
const JSON = require('lossless-json') // override JSON for user's code
module.exports = function (json, code) {
if (process.env.FX_APPLY) {
return global[process.env.FX_APPLY](code)(json)
}
if ('.' === code) {
return json
}
if ('?' === code) {
return Object.keys(json)
}
if (/^(\.\w*)+\[]/.test(code)) {
function fold(s) {
if (s.length === 1) {
return 'x => x' + s[0]
}
let obj = s.shift()
obj = obj === '.' ? 'x' : 'x' + obj
return `x => Object.values(${obj}).flatMap(${fold(s)})`
}
code = fold(code.split('[]'))
}
if (/^\.\[/.test(code)) {
return eval(`function fn() {
return this${code.substring(1)}
}; fn`).call(json)
}
if (/^\./.test(code)) {
return eval(`function fn() {
return this${code}
}; fn`).call(json)
}
const fn = eval(`function fn() {
return ${code}
}; fn`).call(json)
if (typeof fn === 'function') {
return fn(json)
}
return fn
}

@ -1,33 +0,0 @@
'use strict'
const JSON = require('lossless-json')
const skip = Symbol('skip')
function select(cb) {
return json => {
if (!cb(json)) {
throw skip
}
return json
}
}
function filter(cb) {
return json => {
if (cb(json)) {
throw skip
}
return json
}
}
function save(json) {
if (!global.FX_FILENAME) {
throw "No filename provided.\nTo edit-in-place, specify JSON file as first argument."
}
require('fs').writeFileSync(global.FX_FILENAME, JSON.stringify(json, null, 2))
return json
}
Object.assign(exports, {skip, select, filter, save})
Object.assign(global, exports)
global.std = exports

@ -1,84 +0,0 @@
'use strict'
const JSON = require('lossless-json')
function apply(cb, input) {
let json
try {
json = JSON.parse(input)
} catch (e) {
process.stderr.write(e.toString() + '\n')
return
}
cb(json)
}
function stream(from, cb) {
let buff = ''
let lastChar = ''
let len = 0
let depth = 0
let isString = false
let count = 0
let head = ''
const check = (i) => {
if (depth <= 0) {
const input = buff.substring(0, len + i + 1)
if (count > 0) {
if (head !== '') {
apply(cb, head)
head = ''
}
apply(cb, input)
} else {
head = input
}
buff = buff.substring(len + i + 1)
len = -i - 1
count++
}
}
return {
isStream() {
return count > 1
},
value() {
return head + buff
},
read() {
let chunk
while ((chunk = from.read())) {
len = buff.length
buff += chunk
for (let i = 0; i < chunk.length; i++) {
if (isString) {
if (chunk[i] === '"' && ((i === 0 && lastChar !== '\\') || (i > 0 && chunk[i - 1] !== '\\'))) {
isString = false
check(i)
}
continue
}
if (chunk[i] === '{' || chunk[i] === '[') {
depth++
} else if (chunk[i] === '}' || chunk[i] === ']') {
depth--
check(i)
} else if (chunk[i] === '"') {
isString = true
}
}
lastChar = chunk[chunk.length - 1]
}
}
}
}
module.exports = stream

@ -1,85 +0,0 @@
'use strict'
const test = require('ava')
const {execSync} = require('child_process')
const stream = require('./stream')
function fx(json, code = '') {
return execSync(`echo '${JSON.stringify(json)}' | node index.js ${code}`).toString('utf8')
}
test('pass', t => {
const r = fx([{'greeting': 'hello world'}])
t.deepEqual(JSON.parse(r), [{'greeting': 'hello world'}])
})
test('anon func', t => {
const r = fx({'key': 'value'}, '\'function (x) { return x.key }\'')
t.is(r, 'value\n')
})
test('arrow func', t => {
const r = fx({'key': 'value'}, '\'x => x.key\'')
t.is(r, 'value\n')
})
test('arrow func ()', t => {
const r = fx({'key': 'value'}, '\'(x) => x.key\'')
t.is(r, 'value\n')
})
test('this bind', t => {
const r = fx([1, 2, 3, 4, 5], '\'this.map(x => x * this.length)\'')
t.deepEqual(JSON.parse(r), [5, 10, 15, 20, 25])
})
test('chain', t => {
const r = fx({'items': ['foo', 'bar']}, '\'this.items\' \'.\' \'x => x[1]\'')
t.is(r, 'bar\n')
})
test('file argument', t => {
const r = execSync(`node index.js package.json .name`).toString('utf8')
t.is(r, 'fx\n')
})
test('stream', t => {
const input = `
{"index": 0} {"index": 1}
{"index": 2, "quote": "\\""}
{"index": 3} "Hello" "world"
{"index": 6, "key": "one \\"two\\" three"}
`
t.plan(7 * (input.length - 1))
for (let i = 0; i < input.length; i++) {
const parts = [input.substring(0, i), input.substring(i)]
const reader = stream(
{
read() {
return parts.shift()
}
},
json => {
t.pass()
}
)
reader.read()
}
})
test('lossless number', t => {
const r = execSync(`echo '{"long": 123456789012345678901}' | node index.js .long`).toString('utf8')
t.is(r, '123456789012345678901\n')
})
test('value iterator', t => {
const r = fx({master: {foo: [{bar: [{val: 1}]}]}}, '.master.foo[].bar[].val')
t.deepEqual(JSON.parse(r), [1])
})
test('value iterator simple', t => {
const r = fx([{val:1},{val:2}], '.[].val')
t.deepEqual(JSON.parse(r), [1, 2])
})
Loading…
Cancel
Save