Skip to content

json

A small jq: reads a JSON file, and prints it indented, or the part a path names. It puts the standard library together: arguments, files, strings.parse_float, recursive enums, lists of pairs and Result.

A path is a sequence of .name and [index] steps (. alone is the whole document). Errors go to standard error, with the exit status 1 for a file that cannot be read or parsed and 2 for a wrong command line.

Terminal window
jihoo run examples/json.jh -- data.json
jihoo run examples/json.jh -- data.json '.users[0].name'
jihoo run examples/json.jh -- data.json '.users' -o users.json
examples/json.jh
import files
import strings
enum Json {
Null
Bool(bool)
Num(f64)
Str(str)
Arr([Json])
// Members in the order the document gives them.
Obj([(str, Json)])
}
// ---- parsing ----
// What is left to parse: the text and where in it.
struct Input {
text: str
at: i64
}
// A value and the input after it, or what is wrong and where.
enum Parsed(T: type) {
Done(T, Input)
Failed(str)
}
fn fail(comptime T: type, inp: Input, what: str) -> Parsed(T) {
// Line and column of the error, counted from 1.
let line = 1
let col = 1
for i in 0..inp.at {
if inp.text[i] == 10 {
line += 1
col = 1
} else {
col += 1
}
}
return Parsed(T).Failed(to_str(line) + ":" + to_str(col) + ": " + what)
}
fn skip_space(inp: Input) -> Input {
while inp.at < len(inp.text) && is_json_space(inp.text[inp.at]) {
inp.at += 1
}
return inp
}
fn is_json_space(b: u8) -> bool {
return b == 32 || b == 9 || b == 10 || b == 13
}
// The next byte, or 0 at the end.
fn peek(inp: Input) -> u8 {
if inp.at < len(inp.text) {
return inp.text[inp.at]
}
return 0
}
fn parse(text: str) -> Result(Json, str) {
match value(Input { text: text, at: 0 }) {
Done(v, rest) => {
rest = skip_space(rest)
if rest.at < len(rest.text) {
match fail(Json, rest, "text after the value") {
Failed(msg) => return Result.Err(msg)
Done(_, _) => {}
}
}
return Result.Ok(v)
}
Failed(msg) => return Result.Err(msg)
}
}
fn value(inp: Input) -> Parsed(Json) {
inp = skip_space(inp)
let c = peek(inp)
if c == 123 { // {
return object(inp)
}
if c == 91 { // [
return array(inp)
}
if c == 34 { // "
match string(inp) {
Done(s, rest) => return Parsed(Json).Done(Json.Str(s), rest)
Failed(msg) => return Parsed(Json).Failed(msg)
}
}
if c == 45 || strings.is_digit(c) {
return number(inp)
}
if word(inp, "true") {
inp.at += 4
return Parsed(Json).Done(Json.Bool(true), inp)
}
if word(inp, "false") {
inp.at += 5
return Parsed(Json).Done(Json.Bool(false), inp)
}
if word(inp, "null") {
inp.at += 4
return Parsed(Json).Done(Json.Null, inp)
}
if c == 0 {
return fail(Json, inp, "the text ends where a value should be")
}
return fail(Json, inp, "expected a value")
}
fn word(inp: Input, w: str) -> bool {
return inp.at + len(w) <= len(inp.text) && inp.text[inp.at:inp.at + len(w)] == w
}
// `-?(0|[1-9][0-9]*)(.[0-9]+)?([eE][+-]?[0-9]+)?`, which `parse_float` reads.
fn number(inp: Input) -> Parsed(Json) {
let start = inp.at
let i = inp.at
let t = inp.text
if peek(inp) == 45 {
i += 1
}
let digits = run_of_digits(t, i)
if digits == 0 || (digits > 1 && t[i] == 48) {
inp.at = i
return fail(Json, inp, "a number needs digits, and no 0 in front of them")
}
i += digits
if i < len(t) && t[i] == 46 { // .
let more = run_of_digits(t, i + 1)
if more == 0 {
inp.at = i + 1
return fail(Json, inp, "expected digits after the point")
}
i += 1 + more
}
if i < len(t) && (t[i] == 101 || t[i] == 69) { // e E
i += 1
if i < len(t) && (t[i] == 43 || t[i] == 45) {
i += 1
}
let more = run_of_digits(t, i)
if more == 0 {
inp.at = i
return fail(Json, inp, "expected digits in the exponent")
}
i += more
}
match strings.parse_float(t[start:i]) {
Some(x) => {
inp.at = i
return Parsed(Json).Done(Json.Num(x), inp)
}
None => return fail(Json, inp, "not a number")
}
}
fn run_of_digits(t: str, from: i64) -> i64 {
let i = from
while i < len(t) && strings.is_digit(t[i]) {
i += 1
}
return i - from
}
// A string, after its opening quote; escapes are decoded into UTF-8.
fn string(inp: Input) -> Parsed(str) {
inp.at += 1
let out: [u8] = []
while inp.at < len(inp.text) {
let c = inp.text[inp.at]
if c == 34 {
inp.at += 1
return Parsed(str).Done(from_bytes(out), inp)
}
if c < 32 {
return fail(str, inp, "a control character in a string; write it as an escape")
}
if c != 92 {
push(out, c)
inp.at += 1
} else {
if inp.at + 1 >= len(inp.text) {
break
}
let e = inp.text[inp.at + 1]
inp.at += 2
if e == 117 { // \uXXXX, maybe a surrogate pair
match code_point(inp) {
Done(cp, rest) => {
inp = rest
out = utf8(out, cp)
}
Failed(msg) => return Parsed(str).Failed(msg)
}
} else {
let b = unescape(e)
if b == 0 {
inp.at -= 1
return fail(str, inp, "unknown escape")
}
push(out, b)
}
}
}
return fail(str, inp, "the text ends inside a string")
}
// The byte an escape `\e` stands for, or 0 for no escape (`\u` aside).
fn unescape(e: u8) -> u8 {
if e == 34 || e == 92 || e == 47 {
return e
}
if e == 98 {
return 8
}
if e == 102 {
return 12
}
if e == 110 {
return 10
}
if e == 114 {
return 13
}
if e == 116 {
return 9
}
return 0
}
// The character of a `\u` escape whose `\u` was read: four hex digits, and
// for a high surrogate a second `\u` escape with the low one.
fn code_point(inp: Input) -> Parsed(i64) {
let hi = hex4(inp)
if hi < 0 {
return fail(i64, inp, "expected four hex digits")
}
inp.at += 4
if hi < 0xd800 || hi > 0xdfff {
return Parsed(i64).Done(hi, inp)
}
if hi > 0xdbff || !word(inp, "\\u") {
return fail(i64, inp, "a lone surrogate")
}
inp.at += 2
let lo = hex4(inp)
if lo < 0xdc00 || lo > 0xdfff {
return fail(i64, inp, "a lone surrogate")
}
inp.at += 4
return Parsed(i64).Done(0x10000 + ((hi - 0xd800) << 10) + (lo - 0xdc00), inp)
}
// Four hex digits at the input as a number, or -1.
fn hex4(inp: Input) -> i64 {
if inp.at + 4 > len(inp.text) {
return -1
}
let n = 0
for k in 0..4 {
let d = hex_digit(inp.text[inp.at + k])
if d < 0 {
return -1
}
n = n * 16 + d
}
return n
}
fn hex_digit(b: u8) -> i64 {
if strings.is_digit(b) {
return (b - 48) as i64
}
if b >= 97 && b <= 102 {
return (b - 87) as i64
}
if b >= 65 && b <= 70 {
return (b - 55) as i64
}
return -1
}
fn utf8(out: [u8], cp: i64) -> [u8] {
if cp < 0x80 {
push(out, cp as u8)
} else if cp < 0x800 {
push(out, (0xc0 | (cp >> 6)) as u8)
push(out, (0x80 | (cp & 0x3f)) as u8)
} else if cp < 0x10000 {
push(out, (0xe0 | (cp >> 12)) as u8)
push(out, (0x80 | ((cp >> 6) & 0x3f)) as u8)
push(out, (0x80 | (cp & 0x3f)) as u8)
} else {
push(out, (0xf0 | (cp >> 18)) as u8)
push(out, (0x80 | ((cp >> 12) & 0x3f)) as u8)
push(out, (0x80 | ((cp >> 6) & 0x3f)) as u8)
push(out, (0x80 | (cp & 0x3f)) as u8)
}
return out
}
fn array(inp: Input) -> Parsed(Json) {
inp.at += 1
let items: [Json] = []
inp = skip_space(inp)
if peek(inp) == 93 {
inp.at += 1
return Parsed(Json).Done(Json.Arr(items), inp)
}
while true {
match value(inp) {
Done(v, rest) => {
push(items, v)
inp = skip_space(rest)
}
Failed(msg) => return Parsed(Json).Failed(msg)
}
let c = peek(inp)
inp.at += 1
if c == 93 {
return Parsed(Json).Done(Json.Arr(items), inp)
}
if c != 44 {
inp.at -= 1
return fail(Json, inp, "expected `,` or `]`")
}
}
trap()
}
fn object(inp: Input) -> Parsed(Json) {
inp.at += 1
let members: [(str, Json)] = []
inp = skip_space(inp)
if peek(inp) == 125 {
inp.at += 1
return Parsed(Json).Done(Json.Obj(members), inp)
}
while true {
inp = skip_space(inp)
if peek(inp) != 34 {
return fail(Json, inp, "expected a member's name")
}
let name = ""
match string(inp) {
Done(s, rest) => {
name = s
inp = skip_space(rest)
}
Failed(msg) => return Parsed(Json).Failed(msg)
}
if peek(inp) != 58 {
return fail(Json, inp, "expected `:`")
}
inp.at += 1
match value(inp) {
Done(v, rest) => {
push(members, (name, v))
inp = skip_space(rest)
}
Failed(msg) => return Parsed(Json).Failed(msg)
}
let c = peek(inp)
inp.at += 1
if c == 125 {
return Parsed(Json).Done(Json.Obj(members), inp)
}
if c != 44 {
inp.at -= 1
return fail(Json, inp, "expected `,` or `}`")
}
}
trap()
}
// ---- printing ----
// `v` as indented JSON, two spaces a level, as `jq` writes it.
fn render(v: Json) -> str {
let out: [u8] = []
out = emit(out, v, 0)
push(out, 10)
return from_bytes(out)
}
fn emit(out: [u8], v: Json, depth: i64) -> [u8] {
match v {
Null => out = text(out, "null")
Bool(b) => out = text(out, to_str(b))
Num(x) => out = text(out, number_text(x))
Str(s) => out = quoted(out, s)
Arr(items) => {
if len(items) == 0 {
return text(out, "[]")
}
push(out, 91)
for i, item in items {
if i > 0 {
push(out, 44)
}
out = indent(out, depth + 1)
out = emit(out, item, depth + 1)
}
out = indent(out, depth)
push(out, 93)
}
Obj(members) => {
if len(members) == 0 {
return text(out, "{}")
}
push(out, 123)
for i, m in members {
if i > 0 {
push(out, 44)
}
out = indent(out, depth + 1)
out = quoted(out, m.0)
out = text(out, ": ")
out = emit(out, m.1, depth + 1)
}
out = indent(out, depth)
push(out, 125)
}
}
return out
}
// Integers as integers; other numbers as the shortest text that reads back.
// JSON has no infinities: a number too large for an `f64` (`1e400`) is
// written as the largest one, as `jq` does.
fn number_text(x: f64) -> str {
if x == trunc(x) && abs(x) < 9007199254740992.0 {
return to_str(x as i64)
}
if abs(x) == 1.0 / 0.0 {
return to_str(copysign(1.7976931348623157e308, x))
}
return to_str(x)
}
fn text(out: [u8], s: str) -> [u8] {
for b in s {
push(out, b)
}
return out
}
fn indent(out: [u8], depth: i64) -> [u8] {
push(out, 10)
for i in 0..2 * depth {
push(out, 32)
}
return out
}
fn quoted(out: [u8], s: str) -> [u8] {
push(out, 34)
for b in s {
if b == 34 || b == 92 {
push(out, 92)
push(out, b)
} else if b == 10 {
out = text(out, "\\n")
} else if b == 9 {
out = text(out, "\\t")
} else if b == 13 {
out = text(out, "\\r")
} else if b < 32 {
out = text(out, "\\u00")
push(out, hex(b >> 4))
push(out, hex(b & 15))
} else {
push(out, b)
}
}
push(out, 34)
return out
}
fn hex(d: u8) -> u8 {
if d < 10 {
return 48 + d
}
return 87 + d
}
// ---- paths ----
enum Step {
Name(str)
Index(i64)
}
fn parse_path(p: str) -> Result([Step], str) {
let steps: [Step] = []
if p == "." {
return Result.Ok(steps)
}
let i = 0
while i < len(p) {
if p[i] == 46 { // .name
let j = i + 1
while j < len(p) && p[j] != 46 && p[j] != 91 {
j += 1
}
if j == i + 1 {
return Result.Err("an empty name in the path at " + to_str(i))
}
push(steps, Step.Name(p[i + 1:j]))
i = j
} else if p[i] == 91 { // [index]
let j = strings.index_from(p, "]", i)
if j < 0 {
return Result.Err("a `[` without `]` in the path")
}
match strings.parse_int(p[i + 1:j]) {
Some(n) => push(steps, Step.Index(n))
None => return Result.Err("not an index: " + p[i + 1:j])
}
i = j + 1
} else {
return Result.Err("a path is `.name` and `[index]` steps, not `" + p[i:] + "`")
}
}
return Result.Ok(steps)
}
// The part of `v` at `steps`, or what is missing. A negative index counts
// from the end.
fn at(v: Json, steps: [Step]) -> Result(Json, str) {
let shown = ""
for step in steps {
match step {
Name(n) => {
shown = shown + "." + n
match v {
Obj(members) => {
let found = false
for m in members {
if !found && m.0 == n {
v = m.1
found = true
}
}
if !found {
return Result.Err(shown + ": no such member")
}
}
_ => return Result.Err(shown + ": not an object")
}
}
Index(i) => {
shown = shown + "[" + to_str(i) + "]"
match v {
Arr(items) => {
let k = i
if k < 0 {
k += len(items)
}
if k < 0 || k >= len(items) {
return Result.Err(shown + ": out of range, the array has " + to_str(len(items)))
}
v = items[k]
}
_ => return Result.Err(shown + ": not an array")
}
}
}
}
return Result.Ok(v)
}
// ---- the command ----
fn usage() -> i64 {
files.eprint("usage: json FILE [PATH] [-o OUT]")
return 2
}
fn main() -> i64 {
let a = args()
let input = ""
let path = "."
let output = ""
let rest: [str] = []
let i = 0
while i < len(a) {
if a[i] == "-o" {
if i + 1 == len(a) {
return usage()
}
output = a[i + 1]
i += 2
} else {
push(rest, a[i])
i += 1
}
}
if len(rest) == 0 || len(rest) > 2 {
return usage()
}
input = rest[0]
if len(rest) == 2 {
path = rest[1]
}
let steps: [Step] = []
match parse_path(path) {
Ok(s) => steps = s
Err(msg) => {
files.eprint(msg)
return usage()
}
}
match files.read(input) {
Err(msg) => {
files.eprint(msg)
return 1
}
Ok(text) => match parse(text) {
Err(msg) => {
files.eprint(input + ":" + msg)
return 1
}
Ok(doc) => match at(doc, steps) {
Err(msg) => {
files.eprint(msg)
return 1
}
Ok(part) => {
let out = render(part)
if output == "" {
// `print` adds the last newline.
print(out[:len(out) - 1])
return 0
}
match files.write(output, out) {
Ok(()) => return 0
Err(msg) => {
files.eprint(msg)
return 1
}
}
}
}
}
}
}

View on GitHub