A tiny jq-like JSON tool written in C++20.
jpick reads JSON from standard input or a file, optionally extracts a value
with a simple path expression, and prints the result back as valid JSON
(compact or pretty-printed).
It was built as a learning project: a hand-written lexer, a recursive-descent
parser, a std::variant-based data model, a path query engine, and a
serializer.
- Hand-written JSON lexer and recursive-descent parser
- Query values with a path expression: object keys, array indices, and iteration (
[]) - Compose queries with the pipe operator (
|) - Build strings with interpolation:
"\(.name): \(.count)" - Format output with
@text,@json,@base64,@base64d,@uri,@sh,@csv,@tsv, likejq - Compact or pretty-printed output, with configurable indentation (
--indent,--tab) - Sort object keys with
-S/--sort-keys - Raw string output (
-r/--raw-output), likejq -r - Read from stdin or a file
- Clear error messages with a non-zero exit code on failure
brew install mangrisano/jpick/jpickPrebuilt binaries for Linux and macOS are attached to each
release. Download the one for
your platform, verify its checksum, make it executable, and move it onto your
PATH:
chmod +x jpick-macos-arm64
sudo mv jpick-macos-arm64 /usr/local/bin/jpickSee Build below.
Requirements: a C++20 compiler and CMake ≥ 3.20.
cmake -S . -B build
cmake --build buildThe executable is produced at build/jpick.
The examples below call it simply as jpick. To use it that way, either run it
as ./build/jpick, or install it onto your PATH:
cmake --install build # -> /usr/local/bin/jpick (may need sudo)
cmake --install build --prefix ~/.local # -> ~/.local/bin/jpick (no sudo)ctest --test-dir build --output-on-failurejpick [OPTIONS] [path] [file]
Positionals:
path TEXT Query path, e.g. '.a.b[0]' (default: whole document)
file TEXT JSON file to read (default: stdin)
Options:
-h,--help Print help and exit
-v,--version Print version and exit
-p,--pretty Pretty-print the output
-r,--raw-output Output strings without quotes or escaping
-S,--sort-keys Sort object keys in the output
--indent INT Indent with N spaces (implies --pretty)
--tab Indent with tabs (implies --pretty)
Output blocks below are shown as literal terminal output.
echo '{"a":1,"b":[1,2]}' | jpick{"a": 1, "b": [1, 2]}
Pretty mode puts every array element and object member on its own line, indented by two spaces per nesting level:
echo '{"a":1,"b":[1,2]}' | jpick --pretty{
"a": 1,
"b": [
1,
2
]
}
echo '{"user":{"name":"anna","age":30}}' | jpick '.user.name'"anna"
echo '{"users":["anna","luca","sara"]}' | jpick '.users[1]'"luca"
Paths combine keys and indices, including nested indices:
echo '{"matrix":[[1,2],[3,4]]}' | jpick '.matrix[1][0]'3
[] expands an array into one result per element (like jq). Following steps
are applied to each element:
echo '{"users":[{"name":"anna"},{"name":"luca"}]}' | jpick '.users[].name'"anna"
"luca"
The pipe operator | feeds every result of one stage into the next. .a | .b
is the same as .a.b, but pipes also let you compose stages freely:
echo '{"users":[{"name":"anna"},{"name":"luca"}]}' | jpick '.users[] | .name'"anna"
"luca"
A "..." segment builds a string, replacing every \(...) with the value the
inner path produces (like jq). The inner expression must yield exactly one
value:
echo '{"items":[{"n":"a","c":1},{"n":"b","c":2}]}' | jpick '.items[] | "\(.n)=\(.c)"'"a=1"
"b=2"
By default strings are printed as valid JSON (quoted). -r/--raw-output prints
top-level strings without quotes or escaping; other values are unchanged:
echo '{"items":[{"n":"a","c":1},{"n":"b","c":2}]}' | jpick -r '.items[] | "\(.n)=\(.c)"'a=1
b=2
-S/--sort-keys emits object keys in ascending order (recursively). Without
it, keys keep the order of the source document:
echo '{"zebra":1,"apple":2,"mango":3}' | jpick -S{"apple": 2, "mango": 3, "zebra": 1}
--indent N uses N spaces per level and --tab uses tabs; both imply
--pretty:
echo '{"a":[1,2]}' | jpick --indent 4{
"a": [
1,
2
]
}
A pipe stage starting with @ formats each value. @csv/@tsv take an array
of scalars; @base64, @base64d, @uri, @sh, @json and @text take any
value (@sh also accepts an array). Combine with -r for clean output.
One example per format (each run as echo '<input>' | jpick -r '. | @<fmt>'):
| Format | Input | Output |
|---|---|---|
@text |
42 |
42 |
@json |
{"a":1,"b":[2,3]} |
{"a": 1, "b": [2, 3]} |
@base64 |
"hello" |
aGVsbG8= |
@base64d |
"aGVsbG8=" |
hello |
@uri |
"a b/c?x=1" |
a%20b%2Fc%3Fx%3D1 |
@sh |
"it's ok" |
'it'\''s ok' |
@csv |
["anna",30,true,null] |
"anna",30,true, |
@tsv |
["anna",30,true,null] |
anna<tab>30<tab>true<tab> |
Turning an array of rows into CSV:
echo '[["anna",30,true],["luca",25,false]]' | jpick -r '.[] | @csv'"anna",30,true
"luca",25,false
echo '{"tags":["cli","json"],"count":2}' | jpick '.tags' --pretty[
"cli",
"json"
]
echo '{"tags":["cli","json"],"count":2}' > data.json
jpick '.tags' data.json["cli", "json"]
Strings, numbers, booleans and null are printed as valid JSON:
echo '{"ok":true,"ratio":6.022e23,"missing":null}' | jpick '.ratio'6.022e+23
Invalid input, a missing field, or an out-of-range index print a message to
stderr and exit with status 1:
echo '{"a":1}' | jpick '.b'
# jpick: Field does not exist (exit code 1)
echo '{"a":[1]}' | jpick '.a[5]'
# jpick: Index out of range (exit code 1).key— descend into an object by key[n]— index into an array (0-based)[]— iterate over every element of an array (one result per element)|— pipe: feed every result of one stage into the next"..."— a string literal;\(...)interpolates the value of an inner path@fmt— format a value:@text,@json,@base64,@base64d,@uri,@sh,@csv,@tsv- Steps can be chained:
.a.b[0].c[1][2],.users[].name,.users[] | .name - An empty path (or none) selects the whole document
include/jpick/
json.hpp # Value data model (std::variant)
lexer.hpp # tokenizer
parser.hpp # recursive-descent parser
query.hpp # path parsing and tree navigation
serializer.hpp # compact and pretty serialization
src/
main.cpp # CLI entry point
tests/
test_jpick.cpp
third_party/
doctest.h # test framework
CLI11.hpp # command-line parsing
All library code lives in the jpick namespace.
- Object key order follows the source document; duplicate keys keep the last
value. Use
-S/--sort-keysto emit keys in ascending order instead. - Unicode
\uXXXXescape sequences are not decoded.
Released under the MIT License.