Skip to content

mangrisano/jpick

Repository files navigation

jpick

CI License: MIT C++20

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.

Features

  • 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, like jq
  • Compact or pretty-printed output, with configurable indentation (--indent, --tab)
  • Sort object keys with -S/--sort-keys
  • Raw string output (-r/--raw-output), like jq -r
  • Read from stdin or a file
  • Clear error messages with a non-zero exit code on failure

Install

Homebrew (macOS / Linux)

brew install mangrisano/jpick/jpick

From a release

Prebuilt 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/jpick

From source

See Build below.

Build

Requirements: a C++20 compiler and CMake ≥ 3.20.

cmake -S . -B build
cmake --build build

The 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)

Run the tests

ctest --test-dir build --output-on-failure

Usage

jpick [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.

Examples

Format JSON (no path)

echo '{"a":1,"b":[1,2]}' | jpick
{"a": 1, "b": [1, 2]}

Pretty-print

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
  ]
}

Extract an object field

echo '{"user":{"name":"anna","age":30}}' | jpick '.user.name'
"anna"

Index into an array

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

Iterate over an array

[] 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"

Pipe

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"

Interpolate values into a string

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"

Raw string output

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

Sort object keys

-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}

Choose the indentation

--indent N uses N spaces per level and --tab uses tabs; both imply --pretty:

echo '{"a":[1,2]}' | jpick --indent 4
{
    "a": [
        1,
        2
    ]
}

Format with @

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

Combine a path with pretty-print

echo '{"tags":["cli","json"],"count":2}' | jpick '.tags' --pretty
[
  "cli",
  "json"
]

Read from a file

echo '{"tags":["cli","json"],"count":2}' > data.json
jpick '.tags' data.json
["cli", "json"]

Scalar values

Strings, numbers, booleans and null are printed as valid JSON:

echo '{"ok":true,"ratio":6.022e23,"missing":null}' | jpick '.ratio'
6.022e+23

Errors

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)

Path syntax

  • .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

Project layout

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.

Notes and limitations

  • Object key order follows the source document; duplicate keys keep the last value. Use -S/--sort-keys to emit keys in ascending order instead.
  • Unicode \uXXXX escape sequences are not decoded.

License

Released under the MIT License.

About

A tiny jq-like JSON tool written in C++20

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages