diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 74eaed1..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -/node_modules/ -/dist/ -package-lock.json diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f2dff56..0000000 --- a/.travis.yml +++ /dev/null @@ -1,4 +0,0 @@ -language: node_js -node_js: - - "node" - - "11" diff --git a/DOCS.md b/DOCS.md deleted file mode 100644 index c0e709a..0000000 --- a/DOCS.md +++ /dev/null @@ -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' -``` diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 7d058f8..0000000 --- a/LICENSE +++ /dev/null @@ -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. diff --git a/README.md b/README.md deleted file mode 100644 index ffd3c60..0000000 --- a/README.md +++ /dev/null @@ -1,106 +0,0 @@ -

fx logo

-

fx example

- -_* 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) diff --git a/bang.js b/bang.js deleted file mode 100644 index 3c43a4b..0000000 --- a/bang.js +++ /dev/null @@ -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) -}) diff --git a/config.js b/config.js deleted file mode 100644 index ee4bc20..0000000 --- a/config.js +++ /dev/null @@ -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, -} diff --git a/find.js b/find.js deleted file mode 100644 index bfd7fc3..0000000 --- a/find.js +++ /dev/null @@ -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 diff --git a/fx.js b/fx.js deleted file mode 100644 index 751c10b..0000000 --- a/fx.js +++ /dev/null @@ -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) - } - } -} diff --git a/index.js b/index.js deleted file mode 100755 index 0464581..0000000 --- a/index.js +++ /dev/null @@ -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') -} diff --git a/package.json b/package.json deleted file mode 100644 index 0a33c49..0000000 --- a/package.json +++ /dev/null @@ -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 ", - "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" - } -} diff --git a/print.js b/print.js deleted file mode 100644 index 6eebc7e..0000000 --- a/print.js +++ /dev/null @@ -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 => '' + s + '') - - return transform(marked) - .split(//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 diff --git a/reduce.js b/reduce.js deleted file mode 100644 index f333f13..0000000 --- a/reduce.js +++ /dev/null @@ -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 -} diff --git a/std.js b/std.js deleted file mode 100644 index 9707675..0000000 --- a/std.js +++ /dev/null @@ -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 diff --git a/stream.js b/stream.js deleted file mode 100644 index 7a31a28..0000000 --- a/stream.js +++ /dev/null @@ -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 diff --git a/test.js b/test.js deleted file mode 100644 index 9fb9991..0000000 --- a/test.js +++ /dev/null @@ -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]) -})