Skip to content

Commit 4bad6ad

Browse files
committed
feat: implement a command line interface (CLI)
1 parent 284c0b4 commit 4bad6ad

File tree

8 files changed

+432
-0
lines changed

8 files changed

+432
-0
lines changed

README.md

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ On this page:
2525
- [Syntax](#syntax)
2626
- [JSON Format](#json-format)
2727
- [JavaScript API](#javascript-api)
28+
- [Command line interface (CLI)](#command-line-interface-cli)
2829
- [Gotchas](#gotchas)
2930
- [Development](#development)
3031
- [Motivation](#motivation)
@@ -566,6 +567,48 @@ try {
566567
}
567568
```
568569

570+
## Command line interface (CLI)
571+
572+
When `jsonquery` is installed globally using npm, it can be used on the command line. To install `jsonquery` globally:
573+
574+
```bash
575+
$ npm install -g @jsonquerylang/jsonquery
576+
```
577+
578+
Usage:
579+
580+
```
581+
$ jsonquery [query] {OPTIONS}
582+
```
583+
584+
Options:
585+
586+
```
587+
--input Input file name
588+
--query Query file name
589+
--output Output file name
590+
--format Can be "text" (default) or "json"
591+
--indentation A string containing the desired indentation,
592+
like " " (default) or " " or "\t". An empty
593+
string will create output without indentation.
594+
--overwrite If true, output can overwrite an existing file
595+
--version, -v Show application version
596+
--help, -h Show this message
597+
```
598+
599+
Example usage:
600+
601+
```
602+
$ jsonquery --input users.json 'sort(.age)'
603+
$ jsonquery --input users.json 'filter(.city == "Rotterdam") | sort(.age)'
604+
$ jsonquery --input users.json 'sort(.age)' > output.json
605+
$ jsonquery --input users.json 'sort(.age)' --output output.json
606+
$ jsonquery --input users.json --query query.txt
607+
$ jsonquery --input users.json --query query.json --format json
608+
$ cat users.json | jsonquery 'sort(.age)'
609+
$ cat users.json | jsonquery 'sort(.age)' > output.json
610+
```
611+
569612
## Gotchas
570613

571614
The JSON Query language has some gotchas. What can be confusing at first is to understand how data is piped through the query. A traditional function call is for example `max(myValues)`, so you may expect to have to write this in JSON Query like `["max", "myValues"]`. However, JSON Query has a functional approach where we create a pipeline like: `data -> max -> result`. So, you will have to write a pipe which first gets this property and next calls the function max: `.myValues | max()`.

bin/cli.js

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env node
2+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3+
import { dirname, join } from 'node:path'
4+
import { fileURLToPath } from 'node:url'
5+
import { parseArgs } from 'node:util'
6+
import { jsonquery } from '../lib/jsonquery.js'
7+
import { help } from './help.js'
8+
9+
const __filename = fileURLToPath(import.meta.url)
10+
const __dirname = dirname(__filename)
11+
12+
const options = {
13+
input: { type: 'string' },
14+
query: { type: 'string' },
15+
output: { type: 'string' },
16+
format: { type: 'string', default: 'text' },
17+
overwrite: { type: 'boolean', default: false },
18+
indentation: { type: 'string', default: ' ' },
19+
version: { type: 'boolean', short: 'v' },
20+
help: { type: 'boolean', short: 'h' }
21+
}
22+
23+
const {
24+
values,
25+
positionals: [inlineQuery]
26+
} = parseArgs({ options, allowPositionals: true })
27+
28+
await run({ ...values, inlineQuery })
29+
30+
/**
31+
* @param {Options} options
32+
* @returns {Promise<void>}
33+
*/
34+
async function run(options) {
35+
if (options.version) {
36+
return writeVersion()
37+
}
38+
39+
if (options.help) {
40+
return writeHelp()
41+
}
42+
43+
try {
44+
const input = await readInput(options)
45+
const query = readQuery(options)
46+
47+
const output = jsonquery(input, query)
48+
49+
return writeOutput(options, output)
50+
} catch (err) {
51+
process.stderr.write(err.toString())
52+
process.exit(1)
53+
}
54+
}
55+
56+
/**
57+
* @param {Options} options
58+
* @returns {Promise<string>}
59+
*/
60+
async function readInput(options) {
61+
const inputStr = options.input ? fileToString(options.input) : await streamToString(process.stdin)
62+
63+
if (inputStr.trim() === '') {
64+
throw Error('No input data provided')
65+
}
66+
67+
return JSON.parse(inputStr)
68+
}
69+
70+
/**
71+
* @param {Options} options
72+
* @returns {Promise<string>}
73+
*/
74+
function readQuery(options) {
75+
const queryStr = options.query
76+
? fileToString(options.query)
77+
: options.inlineQuery
78+
? options.inlineQuery
79+
: throwError('No query provided')
80+
81+
return options.format === 'text' || options.format === undefined
82+
? queryStr
83+
: options.format === 'json'
84+
? JSON.parse(queryStr)
85+
: throwError(`Unknown format "${options.format}". Choose either "text" (default) or "json".`)
86+
}
87+
88+
/**
89+
* @param {Options} options
90+
* @param {JSON} output
91+
*/
92+
function writeOutput(options, output) {
93+
const outputStr = JSON.stringify(output, null, options.indentation)
94+
95+
if (options.output) {
96+
if (existsSync(options.output) && !options.overwrite) {
97+
throwError(`Cannot overwrite existing file "${options.output}"`)
98+
}
99+
100+
writeFileSync(options.output, outputStr)
101+
} else {
102+
process.stdout.write(outputStr)
103+
}
104+
}
105+
106+
function writeVersion() {
107+
const file = join(__dirname, '../package.json')
108+
const pkg = JSON.parse(String(readFileSync(file, 'utf-8')))
109+
110+
process.stdout.write(pkg.version)
111+
}
112+
113+
function writeHelp() {
114+
process.stdout.write(help)
115+
}
116+
117+
function fileToString(fileName) {
118+
return String(readFileSync(fileName))
119+
}
120+
121+
/**
122+
* @param {ReadableStream} readableStream
123+
* @returns {Promise<string>}
124+
*/
125+
function streamToString(readableStream) {
126+
return new Promise((resolve, reject) => {
127+
let text = ''
128+
129+
readableStream.on('data', (chunk) => {
130+
text += String(chunk)
131+
})
132+
readableStream.on('end', () => {
133+
readableStream.destroy()
134+
resolve(text)
135+
})
136+
readableStream.on('error', (err) => reject(err))
137+
})
138+
}
139+
140+
function throwError(message) {
141+
throw new Error(message)
142+
}
143+
144+
/**
145+
* @typedef {Object} Options
146+
* @property {boolean} [version]
147+
* @property {boolean} [help]
148+
* @property {string} [input]
149+
* @property {string} [query]
150+
* @property {string} [output]
151+
* @property {string} [inlineQuery]
152+
* @property {'text' | 'json'} [format='text']
153+
* @property {string} [indentation=' ']
154+
* @property {boolean} [overwrite=false]
155+
*/

bin/help.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
export const help = `
2+
jsonquery
3+
https://github.com/jsonquerylang/jsonquery
4+
5+
Query JSON documents. The command line application requires an input document
6+
and query, and returns output containing the query result. The query can be
7+
either in text format (default) or json format.
8+
9+
Usage:
10+
11+
jsonquery [query] {OPTIONS}
12+
13+
Options:
14+
15+
--input Input file name
16+
--query Query file name
17+
--output Output file name
18+
--format Can be "text" (default) or "json"
19+
--indentation A string containing the desired indentation,
20+
like " " (default) or " " or "\\t". An empty
21+
string will create output without indentation.
22+
--overwrite If true, output can overwrite an existing file
23+
--version, -v Show application version
24+
--help, -h Show this message
25+
26+
Example usage:
27+
28+
jsonquery --input users.json 'sort(.age)'
29+
jsonquery --input users.json 'filter(.city == "Rotterdam") | sort(.age)'
30+
jsonquery --input users.json 'sort(.age)' > output.json
31+
jsonquery --input users.json 'sort(.age)' --output output.json
32+
jsonquery --input users.json --query query.txt
33+
jsonquery --input users.json --query query.json --format json
34+
cat users.json | jsonquery 'sort(.age)'
35+
cat users.json | jsonquery 'sort(.age)' > output.json
36+
37+
`

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"type": "git",
2121
"url": "git+https://github.com/jsonquerylang/jsonquery.git"
2222
},
23+
"bin": {
24+
"jsonquery": "./bin/cli.js"
25+
},
2326
"module": "./lib/jsonquery.js",
2427
"types": "./lib/jsonquery.d.ts",
2528
"exports": {

0 commit comments

Comments
 (0)