├── .gitignore ├── .travis.yml ├── AUTHORS ├── COPYING ├── ChangeLog ├── Dockerfile ├── Makefile.am ├── NEWS ├── README ├── README.md ├── base64.c ├── base64.h ├── build-aux └── tap-driver.sh ├── configure.ac ├── debian ├── changelog ├── compat ├── control ├── copyright ├── dirs ├── docs ├── jo.manpages ├── rules └── source │ └── format ├── jo.1 ├── jo.bash ├── jo.c ├── jo.md ├── jo.pandoc ├── jo.zsh ├── json.c ├── json.h ├── meson.build ├── press.md ├── rpm-build └── jo.spec ├── snapcraft.yaml └── tests ├── jo-creator.txt ├── jo-large1.json ├── jo-large2.json ├── jo-logo.png ├── jo.01.exp ├── jo.01.sh ├── jo.02.exp ├── jo.02.sh ├── jo.03.exp ├── jo.03.sh ├── jo.04.exp ├── jo.04.sh ├── jo.05.exp ├── jo.05.sh ├── jo.06.exp ├── jo.06.sh ├── jo.07.sh.in ├── jo.08.exp ├── jo.08.sh ├── jo.09.exp ├── jo.09.sh ├── jo.10.exp ├── jo.10.sh ├── jo.11.exp ├── jo.11.sh ├── jo.12.exp ├── jo.12.sh ├── jo.13.exp ├── jo.13.sh ├── jo.14.exp ├── jo.14.sh ├── jo.15.exp ├── jo.15.sh ├── jo.16.exp ├── jo.16.sh ├── jo.17.exp ├── jo.17.sh ├── jo.18.exp ├── jo.18.sh ├── jo.19.exp ├── jo.19.sh ├── jo.20.exp ├── jo.20.sh ├── jo.21.exp ├── jo.21.sh ├── jo.22.exp ├── jo.22.sh ├── jo.23.exp ├── jo.23.sh ├── jo.24.exp ├── jo.24.sh ├── jo.25.exp ├── jo.25.sh ├── jo.26.exp ├── jo.26.sh ├── jo.27.exp ├── jo.27.sh └── jo.test /.gitignore: -------------------------------------------------------------------------------- 1 | *.dSYM 2 | .DS_Store 3 | *.o 4 | *.so 5 | *.la 6 | *.lai 7 | *.so.* 8 | 9 | # Autotools junk 10 | .libs 11 | .deps 12 | .dirstamp 13 | libtool 14 | *.log 15 | stamp-h1 16 | config.log 17 | config.status 18 | autom4te.cache 19 | INSTALL 20 | Makefile 21 | jo-*.tar.gz 22 | configure 23 | aclocal.m4 24 | Makefile.in 25 | version.h 26 | config.cache 27 | m4/libtool.m4 28 | m4/ltoptions.m4 29 | m4/ltsugar.m4 30 | m4/ltversion.m4 31 | m4/lt~obsolete.m4 32 | *.trs 33 | install-sh 34 | depcomp 35 | missing 36 | build-aux/compile 37 | build-aux/depcomp 38 | build-aux/install-sh 39 | build-aux/missing 40 | 41 | tests/jo.07.sh 42 | jo 43 | g 44 | 45 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | 5 | arch: 6 | - amd64 7 | - ppc64le 8 | sudo: false 9 | language: c 10 | compiler: 11 | - gcc 12 | - clang 13 | before_install: 14 | - echo $TRAVIS_OS_NAME 15 | - uname -a 16 | - echo $LANG 17 | - echo $LC_ALL 18 | before_script: 19 | - autoreconf -i 20 | - ./configure 21 | script: 22 | - make check 23 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Jan-Piet Mens 2 | Adrian Ho 3 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2016 Jan-Piet Mens 3 | * 4 | * This program is free software; you can redistribute it and/or 5 | * modify it under the terms of the GNU General Public License 6 | * as published by the Free Software Foundation; either version 2 7 | * of the License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program; if not, see 16 | * . 17 | */ 18 | 19 | #-------------------------------------------------------------------- 20 | json.[ch] 21 | 22 | /* 23 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 24 | All rights reserved. 25 | 26 | Permission is hereby granted, free of charge, to any person obtaining a copy 27 | of this software and associated documentation files (the "Software"), to deal 28 | in the Software without restriction, including without limitation the rights 29 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 30 | copies of the Software, and to permit persons to whom the Software is 31 | furnished to do so, subject to the following conditions: 32 | 33 | The above copyright notice and this permission notice shall be included in 34 | all copies or substantial portions of the Software. 35 | 36 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 37 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 38 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 39 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 40 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 41 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 42 | THE SOFTWARE. 43 | */ 44 | 45 | -------------------------------------------------------------------------------- /ChangeLog: -------------------------------------------------------------------------------- 1 | 2 | 2022-11-04 1.9 3 | - FIX: fix reading of long lines from stdin (mk 2) (#195) 4 | - FIX: add missing test files (#196) 5 | 6 | 2022-11-04 1.8 7 | - FIX: fix reading of long lines from stdin (mk 2) (#195) 8 | 9 | 2022-10-29 1.7 10 | - FIX: fix warnings on Windows build and reimplement err{x,} functions (#193) 11 | - FIX: fix reading of long lines from stdin and refactor slurp_file() in the process. (#192) 12 | - NEW: Add option -o outfile for when not run from a shell and redirect '>' is not an option. (#189) 13 | - FIX: fix tables in jo.md (#167) 14 | 15 | 2022-01-05 1.6 16 | 17 | - FIX: repair tests broken by AUTHORS change (#164) 18 | - FIX: repair make distcheck by removing copied _jo zsh functions 19 | 20 | 2022-01-04 1.5 21 | 22 | - NEW: replace asserts with human errors (#162) 23 | - NEW: zsh completion (#158) 24 | - FIX: stdin filter on Windows (# 25 | - FIX: several cleanups 26 | - NEW: Meson build 27 | - UPD: snap to newer base (#149) 28 | - NEW: option to deduplicate keys (#143, #145) 29 | - NEW: Filter functionality (#141) 30 | - FIX: file embedding 31 | - FIX: add missing tests to Makefile.am 32 | 33 | 2020-07-18 1.4 34 | 35 | - FIX: Coercion flag logic now permits getopt(3) double-dash 36 | - FIX: Documentation clarifies special characters 37 | - FIX: Jo builds on snap builds (#110) 38 | - FIX: Jo builds on systems with slightly older pkg-config (#107) 39 | 40 | 41 | 2019-11-04 1.3 42 | - FIX: Escaped @ ("\@") is treated as "@" (#42, #103) 43 | - NEW: Support reading JSON array elements (#91) 44 | - UPD: Add home and removable-media interfaces to snap (#94) 45 | - FIX: fix unlikely crash after malloc fail when base64 encoding. 46 | - NEW: Support reading nested data from pipes (#82) 47 | 48 | 2018-12-10 1.2 49 | - NEW: Dockerfile (#76) 50 | - UPD: add examples of empty arrays/objects to manual (#74) 51 | - NEW: support -e to ignore empty stdin; contributed by Robi Karp 52 | - NEW: object-path support (#57) 53 | 54 | 2017-05-18 1.1 55 | - NEW: type coercion (#55) 56 | - FIX: quotes in quotes and double quotes at begin of string (#47) 57 | - FIX: catch null value in assignmen (#46) 58 | - NEW: support for key:=file.json for reading object values from a file (#43) 59 | - NEW: PPA contributed by Ross Duggan in #32 60 | - FIX: "null" is now handled like we handle "true" and "false"; disable with -B 61 | - NEW: more tests in the test suite 62 | 63 | 2016-03-11 1.0 64 | - NEW: read JSON element values from files (#22) 65 | - FIX: usage diagnostic 66 | - NEW: add support for OpenBSD pledge(2) (#21) 67 | 68 | 2016-03-10 0.9 69 | - UPD: revert support for $JO_PRETTY et. al; it was a bad idea 70 | 71 | 2016-03-10 0.8 72 | - UPD: new test suite 73 | - NEW: support for nested elements (#19) 74 | - NEW: if $JO_PRETTY is set, jo will always pretty-print 75 | - NEW: Define $JO_SPACER to any desired number of spaces or tabs for pretty-printing (#18) 76 | 77 | 2016-03-09 0.7 78 | - NEW: strings "true"/"false" now default to booleans; avoid with -B (#17) 79 | - FIX: test.sh get quotes to prevent failures with pdksh (#16) 80 | - FIX: pretty-print Version if requested (#15) 81 | - FIX: Add cast to suppress warning when compiling with GCC 4.8.4 (#14) 82 | 83 | 2016-03-08 0.6 84 | - FIX: make build work on CentOS 6 (#13) 85 | - NEW: JSONy version with -V 86 | 87 | 2016-03-08 0.5 88 | - FIX: fileno error (#12) 89 | 90 | 2016-03-08 0.4 91 | - NEW: Win32 support for CJK contributed by @mattn 92 | 93 | 2016-03-08 0.3 94 | - NEW: autotools support 95 | - NEW: option -v 96 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine AS builder 2 | RUN apk -U add automake autoconf build-base make pkgconf 3 | COPY . /src 4 | WORKDIR /src 5 | RUN autoreconf -i && ./configure && make check && make install 6 | 7 | FROM alpine 8 | COPY --from=builder /usr/local/bin/jo /bin/jo 9 | ENTRYPOINT ["/bin/jo"] 10 | -------------------------------------------------------------------------------- /Makefile.am: -------------------------------------------------------------------------------- 1 | 2 | AM_CFLAGS = -Wall -O2 3 | 4 | bin_PROGRAMS = jo 5 | jo_SOURCES = jo.c json.c json.h base64.c base64.h 6 | jo_EXTRA = jo.pandoc 7 | dist_man_MANS = jo.1 8 | jo_LDADD = -lm 9 | 10 | bashcompdir = @bashcompdir@ 11 | dist_bashcomp_DATA = jo.bash 12 | 13 | zshcompdir = $(datadir)/zsh/site-functions 14 | dist_zshcomp_DATA = jo.zsh 15 | install-data-hook: 16 | mv -f $(DESTDIR)$(zshcompdir)/jo.zsh $(DESTDIR)$(zshcompdir)/_jo 17 | 18 | uninstall-local: 19 | rm -f $(DESTDIR)$(zshcompdir)/_jo 20 | 21 | if USE_PANDOC 22 | # Add targets to rebuild pages 23 | jo.1: jo.pandoc 24 | @test -n "$(PANDOC)" || \ 25 | { echo 'pandoc' not found during configure.; exit 1; } 26 | $(PANDOC) -s -w man -f markdown -o $@ $< 27 | 28 | jo.md: jo.pandoc 29 | @test -n "$(PANDOC)" || \ 30 | { echo 'pandoc' not found during configure.; exit 1; } 31 | $(PANDOC) -s -w gfm -f markdown-smart -o $@ $< 32 | 33 | endif 34 | 35 | # docdir = $(datadir)/doc/@PACKAGE@ 36 | # doc_DATA = README.md 37 | 38 | # If on OS/X, fail if $COPYFILE_DISABLE is not in the environment 39 | # so that tar doesn't bundle the AppleDouble attributes 40 | 41 | dist-hook: 42 | if test $$(uname -s) = "Darwin" -a "x$$COPYFILE_DISABLE" = "x"; then echo "Set COPYFILE_DISABLE before making dist" >&2; exit 2; fi 43 | 44 | TEST_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) \ 45 | $(top_srcdir)/build-aux/tap-driver.sh 46 | 47 | TESTS = tests/jo.test 48 | 49 | EXTRA_DIST = $(jo_EXTRA) \ 50 | $(TESTS) \ 51 | tests/jo.01.sh tests/jo.01.exp \ 52 | tests/jo.02.sh tests/jo.02.exp \ 53 | tests/jo.03.sh tests/jo.03.exp \ 54 | tests/jo.04.sh tests/jo.04.exp \ 55 | tests/jo.05.sh tests/jo.05.exp \ 56 | tests/jo.06.sh tests/jo.06.exp \ 57 | tests/jo.07.sh.in \ 58 | tests/jo.08.sh tests/jo.08.exp \ 59 | tests/jo.09.sh tests/jo.09.exp \ 60 | tests/jo.10.sh tests/jo.10.exp \ 61 | tests/jo.11.sh tests/jo.11.exp \ 62 | tests/jo.12.sh tests/jo.12.exp \ 63 | tests/jo.13.sh tests/jo.13.exp tests/jo-logo.png \ 64 | tests/jo.14.sh tests/jo.14.exp \ 65 | tests/jo.15.sh tests/jo.15.exp \ 66 | tests/jo.16.sh tests/jo.16.exp \ 67 | tests/jo.17.sh tests/jo.17.exp tests/jo-creator.txt \ 68 | tests/jo.18.sh tests/jo.18.exp \ 69 | tests/jo.19.sh tests/jo.19.exp \ 70 | tests/jo.20.sh tests/jo.20.exp tests/jo-large1.json tests/jo-large2.json \ 71 | tests/jo.21.sh tests/jo.21.exp \ 72 | tests/jo.22.sh tests/jo.22.exp \ 73 | tests/jo.23.sh tests/jo.23.exp \ 74 | tests/jo.24.sh tests/jo.24.exp \ 75 | tests/jo.25.sh tests/jo.25.exp \ 76 | tests/jo.26.sh tests/jo.26.exp \ 77 | tests/jo.27.sh tests/jo.27.exp 78 | -------------------------------------------------------------------------------- /NEWS: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmens/jo/d7b01392cc1a6bc381741cdbc062b06403bbdb91/NEWS -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | README.md -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jo 2 | 3 | ![jo logo](tests/jo-logo.png) 4 | 5 | This is `jo`, a small utility to create JSON objects 6 | 7 | ```bash 8 | $ jo -p name=jo n=17 parser=false 9 | { 10 | "name": "jo", 11 | "n": 17, 12 | "parser": false 13 | } 14 | ``` 15 | 16 | or arrays 17 | 18 | ```bash 19 | $ seq 1 10 | jo -a 20 | [1,2,3,4,5,6,7,8,9,10] 21 | ``` 22 | 23 | It has a [manual](jo.md), and you can read [why I wrote jo](http://jpmens.net/2016/03/05/a-shell-command-to-create-json-jo/). 24 | 25 | ## Build from Release tarball 26 | 27 | To build from [a release](https://github.com/jpmens/jo/releases) you will need a C compiler to install from a source tarball which you download from the [Releases page](https://github.com/jpmens/jo/releases). 28 | 29 | ```bash 30 | tar xvzf jo-1.3.tar.gz 31 | cd jo-1.3 32 | autoreconf -i 33 | ./configure 34 | make check 35 | make install 36 | ``` 37 | 38 | 39 | ## Build from Github 40 | 41 | [![Build Status](https://api.travis-ci.com/jpmens/jo.svg?branch=master)](https://travis-ci.com/github/jpmens/jo) 42 | 43 | To install from the repository, you will need a C compiler as well as a relatively recent version of _automake_ and _autoconf_. 44 | 45 | ```bash 46 | git clone https://github.com/jpmens/jo.git 47 | cd jo 48 | autoreconf -i 49 | ./configure 50 | make check 51 | make install 52 | ``` 53 | 54 | ## Install 55 | 56 | ### Homebrew 57 | 58 | ```bash 59 | brew install jo 60 | ``` 61 | 62 | ### MacPorts 63 | 64 | ```bash 65 | sudo port install jo 66 | ``` 67 | 68 | ### Ubuntu 69 | 70 | ``` 71 | apt-get install jo 72 | ``` 73 | 74 | ### Gentoo 75 | 76 | ``` 77 | emerge jo 78 | ``` 79 | 80 | ### Fedora 81 | 82 | ``` 83 | dnf install jo 84 | ``` 85 | 86 | ### Snap 87 | 88 | Thanks to [Roger Light](https://twitter.com/ralight/status/1166023769623867398), _jo_ is available as a [snap package](https://snapcraft.io/jo). Use `snap install jo` from a Linux distro that supports snaps. 89 | 90 | ### Windows 91 | ```cmd 92 | scoop install jo 93 | ``` 94 | 95 | ### Windows WSL2 96 | 97 | As shown in [#175](https://github.com/jpmens/jo/issues/175) when using _git_ on Windows WSL2 it should be necessary to disable automatic CRLF conversion in _git_ or the tests will fail: 98 | 99 | ```cmd 100 | git config --local core.autocrlf false 101 | ``` 102 | 103 | ### AIX 104 | 105 | _jo_ builds and passes all tests on AIX 7.1 using the _autoconf_, _automake_, _gcc_, and _pkg-config_ RPMs from IBM's [AIX Toolbox for Open Source Software](https://www.ibm.com/support/pages/node/883796). The _xlclang_ compiler from IBM's xlC/C++ suite for AIX will also build _jo_. 106 | 107 | ## Others 108 | 109 | * [voidlinux](https://github.com/voidlinux/void-packages/tree/master/srcpkgs/jo) 110 | * [ArchLinux](https://archlinux.org/packages/extra/x86_64/jo/) 111 | * [OpenBSD](http://openports.se/textproc/jo) 112 | * [FreeBSD](https://www.freshports.org/textproc/jo) 113 | * [Guix](https://packages.guix.gnu.org/packages/jo/) 114 | * [pkgsrc](http://pkgsrc.se/textproc/jo) 115 | * [repology.org](https://repology.org/metapackage/jo/versions) 116 | * [Docker](https://hub.docker.com/repository/docker/jpmens/jo) 117 | 118 | ## See also 119 | 120 | * [gjo](https://github.com/skanehira/gjo) 121 | * [rjo](https://github.com/dskkato/rjo) 122 | * [jjo](https://github.com/memoryhole/jjo) 123 | * [jf](https://github.com/sayanarijit/jf) 124 | 125 | ## Credits 126 | 127 | * `json.[ch]` by 2011 Joseph A. Adams (joeyadams3.14159[at]gmail.com). 128 | -------------------------------------------------------------------------------- /base64.c: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | This code is public domain software. 4 | 5 | */ 6 | 7 | #include "base64.h" 8 | 9 | #include 10 | #include 11 | 12 | // base64 encoding 13 | // 14 | // buf: binary input data 15 | // size: size of input (bytes) 16 | // return: base64-encoded string (null-terminated) 17 | // memory for output will be allocated here, free it later 18 | // 19 | char* base64_encode(const void* buf, size_t size) 20 | { 21 | static const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 22 | 23 | char* str = (char*) malloc((size+3)*4/3 + 1); 24 | 25 | char* p = str; 26 | const unsigned char* q = (const unsigned char*) buf; 27 | size_t i = 0; 28 | 29 | if (str == NULL) { 30 | return NULL; 31 | } 32 | 33 | while (i < size) { 34 | int c = q[i++]; 35 | c *= 256; 36 | if (i < size) 37 | c += q[i]; 38 | i++; 39 | 40 | c *= 256; 41 | if (i < size) 42 | c += q[i]; 43 | i++; 44 | 45 | *p++ = base64[(c & 0x00fc0000) >> 18]; 46 | *p++ = base64[(c & 0x0003f000) >> 12]; 47 | 48 | if (i > size + 1) 49 | *p++ = '='; 50 | else 51 | *p++ = base64[(c & 0x00000fc0) >> 6]; 52 | 53 | if (i > size) 54 | *p++ = '='; 55 | else 56 | *p++ = base64[c & 0x0000003f]; 57 | } 58 | 59 | *p = 0; 60 | 61 | return str; 62 | } 63 | 64 | 65 | // single base64 character conversion 66 | // 67 | static int POS(char c) 68 | { 69 | if (c>='A' && c<='Z') return c - 'A'; 70 | if (c>='a' && c<='z') return c - 'a' + 26; 71 | if (c>='0' && c<='9') return c - '0' + 52; 72 | if (c == '+') return 62; 73 | if (c == '/') return 63; 74 | if (c == '=') return -1; 75 | return -2; 76 | } 77 | 78 | // base64 decoding 79 | // 80 | // s: base64 string, must be null-terminated 81 | // data: output buffer for decoded data 82 | // data_len size of decoded data 83 | // return: allocated data buffer 84 | // 85 | void* base64_decode(const char* s, size_t *data_len) 86 | { 87 | const char *p; 88 | unsigned char *q, *data; 89 | int n[4] = { 0, 0, 0, 0 }; 90 | 91 | size_t len = strlen(s); 92 | if (len % 4) 93 | return NULL; 94 | data = (unsigned char*) malloc(len/4*3); 95 | q = (unsigned char*) data; 96 | 97 | for (p = s; *p; ) { 98 | n[0] = POS(*p++); 99 | n[1] = POS(*p++); 100 | n[2] = POS(*p++); 101 | n[3] = POS(*p++); 102 | 103 | if (n[0] == -2 || n[1] == -2 || n[2] == -2 || n[3] == -2) 104 | return NULL; 105 | 106 | if (n[0] == -1 || n[1] == -1) 107 | return NULL; 108 | 109 | if (n[2] == -1 && n[3] != -1) 110 | return NULL; 111 | 112 | q[0] = (n[0] << 2) + (n[1] >> 4); 113 | if (n[2] != -1) 114 | q[1] = ((n[1] & 15) << 4) + (n[2] >> 2); 115 | if (n[3] != -1) 116 | q[2] = ((n[2] & 3) << 6) + n[3]; 117 | q += 3; 118 | } 119 | 120 | *data_len = q-data - (n[2]==-1) - (n[3]==-1); 121 | 122 | return data; 123 | } 124 | -------------------------------------------------------------------------------- /base64.h: -------------------------------------------------------------------------------- 1 | #ifndef BASE64_H_GUARD 2 | #define BASE64_H_GUARD 3 | 4 | #include 5 | 6 | char* base64_encode(const void* buf, size_t size); 7 | void* base64_decode(const char* s, size_t *data_len); 8 | 9 | #endif 10 | -------------------------------------------------------------------------------- /build-aux/tap-driver.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | # Copyright (C) 2011-2013 Free Software Foundation, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2, or (at your option) 7 | # any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program. If not, see . 16 | 17 | # As a special exception to the GNU General Public License, if you 18 | # distribute this file as part of a program that contains a 19 | # configuration script generated by Autoconf, you may include it under 20 | # the same distribution terms that you use for the rest of that program. 21 | 22 | # This file is maintained in Automake, please report 23 | # bugs to or send patches to 24 | # . 25 | 26 | scriptversion=2011-12-27.17; # UTC 27 | 28 | # Make unconditional expansion of undefined variables an error. This 29 | # helps a lot in preventing typo-related bugs. 30 | set -u 31 | 32 | me=tap-driver.sh 33 | 34 | fatal () 35 | { 36 | echo "$me: fatal: $*" >&2 37 | exit 1 38 | } 39 | 40 | usage_error () 41 | { 42 | echo "$me: $*" >&2 43 | print_usage >&2 44 | exit 2 45 | } 46 | 47 | print_usage () 48 | { 49 | cat < 136 | # 137 | trap : 1 3 2 13 15 138 | if test $merge -gt 0; then 139 | exec 2>&1 140 | else 141 | exec 2>&3 142 | fi 143 | "$@" 144 | echo $? 145 | ) | LC_ALL=C ${AM_TAP_AWK-awk} \ 146 | -v me="$me" \ 147 | -v test_script_name="$test_name" \ 148 | -v log_file="$log_file" \ 149 | -v trs_file="$trs_file" \ 150 | -v expect_failure="$expect_failure" \ 151 | -v merge="$merge" \ 152 | -v ignore_exit="$ignore_exit" \ 153 | -v comments="$comments" \ 154 | -v diag_string="$diag_string" \ 155 | ' 156 | # FIXME: the usages of "cat >&3" below could be optimized when using 157 | # FIXME: GNU awk, and/on on systems that supports /dev/fd/. 158 | 159 | # Implementation note: in what follows, `result_obj` will be an 160 | # associative array that (partly) simulates a TAP result object 161 | # from the `TAP::Parser` perl module. 162 | 163 | ## ----------- ## 164 | ## FUNCTIONS ## 165 | ## ----------- ## 166 | 167 | function fatal(msg) 168 | { 169 | print me ": " msg | "cat >&2" 170 | exit 1 171 | } 172 | 173 | function abort(where) 174 | { 175 | fatal("internal error " where) 176 | } 177 | 178 | # Convert a boolean to a "yes"/"no" string. 179 | function yn(bool) 180 | { 181 | return bool ? "yes" : "no"; 182 | } 183 | 184 | function add_test_result(result) 185 | { 186 | if (!test_results_index) 187 | test_results_index = 0 188 | test_results_list[test_results_index] = result 189 | test_results_index += 1 190 | test_results_seen[result] = 1; 191 | } 192 | 193 | # Whether the test script should be re-run by "make recheck". 194 | function must_recheck() 195 | { 196 | for (k in test_results_seen) 197 | if (k != "XFAIL" && k != "PASS" && k != "SKIP") 198 | return 1 199 | return 0 200 | } 201 | 202 | # Whether the content of the log file associated to this test should 203 | # be copied into the "global" test-suite.log. 204 | function copy_in_global_log() 205 | { 206 | for (k in test_results_seen) 207 | if (k != "PASS") 208 | return 1 209 | return 0 210 | } 211 | 212 | # FIXME: this can certainly be improved ... 213 | function get_global_test_result() 214 | { 215 | if ("ERROR" in test_results_seen) 216 | return "ERROR" 217 | if ("FAIL" in test_results_seen || "XPASS" in test_results_seen) 218 | return "FAIL" 219 | all_skipped = 1 220 | for (k in test_results_seen) 221 | if (k != "SKIP") 222 | all_skipped = 0 223 | if (all_skipped) 224 | return "SKIP" 225 | return "PASS"; 226 | } 227 | 228 | function stringify_result_obj(result_obj) 229 | { 230 | if (result_obj["is_unplanned"] || result_obj["number"] != testno) 231 | return "ERROR" 232 | 233 | if (plan_seen == LATE_PLAN) 234 | return "ERROR" 235 | 236 | if (result_obj["directive"] == "TODO") 237 | return result_obj["is_ok"] ? "XPASS" : "XFAIL" 238 | 239 | if (result_obj["directive"] == "SKIP") 240 | return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL; 241 | 242 | if (length(result_obj["directive"])) 243 | abort("in function stringify_result_obj()") 244 | 245 | return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL 246 | } 247 | 248 | function decorate_result(result) 249 | { 250 | color_name = color_for_result[result] 251 | if (color_name) 252 | return color_map[color_name] "" result "" color_map["std"] 253 | # If we are not using colorized output, or if we do not know how 254 | # to colorize the given result, we should return it unchanged. 255 | return result 256 | } 257 | 258 | function report(result, details) 259 | { 260 | if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/) 261 | { 262 | msg = ": " test_script_name 263 | add_test_result(result) 264 | } 265 | else if (result == "#") 266 | { 267 | msg = " " test_script_name ":" 268 | } 269 | else 270 | { 271 | abort("in function report()") 272 | } 273 | if (length(details)) 274 | msg = msg " " details 275 | # Output on console might be colorized. 276 | print decorate_result(result) msg 277 | # Log the result in the log file too, to help debugging (this is 278 | # especially true when said result is a TAP error or "Bail out!"). 279 | print result msg | "cat >&3"; 280 | } 281 | 282 | function testsuite_error(error_message) 283 | { 284 | report("ERROR", "- " error_message) 285 | } 286 | 287 | function handle_tap_result() 288 | { 289 | details = result_obj["number"]; 290 | if (length(result_obj["description"])) 291 | details = details " " result_obj["description"] 292 | 293 | if (plan_seen == LATE_PLAN) 294 | { 295 | details = details " # AFTER LATE PLAN"; 296 | } 297 | else if (result_obj["is_unplanned"]) 298 | { 299 | details = details " # UNPLANNED"; 300 | } 301 | else if (result_obj["number"] != testno) 302 | { 303 | details = sprintf("%s # OUT-OF-ORDER (expecting %d)", 304 | details, testno); 305 | } 306 | else if (result_obj["directive"]) 307 | { 308 | details = details " # " result_obj["directive"]; 309 | if (length(result_obj["explanation"])) 310 | details = details " " result_obj["explanation"] 311 | } 312 | 313 | report(stringify_result_obj(result_obj), details) 314 | } 315 | 316 | # `skip_reason` should be empty whenever planned > 0. 317 | function handle_tap_plan(planned, skip_reason) 318 | { 319 | planned += 0 # Avoid getting confused if, say, `planned` is "00" 320 | if (length(skip_reason) && planned > 0) 321 | abort("in function handle_tap_plan()") 322 | if (plan_seen) 323 | { 324 | # Error, only one plan per stream is acceptable. 325 | testsuite_error("multiple test plans") 326 | return; 327 | } 328 | planned_tests = planned 329 | # The TAP plan can come before or after *all* the TAP results; we speak 330 | # respectively of an "early" or a "late" plan. If we see the plan line 331 | # after at least one TAP result has been seen, assume we have a late 332 | # plan; in this case, any further test result seen after the plan will 333 | # be flagged as an error. 334 | plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN) 335 | # If testno > 0, we have an error ("too many tests run") that will be 336 | # automatically dealt with later, so do not worry about it here. If 337 | # $plan_seen is true, we have an error due to a repeated plan, and that 338 | # has already been dealt with above. Otherwise, we have a valid "plan 339 | # with SKIP" specification, and should report it as a particular kind 340 | # of SKIP result. 341 | if (planned == 0 && testno == 0) 342 | { 343 | if (length(skip_reason)) 344 | skip_reason = "- " skip_reason; 345 | report("SKIP", skip_reason); 346 | } 347 | } 348 | 349 | function extract_tap_comment(line) 350 | { 351 | if (index(line, diag_string) == 1) 352 | { 353 | # Strip leading `diag_string` from `line`. 354 | line = substr(line, length(diag_string) + 1) 355 | # And strip any leading and trailing whitespace left. 356 | sub("^[ \t]*", "", line) 357 | sub("[ \t]*$", "", line) 358 | # Return what is left (if any). 359 | return line; 360 | } 361 | return ""; 362 | } 363 | 364 | # When this function is called, we know that line is a TAP result line, 365 | # so that it matches the (perl) RE "^(not )?ok\b". 366 | function setup_result_obj(line) 367 | { 368 | # Get the result, and remove it from the line. 369 | result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0) 370 | sub("^(not )?ok[ \t]*", "", line) 371 | 372 | # If the result has an explicit number, get it and strip it; otherwise, 373 | # automatically assing the next progresive number to it. 374 | if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/) 375 | { 376 | match(line, "^[0-9]+") 377 | # The final `+ 0` is to normalize numbers with leading zeros. 378 | result_obj["number"] = substr(line, 1, RLENGTH) + 0 379 | line = substr(line, RLENGTH + 1) 380 | } 381 | else 382 | { 383 | result_obj["number"] = testno 384 | } 385 | 386 | if (plan_seen == LATE_PLAN) 387 | # No further test results are acceptable after a "late" TAP plan 388 | # has been seen. 389 | result_obj["is_unplanned"] = 1 390 | else if (plan_seen && testno > planned_tests) 391 | result_obj["is_unplanned"] = 1 392 | else 393 | result_obj["is_unplanned"] = 0 394 | 395 | # Strip trailing and leading whitespace. 396 | sub("^[ \t]*", "", line) 397 | sub("[ \t]*$", "", line) 398 | 399 | # This will have to be corrected if we have a "TODO"/"SKIP" directive. 400 | result_obj["description"] = line 401 | result_obj["directive"] = "" 402 | result_obj["explanation"] = "" 403 | 404 | if (index(line, "#") == 0) 405 | return # No possible directive, nothing more to do. 406 | 407 | # Directives are case-insensitive. 408 | rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*" 409 | 410 | # See whether we have the directive, and if yes, where. 411 | pos = match(line, rx "$") 412 | if (!pos) 413 | pos = match(line, rx "[^a-zA-Z0-9_]") 414 | 415 | # If there was no TAP directive, we have nothing more to do. 416 | if (!pos) 417 | return 418 | 419 | # Let`s now see if the TAP directive has been escaped. For example: 420 | # escaped: ok \# SKIP 421 | # not escaped: ok \\# SKIP 422 | # escaped: ok \\\\\# SKIP 423 | # not escaped: ok \ # SKIP 424 | if (substr(line, pos, 1) == "#") 425 | { 426 | bslash_count = 0 427 | for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--) 428 | bslash_count += 1 429 | if (bslash_count % 2) 430 | return # Directive was escaped. 431 | } 432 | 433 | # Strip the directive and its explanation (if any) from the test 434 | # description. 435 | result_obj["description"] = substr(line, 1, pos - 1) 436 | # Now remove the test description from the line, that has been dealt 437 | # with already. 438 | line = substr(line, pos) 439 | # Strip the directive, and save its value (normalized to upper case). 440 | sub("^[ \t]*#[ \t]*", "", line) 441 | result_obj["directive"] = toupper(substr(line, 1, 4)) 442 | line = substr(line, 5) 443 | # Now get the explanation for the directive (if any), with leading 444 | # and trailing whitespace removed. 445 | sub("^[ \t]*", "", line) 446 | sub("[ \t]*$", "", line) 447 | result_obj["explanation"] = line 448 | } 449 | 450 | function get_test_exit_message(status) 451 | { 452 | if (status == 0) 453 | return "" 454 | if (status !~ /^[1-9][0-9]*$/) 455 | abort("getting exit status") 456 | if (status < 127) 457 | exit_details = "" 458 | else if (status == 127) 459 | exit_details = " (command not found?)" 460 | else if (status >= 128 && status <= 255) 461 | exit_details = sprintf(" (terminated by signal %d?)", status - 128) 462 | else if (status > 256 && status <= 384) 463 | # We used to report an "abnormal termination" here, but some Korn 464 | # shells, when a child process die due to signal number n, can leave 465 | # in $? an exit status of 256+n instead of the more standard 128+n. 466 | # Apparently, both behaviours are allowed by POSIX (2008), so be 467 | # prepared to handle them both. See also Austing Group report ID 468 | # 0000051 469 | exit_details = sprintf(" (terminated by signal %d?)", status - 256) 470 | else 471 | # Never seen in practice. 472 | exit_details = " (abnormal termination)" 473 | return sprintf("exited with status %d%s", status, exit_details) 474 | } 475 | 476 | function write_test_results() 477 | { 478 | print ":global-test-result: " get_global_test_result() > trs_file 479 | print ":recheck: " yn(must_recheck()) > trs_file 480 | print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file 481 | for (i = 0; i < test_results_index; i += 1) 482 | print ":test-result: " test_results_list[i] > trs_file 483 | close(trs_file); 484 | } 485 | 486 | BEGIN { 487 | 488 | ## ------- ## 489 | ## SETUP ## 490 | ## ------- ## 491 | 492 | '"$init_colors"' 493 | 494 | # Properly initialized once the TAP plan is seen. 495 | planned_tests = 0 496 | 497 | COOKED_PASS = expect_failure ? "XPASS": "PASS"; 498 | COOKED_FAIL = expect_failure ? "XFAIL": "FAIL"; 499 | 500 | # Enumeration-like constants to remember which kind of plan (if any) 501 | # has been seen. It is important that NO_PLAN evaluates "false" as 502 | # a boolean. 503 | NO_PLAN = 0 504 | EARLY_PLAN = 1 505 | LATE_PLAN = 2 506 | 507 | testno = 0 # Number of test results seen so far. 508 | bailed_out = 0 # Whether a "Bail out!" directive has been seen. 509 | 510 | # Whether the TAP plan has been seen or not, and if yes, which kind 511 | # it is ("early" is seen before any test result, "late" otherwise). 512 | plan_seen = NO_PLAN 513 | 514 | ## --------- ## 515 | ## PARSING ## 516 | ## --------- ## 517 | 518 | is_first_read = 1 519 | 520 | while (1) 521 | { 522 | # Involutions required so that we are able to read the exit status 523 | # from the last input line. 524 | st = getline 525 | if (st < 0) # I/O error. 526 | fatal("I/O error while reading from input stream") 527 | else if (st == 0) # End-of-input 528 | { 529 | if (is_first_read) 530 | abort("in input loop: only one input line") 531 | break 532 | } 533 | if (is_first_read) 534 | { 535 | is_first_read = 0 536 | nextline = $0 537 | continue 538 | } 539 | else 540 | { 541 | curline = nextline 542 | nextline = $0 543 | $0 = curline 544 | } 545 | # Copy any input line verbatim into the log file. 546 | print | "cat >&3" 547 | # Parsing of TAP input should stop after a "Bail out!" directive. 548 | if (bailed_out) 549 | continue 550 | 551 | # TAP test result. 552 | if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) 553 | { 554 | testno += 1 555 | setup_result_obj($0) 556 | handle_tap_result() 557 | } 558 | # TAP plan (normal or "SKIP" without explanation). 559 | else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/) 560 | { 561 | # The next two lines will put the number of planned tests in $0. 562 | sub("^1\\.\\.", "") 563 | sub("[^0-9]*$", "") 564 | handle_tap_plan($0, "") 565 | continue 566 | } 567 | # TAP "SKIP" plan, with an explanation. 568 | else if ($0 ~ /^1\.\.0+[ \t]*#/) 569 | { 570 | # The next lines will put the skip explanation in $0, stripping 571 | # any leading and trailing whitespace. This is a little more 572 | # tricky in truth, since we want to also strip a potential leading 573 | # "SKIP" string from the message. 574 | sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "") 575 | sub("[ \t]*$", ""); 576 | handle_tap_plan(0, $0) 577 | } 578 | # "Bail out!" magic. 579 | # Older versions of prove and TAP::Harness (e.g., 3.17) did not 580 | # recognize a "Bail out!" directive when preceded by leading 581 | # whitespace, but more modern versions (e.g., 3.23) do. So we 582 | # emulate the latter, "more modern" behaviour. 583 | else if ($0 ~ /^[ \t]*Bail out!/) 584 | { 585 | bailed_out = 1 586 | # Get the bailout message (if any), with leading and trailing 587 | # whitespace stripped. The message remains stored in `$0`. 588 | sub("^[ \t]*Bail out![ \t]*", ""); 589 | sub("[ \t]*$", ""); 590 | # Format the error message for the 591 | bailout_message = "Bail out!" 592 | if (length($0)) 593 | bailout_message = bailout_message " " $0 594 | testsuite_error(bailout_message) 595 | } 596 | # Maybe we have too look for dianogtic comments too. 597 | else if (comments != 0) 598 | { 599 | comment = extract_tap_comment($0); 600 | if (length(comment)) 601 | report("#", comment); 602 | } 603 | } 604 | 605 | ## -------- ## 606 | ## FINISH ## 607 | ## -------- ## 608 | 609 | # A "Bail out!" directive should cause us to ignore any following TAP 610 | # error, as well as a non-zero exit status from the TAP producer. 611 | if (!bailed_out) 612 | { 613 | if (!plan_seen) 614 | { 615 | testsuite_error("missing test plan") 616 | } 617 | else if (planned_tests != testno) 618 | { 619 | bad_amount = testno > planned_tests ? "many" : "few" 620 | testsuite_error(sprintf("too %s tests run (expected %d, got %d)", 621 | bad_amount, planned_tests, testno)) 622 | } 623 | if (!ignore_exit) 624 | { 625 | # Fetch exit status from the last line. 626 | exit_message = get_test_exit_message(nextline) 627 | if (exit_message) 628 | testsuite_error(exit_message) 629 | } 630 | } 631 | 632 | write_test_results() 633 | 634 | exit 0 635 | 636 | } # End of "BEGIN" block. 637 | ' 638 | 639 | # TODO: document that we consume the file descriptor 3 :-( 640 | } 3>"$log_file" 641 | 642 | test $? -eq 0 || fatal "I/O or internal error" 643 | 644 | # Local Variables: 645 | # mode: shell-script 646 | # sh-indentation: 2 647 | # eval: (add-hook 'write-file-hooks 'time-stamp) 648 | # time-stamp-start: "scriptversion=" 649 | # time-stamp-format: "%:y-%02m-%02d.%02H" 650 | # time-stamp-time-zone: "UTC" 651 | # time-stamp-end: "; # UTC" 652 | # End: 653 | -------------------------------------------------------------------------------- /configure.ac: -------------------------------------------------------------------------------- 1 | AC_PREREQ([2.63]) 2 | AC_INIT([jo], [1.9], [jp@mens.de]) 3 | AC_CONFIG_AUX_DIR([build-aux]) 4 | AC_CONFIG_SRCDIR([jo.c]) 5 | 6 | # Checks for programs. 7 | AC_PROG_CC 8 | 9 | # Checks for header files. 10 | AC_USE_SYSTEM_EXTENSIONS 11 | AC_CHECK_HEADERS([stddef.h stdint.h stdlib.h string.h unistd.h stdbool.h]) 12 | 13 | # Checks for library functions. 14 | # AC_FUNC_MALLOC 15 | # AC_FUNC_REALLOC 16 | AC_FUNC_STRTOD 17 | AC_CHECK_FUNCS([strchr strrchr strlcpy strlcat snprintf pledge err errx]) 18 | 19 | # backport PKG_CHECK_VAR from pkgconfig 0.29 20 | m4_ifndef([PKG_CHECK_VAR], [AC_DEFUN([PKG_CHECK_VAR], 21 | [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl 22 | AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl 23 | 24 | _PKG_CONFIG([$1], [variable="][$3]["], [$2]) 25 | AS_VAR_COPY([$1], [pkg_cv_][$1]) 26 | 27 | AS_VAR_IF([$1], [""], [$5], [$4])dnl 28 | ])dnl PKG_CHECK_VAR 29 | ]) 30 | 31 | 32 | AM_INIT_AUTOMAKE([foreign -Wall]) 33 | AM_SILENT_RULES([yes]) 34 | AC_REQUIRE_AUX_FILE([tap-driver.sh]) 35 | 36 | AC_ARG_VAR(PANDOC, [pandoc path]) 37 | AC_PATH_PROG(PANDOC, [pandoc], []) 38 | if test -z "$PANDOC" 39 | then 40 | AC_MSG_WARN([pandoc not found, man pages rebuild will not be possible]) 41 | fi 42 | AM_CONDITIONAL([USE_PANDOC], [test -n "$PANDOC"]) 43 | 44 | PKG_CHECK_VAR(bashcompdir, [bash-completion], [completionsdir], , 45 | bashcompdir="${sysconfdir}/bash_completion.d") 46 | AC_SUBST(bashcompdir) 47 | 48 | AC_CONFIG_FILES([Makefile tests/jo.07.sh]) 49 | AC_OUTPUT 50 | 51 | echo " 52 | Jo.............: version $PACKAGE_VERSION 53 | Prefix.........: $prefix 54 | C compiler.....: $CC $CFLAGS $CPPFLAGS 55 | Pandoc.........: ${PANDOC:-NONE} 56 | Bash completion: $bashcompdir/jo.bash 57 | 58 | Now type 'make @<:@@:>@' 59 | where the optional is: 60 | all - build all binaries 61 | check - run the tests 62 | install - install everything 63 | " 64 | -------------------------------------------------------------------------------- /debian/changelog: -------------------------------------------------------------------------------- 1 | jo (1.0) trusty; urgency=low 2 | 3 | * Initial packaging for jo 4 | 5 | -- Ross Duggan Sat, 12 Mar 2016 18:30:00 +0100 6 | -------------------------------------------------------------------------------- /debian/compat: -------------------------------------------------------------------------------- 1 | 8 2 | -------------------------------------------------------------------------------- /debian/control: -------------------------------------------------------------------------------- 1 | Source: jo 2 | Section: devel 3 | Priority: optional 4 | Maintainer: Jan-Piet Mens 5 | Build-Depends: debhelper (>= 8.0.0), dh-autoreconf, libtool, autoconf (>= 2.69), make 6 | Standards-Version: 3.9.3 7 | 8 | Package: jo 9 | Architecture: any 10 | Description: A shell command to create JSON 11 | -------------------------------------------------------------------------------- /debian/copyright: -------------------------------------------------------------------------------- 1 | Format: http://dep.debian.net/deps/dep5 2 | Upstream-Name: jo 3 | Source: https://github.com/jpmens/jo 4 | 5 | Files: * 6 | Copyright: Jan-Piet Mens 7 | License: GPLv2 8 | This program is free software; you can redistribute it and/or 9 | modify it under the terms of the GNU General Public License 10 | as published by the Free Software Foundation; either version 2 11 | of the License, or (at your option) any later version. 12 | . 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | . 18 | You should have received a copy of the GNU General Public License 19 | along with this program; if not, see 20 | . 21 | -------------------------------------------------------------------------------- /debian/dirs: -------------------------------------------------------------------------------- 1 | usr/bin 2 | usr/share/man/man1 3 | -------------------------------------------------------------------------------- /debian/docs: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmens/jo/d7b01392cc1a6bc381741cdbc062b06403bbdb91/debian/docs -------------------------------------------------------------------------------- /debian/jo.manpages: -------------------------------------------------------------------------------- 1 | jo.1 2 | -------------------------------------------------------------------------------- /debian/rules: -------------------------------------------------------------------------------- 1 | #!/usr/bin/make -f 2 | # -*- makefile -*- 3 | # Sample debian/rules that uses debhelper. 4 | # This file was originally written by Joey Hess and Craig Small. 5 | # As a special exception, when this file is copied by dh-make into a 6 | # dh-make output file, you may use that output file without restriction. 7 | # This special exception was added by Craig Small in version 0.37 of dh-make. 8 | 9 | # Uncomment this to turn on verbose mode. 10 | #export DH_VERBOSE=1 11 | 12 | %: 13 | dh $@ --with autoreconf 14 | -------------------------------------------------------------------------------- /debian/source/format: -------------------------------------------------------------------------------- 1 | 3.0 (quilt) 2 | -------------------------------------------------------------------------------- /jo.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Automatically generated by Pandoc 2.12 3 | .\" 4 | .TH "JO" "1" "" "User Manuals" "" 5 | .hy 6 | .SH NAME 7 | .PP 8 | jo - JSON output from a shell 9 | .SH SYNOPSIS 10 | .PP 11 | jo [-p] [-a] [-B] [-D] [-e] [-n] [-v] [-V] [-d keydelim] [-f file] 12 | [\[en]] [ [-s|-n|-b] word \&...] 13 | .SH DESCRIPTION 14 | .PP 15 | \f[I]jo\f[R] creates a JSON string on \f[I]stdout\f[R] from 16 | \f[I]word\f[R]s given it as arguments or read from \f[I]stdin\f[R]. 17 | If \f[C]-f\f[R] is specified, \f[I]jo\f[R] first loads the contents of 18 | \f[I]file\f[R] as a JSON object or array, then modifies it with 19 | subsequent \f[I]word\f[R]s before printing the final JSON string to 20 | \f[I]stdout\f[R]. 21 | \f[I]file\f[R] may be specified as \f[C]-\f[R] to read from 22 | \f[I]jo\f[R]\[cq]s standard input; this takes precedence over reading 23 | \f[I]word\f[R]s from \f[I]stdin\f[R]. 24 | .PP 25 | Without option \f[C]-a\f[R] it generates an object whereby each 26 | \f[I]word\f[R] is a \f[C]key=value\f[R] (or \f[C]key\[at]value\f[R]) 27 | pair with \f[I]key\f[R] being the JSON object element and 28 | \f[I]value\f[R] its value. 29 | \f[I]jo\f[R] attempts to guess the type of \f[I]value\f[R] in order to 30 | create number (using \f[I]strtod(3)\f[R]), string, or null values in 31 | JSON. 32 | .PP 33 | A missing or empty \f[I]value\f[R] normally results in an element whose 34 | value is \f[C]null\f[R]. 35 | If \f[C]-n\f[R] is specified, this element is not created. 36 | .PP 37 | \f[I]jo\f[R] normally treats \f[I]key\f[R] as a literal string value. 38 | If the \f[C]-d\f[R] option is specified, \f[I]key\f[R] will be 39 | interpreted as an \f[I]object path\f[R], whose individual components are 40 | separated by the first character of \f[I]keydelim\f[R]. 41 | .PP 42 | \f[I]jo\f[R] normally treats \f[I]value\f[R] as a literal string value, 43 | unless it begins with one of the following characters: 44 | .PP 45 | .TS 46 | tab(@); 47 | l l. 48 | T{ 49 | value 50 | T}@T{ 51 | action 52 | T} 53 | _ 54 | T{ 55 | \[at]file 56 | T}@T{ 57 | substitute the contents of \f[I]file\f[R] as-is 58 | T} 59 | T{ 60 | %file 61 | T}@T{ 62 | substitute the contents of \f[I]file\f[R] in base64-encoded form 63 | T} 64 | T{ 65 | :file 66 | T}@T{ 67 | interpret the contents of \f[I]file\f[R] as JSON, and substitute the 68 | result 69 | T} 70 | .TE 71 | .PP 72 | Escape the special character with a backslash to prevent this 73 | interpretation. 74 | .PP 75 | \f[I]jo\f[R] treats \f[C]key\[at]value\f[R] specifically as boolean JSON 76 | elements: if the value begins with \f[C]T\f[R], \f[C]t\f[R], or the 77 | numeric value is greater than zero, the result is \f[C]true\f[R], else 78 | \f[C]false\f[R]. 79 | .PP 80 | \f[I]jo\f[R] creates an array instead of an object when \f[C]-a\f[R] is 81 | specified. 82 | .PP 83 | When the \f[C]:=\f[R] operator is used in a \f[I]word\f[R], the name to 84 | the right of \f[C]:=\f[R] is a file containing JSON which is parsed and 85 | assigned to the key left of the operator. 86 | The file may be specified as \f[C]-\f[R] to read from \f[I]jo\f[R]\[cq]s 87 | standard input. 88 | .SH TYPE COERCION 89 | .PP 90 | \f[I]jo\f[R]\[cq]s type guesses can be overridden on a per-word basis by 91 | prefixing \f[I]word\f[R] with \f[C]-s\f[R] for \f[I]string\f[R], 92 | \f[C]-n\f[R] for \f[I]number\f[R], or \f[C]-b\f[R] for 93 | \f[I]boolean\f[R]. 94 | The list of \f[I]word\f[R]s \f[I]must\f[R] be prefixed with 95 | \f[C]--\f[R], to indicate to \f[I]jo\f[R] that there are no more global 96 | options. 97 | .PP 98 | Type coercion works as follows: 99 | .PP 100 | .TS 101 | tab(@); 102 | l l l l l. 103 | T{ 104 | word 105 | T}@T{ 106 | -s 107 | T}@T{ 108 | -n 109 | T}@T{ 110 | -b 111 | T}@T{ 112 | default 113 | T} 114 | _ 115 | T{ 116 | a= 117 | T}@T{ 118 | \[lq]a\[rq]:\[dq]\[dq] 119 | T}@T{ 120 | \[lq]a\[rq]:0 121 | T}@T{ 122 | \[lq]a\[rq]:false 123 | T}@T{ 124 | \[lq]a\[rq]:null 125 | T} 126 | T{ 127 | a=string 128 | T}@T{ 129 | \[lq]a\[rq]:\[lq]string\[rq] 130 | T}@T{ 131 | \[lq]a\[rq]:6 132 | T}@T{ 133 | \[lq]a\[rq]:true 134 | T}@T{ 135 | \[lq]a\[rq]:\[lq]string\[rq] 136 | T} 137 | T{ 138 | a=\[dq]quoted\[dq] 139 | T}@T{ 140 | \[lq]a\[rq]:\[lq]\[dq]quoted\[dq]\[rq] 141 | T}@T{ 142 | \[lq]a\[rq]:8 143 | T}@T{ 144 | \[lq]a\[rq]:true 145 | T}@T{ 146 | \[lq]a\[rq]:\[lq]\[dq]quoted\[dq]\[rq] 147 | T} 148 | T{ 149 | a=12345 150 | T}@T{ 151 | \[lq]a\[rq]:\[lq]12345\[rq] 152 | T}@T{ 153 | \[lq]a\[rq]:12345 154 | T}@T{ 155 | \[lq]a\[rq]:true 156 | T}@T{ 157 | \[lq]a\[rq]:12345 158 | T} 159 | T{ 160 | a=true 161 | T}@T{ 162 | \[lq]a\[rq]:\[lq]true\[rq] 163 | T}@T{ 164 | \[lq]a\[rq]:1 165 | T}@T{ 166 | \[lq]a\[rq]:true 167 | T}@T{ 168 | \[lq]a\[rq]:true 169 | T} 170 | T{ 171 | a=false 172 | T}@T{ 173 | \[lq]a\[rq]:\[lq]false\[rq] 174 | T}@T{ 175 | \[lq]a\[rq]:0 176 | T}@T{ 177 | \[lq]a\[rq]:false 178 | T}@T{ 179 | \[lq]a\[rq]:false 180 | T} 181 | T{ 182 | a=null 183 | T}@T{ 184 | \[lq]a\[rq]:\[dq]\[dq] 185 | T}@T{ 186 | \[lq]a\[rq]:0 187 | T}@T{ 188 | \[lq]a\[rq]:false 189 | T}@T{ 190 | \[lq]a\[rq]:null 191 | T} 192 | .TE 193 | .PP 194 | Coercing a non-number string to number outputs the \f[I]length\f[R] of 195 | the string. 196 | .PP 197 | Coercing a non-boolean string to boolean outputs \f[C]false\f[R] if the 198 | string is empty, \f[C]true\f[R] otherwise. 199 | .PP 200 | Type coercion only applies to \f[C]key=value\f[R] words, and individual 201 | words in a \f[C]-a\f[R] array. 202 | Coercing other words has no effect. 203 | .SH EXAMPLES 204 | .PP 205 | Create an object. 206 | Note how the incorrectly-formatted float value becomes a string: 207 | .IP 208 | .nf 209 | \f[C] 210 | $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name=\[dq]JP Mens\[dq] nada= coffee\[at]T 211 | {\[dq]tst\[dq]:1457081292,\[dq]lat\[dq]:12.3456,\[dq]cc\[dq]:\[dq]FR\[dq],\[dq]badfloat\[dq]:\[dq]3.14159.26\[dq],\[dq]name\[dq]:\[dq]JP Mens\[dq],\[dq]nada\[dq]:null,\[dq]coffee\[dq]:true} 212 | \f[R] 213 | .fi 214 | .PP 215 | Pretty-print an array with a list of files in the current directory: 216 | .IP 217 | .nf 218 | \f[C] 219 | $ jo -p -a * 220 | [ 221 | \[dq]Makefile\[dq], 222 | \[dq]README.md\[dq], 223 | \[dq]jo.1\[dq], 224 | \[dq]jo.c\[dq], 225 | \[dq]jo.pandoc\[dq], 226 | \[dq]json.c\[dq], 227 | \[dq]json.h\[dq] 228 | ] 229 | \f[R] 230 | .fi 231 | .PP 232 | Create objects within objects; this works because if the first character 233 | of value is an open brace or a bracket we attempt to decode the 234 | remainder as JSON. 235 | Beware spaces in strings \&... 236 | .IP 237 | .nf 238 | \f[C] 239 | $ jo -p name=JP object=$(jo fruit=Orange hungry\[at]0 point=$(jo x=10 y=20 list=$(jo -a 1 2 3 4 5)) number=17) sunday\[at]0 240 | { 241 | \[dq]name\[dq]: \[dq]JP\[dq], 242 | \[dq]object\[dq]: { 243 | \[dq]fruit\[dq]: \[dq]Orange\[dq], 244 | \[dq]hungry\[dq]: false, 245 | \[dq]point\[dq]: { 246 | \[dq]x\[dq]: 10, 247 | \[dq]y\[dq]: 20, 248 | \[dq]list\[dq]: [ 249 | 1, 250 | 2, 251 | 3, 252 | 4, 253 | 5 254 | ] 255 | }, 256 | \[dq]number\[dq]: 17 257 | }, 258 | \[dq]sunday\[dq]: false 259 | } 260 | \f[R] 261 | .fi 262 | .PP 263 | Booleans as strings or as boolean (pay particular attention to 264 | \f[I]switch\f[R]; the \f[C]-B\f[R] option disables the default detection 265 | of the \[lq]\f[C]true\f[R]\[rq], \[lq]\f[C]false\f[R]\[rq], and 266 | \[lq]\f[C]null\f[R]\[rq] strings): 267 | .IP 268 | .nf 269 | \f[C] 270 | $ jo switch=true morning\[at]0 271 | {\[dq]switch\[dq]:true,\[dq]morning\[dq]:false} 272 | 273 | $ jo -B switch=true morning\[at]0 274 | {\[dq]switch\[dq]:\[dq]true\[dq],\[dq]morning\[dq]:false} 275 | \f[R] 276 | .fi 277 | .PP 278 | Elements (objects and arrays) can be nested. 279 | The following example nests an array called \f[I]point\f[R] and an 280 | object named \f[I]geo\f[R]: 281 | .IP 282 | .nf 283 | \f[C] 284 | $ jo -p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 285 | { 286 | \[dq]name\[dq]: \[dq]Jane\[dq], 287 | \[dq]point\[dq]: [ 288 | 1, 289 | 2 290 | ], 291 | \[dq]geo\[dq]: { 292 | \[dq]lat\[dq]: 10, 293 | \[dq]lon\[dq]: 20 294 | } 295 | } 296 | \f[R] 297 | .fi 298 | .PP 299 | The same example, using object paths: 300 | .IP 301 | .nf 302 | \f[C] 303 | $ jo -p -d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 304 | { 305 | \[dq]name\[dq]: \[dq]Jane\[dq], 306 | \[dq]point\[dq]: [ 307 | 1, 308 | 2 309 | ], 310 | \[dq]geo\[dq]: { 311 | \[dq]lat\[dq]: 10, 312 | \[dq]lon\[dq]: 20 313 | } 314 | } 315 | \f[R] 316 | .fi 317 | .PP 318 | Without \f[C]-d\f[R], a different object is generated: 319 | .IP 320 | .nf 321 | \f[C] 322 | $ jo -p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 323 | { 324 | \[dq]name\[dq]: \[dq]Jane\[dq], 325 | \[dq]point\[dq]: [ 326 | 1, 327 | 2 328 | ], 329 | \[dq]geo.lat\[dq]: 10, 330 | \[dq]geo.lon\[dq]: 20 331 | } 332 | \f[R] 333 | .fi 334 | .PP 335 | Create empty objects or arrays, intentionally or potentially: 336 | .IP 337 | .nf 338 | \f[C] 339 | $ jo < /dev/null 340 | {} 341 | 342 | $ MY_ARRAY=(a=1 b=2) 343 | $ jo -a \[dq]${MY_ARRAY[\[at]]}\[dq] < /dev/null 344 | [\[dq]a=1\[dq],\[dq]b=2\[dq]] 345 | \f[R] 346 | .fi 347 | .PP 348 | Type coercion: 349 | .IP 350 | .nf 351 | \f[C] 352 | $ jo -p -- -s a=true b=true -s c=123 d=123 -b e=\[dq]1\[dq] -b f=\[dq]true\[dq] -n g=\[dq]This is a test\[dq] -b h=\[dq]This is a test\[dq] 353 | { 354 | \[dq]a\[dq]: \[dq]true\[dq], 355 | \[dq]b\[dq]: true, 356 | \[dq]c\[dq]: \[dq]123\[dq], 357 | \[dq]d\[dq]: 123, 358 | \[dq]e\[dq]: true, 359 | \[dq]f\[dq]: true, 360 | \[dq]g\[dq]: 14, 361 | \[dq]h\[dq]: true 362 | } 363 | 364 | $ jo -a -- -s 123 -n \[dq]This is a test\[dq] -b C_Rocks 456 365 | [\[dq]123\[dq],14,true,456] 366 | \f[R] 367 | .fi 368 | .PP 369 | Read element values from files: a value which starts with 370 | \f[C]\[at]\f[R] is read in plain whereas if it begins with a \f[C]%\f[R] 371 | it will be base64-encoded and if it starts with \f[C]:\f[R] the contents 372 | are interpreted as JSON: 373 | .IP 374 | .nf 375 | \f[C] 376 | $ jo program=jo authors=\[at]AUTHORS 377 | {\[dq]program\[dq]:\[dq]jo\[dq],\[dq]authors\[dq]:\[dq]Jan-Piet Mens \[dq]} 378 | 379 | $ jo filename=AUTHORS content=%AUTHORS 380 | {\[dq]filename\[dq]:\[dq]AUTHORS\[dq],\[dq]content\[dq]:\[dq]SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K\[dq]} 381 | 382 | $ jo nested=:nested.json 383 | {\[dq]nested\[dq]:{\[dq]field1\[dq]:123,\[dq]field2\[dq]:\[dq]abc\[dq]}} 384 | \f[R] 385 | .fi 386 | .PP 387 | These characters can be escaped to avoid interpretation: 388 | .IP 389 | .nf 390 | \f[C] 391 | $ jo name=\[dq]JP Mens\[dq] twitter=\[aq]\[rs]\[at]jpmens\[aq] 392 | {\[dq]name\[dq]:\[dq]JP Mens\[dq],\[dq]twitter\[dq]:\[dq]\[at]jpmens\[dq]} 393 | 394 | $ jo char=\[dq] \[dq] URIescape=\[rs]\[rs]%20 395 | {\[dq]char\[dq]:\[dq] \[dq],\[dq]URIescape\[dq]:\[dq]%20\[dq]} 396 | 397 | $ jo action=\[dq]split window\[dq] vimcmd=\[dq]\[rs]:split\[dq] 398 | {\[dq]action\[dq]:\[dq]split window\[dq],\[dq]vimcmd\[dq]:\[dq]:split\[dq]} 399 | \f[R] 400 | .fi 401 | .PP 402 | Read element values from a file in order to overcome ARG_MAX limits 403 | during object assignment: 404 | .IP 405 | .nf 406 | \f[C] 407 | $ ls | jo -a > child.json 408 | $ jo files:=child.json 409 | {\[dq]files\[dq]:[\[dq]AUTHORS\[dq],\[dq]COPYING\[dq],\[dq]ChangeLog\[dq] .... 410 | 411 | $ ls *.c | jo -a > source.json; ls *.h | jo -a > headers.json 412 | $ jo -a :source.json :headers.json 413 | [[\[dq]base64.c\[dq],\[dq]jo.c\[dq],\[dq]json.c\[dq]],[\[dq]base64.h\[dq],\[dq]json.h\[dq]]] 414 | \f[R] 415 | .fi 416 | .PP 417 | Add elements to existing JSON: 418 | .IP 419 | .nf 420 | \f[C] 421 | $ jo -f source.json 1 | jo -f - 2 3 422 | [\[dq]base64.c\[dq],\[dq]jo.c\[dq],\[dq]json.c\[dq],1,2,3] 423 | 424 | $ curl -s \[aq]https://noembed.com/embed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ\[aq] | jo -f - status=Rickrolled 425 | { ...., \[dq]type\[dq]:\[dq]video\[dq],\[dq]author_url\[dq]:\[dq]https://www.youtube.com/user/RickAstleyVEVO\[dq],\[dq]status\[dq]:\[dq]Rickrolled\[dq]} 426 | \f[R] 427 | .fi 428 | .PP 429 | Deduplicate object keys (\f[I]jo\f[R] appends duplicate object keys by 430 | default): 431 | .IP 432 | .nf 433 | \f[C] 434 | $ jo a=1 b=2 a=3 435 | {\[dq]a\[dq]:1,\[dq]b\[dq]:2,\[dq]a\[dq]:3} 436 | $ jo -D a=1 b=2 a=3 437 | {\[dq]a\[dq]:3,\[dq]b\[dq]:2} 438 | \f[R] 439 | .fi 440 | .SH OPTIONS 441 | .PP 442 | \f[I]jo\f[R] understands the following global options. 443 | .TP 444 | -a 445 | Interpret the list of \f[I]words\f[R] as array values and produce an 446 | array instead of an object. 447 | .TP 448 | -B 449 | By default, \f[I]jo\f[R] interprets the strings \[lq]\f[C]true\f[R]\[rq] 450 | and \[lq]\f[C]false\f[R]\[rq] as boolean elements \f[C]true\f[R] and 451 | \f[C]false\f[R] respectively, and \[lq]\f[C]null\f[R]\[rq] as 452 | \f[C]null\f[R]. 453 | Disable with this option. 454 | .TP 455 | -D 456 | Deduplicate object keys. 457 | .TP 458 | -e 459 | Ignore empty stdin (i.e.\ don\[cq]t produce a diagnostic error when 460 | \f[I]stdin\f[R] is empty) 461 | .TP 462 | -n 463 | Do not add keys with empty values. 464 | .TP 465 | -p 466 | Pretty-print the JSON string on output instead of the terse one-line 467 | output it prints by default. 468 | .TP 469 | -v 470 | Show version and exit. 471 | .TP 472 | -V 473 | Show version as a JSON object and exit. 474 | .SH BUGS 475 | .PP 476 | Probably. 477 | .PP 478 | If a value given to \f[I]jo\f[R] expands to empty in the shell, then 479 | \f[I]jo\f[R] produces a \f[C]null\f[R] in object mode, and might appear 480 | to hang in array mode; it is not hanging, rather it\[cq]s reading 481 | \f[I]stdin\f[R]. 482 | This is not a bug. 483 | .PP 484 | Numeric values are converted to numbers which can produce undesired 485 | results. 486 | If you quote a numeric value, \f[I]jo\f[R] will make it a string. 487 | Compare the following: 488 | .IP 489 | .nf 490 | \f[C] 491 | $ jo a=1.0 492 | {\[dq]a\[dq]:1} 493 | $ jo a=\[rs]\[dq]1.0\[rs]\[dq] 494 | {\[dq]a\[dq]:\[dq]1.0\[dq]} 495 | \f[R] 496 | .fi 497 | .PP 498 | Omitting a closing bracket on a nested element causes a diagnostic 499 | message to print, but the output contains garbage anyway. 500 | This was designed thusly. 501 | .SH RETURN CODES 502 | .PP 503 | \f[I]jo\f[R] exits with a code 0 on success and non-zero on failure 504 | after indicating what caused the failure. 505 | .SH AVAILABILITY 506 | .PP 507 | 508 | .SH CREDITS 509 | .IP \[bu] 2 510 | This program uses \f[C]json.[ch]\f[R], by Joseph A. 511 | Adams. 512 | .SH SEE ALSO 513 | .IP \[bu] 2 514 | 515 | .IP \[bu] 2 516 | 517 | .IP \[bu] 2 518 | 519 | .IP \[bu] 2 520 | strtod(3) 521 | .SH AUTHOR 522 | .PP 523 | Jan-Piet Mens 524 | -------------------------------------------------------------------------------- /jo.bash: -------------------------------------------------------------------------------- 1 | # bash completion for jo(1) 2 | 3 | _jo() { 4 | 5 | # Don't split words on =, for =@ and =% handling 6 | COMP_WORDBREAKS=${COMP_WORDBREAKS//=} 7 | 8 | # No completion if an exit causing flag is around 9 | local i 10 | for i in ${!COMP_WORDS[@]}; do 11 | [[ $i -ne $COMP_CWORD && ${COMP_WORDS[i]} == -*[hvV]* ]] && return 0 12 | done 13 | 14 | # Complete available options following a dash 15 | if [[ $2 == -* ]]; then 16 | COMPREPLY=( $(compgen -W '-a -B -h -p -v -V' -- "$2") ) 17 | return 0 18 | fi 19 | 20 | # Complete filenames on =@ and =% 21 | if [[ $2 == *=[@%]* ]]; then 22 | local file prefix 23 | file="${2#*=[@%]}" 24 | prefix="${2:0:${#2}-${#file}}" 25 | compopt -o filenames 26 | COMPREPLY=( $(compgen -f -- "$file") ) 27 | if [[ ${#COMPREPLY[@]} -eq 1 ]]; then 28 | if [[ -d "${COMPREPLY[0]}" ]]; then 29 | COMPREPLY[0]+=/ 30 | compopt -o nospace 31 | fi 32 | COMPREPLY[0]="$prefix${COMPREPLY[0]}" 33 | fi 34 | return 0 35 | fi 36 | 37 | } && 38 | complete -F _jo jo 39 | -------------------------------------------------------------------------------- /jo.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #ifndef _AIX 8 | # include 9 | #endif 10 | #include 11 | #if !defined(WIN32) && !defined(_AIX) 12 | # include 13 | #endif 14 | #include "json.h" 15 | #include "base64.h" 16 | 17 | /* 18 | * Copyright (C) 2016-2019 Jan-Piet Mens 19 | * 20 | * This program is free software; you can redistribute it and/or 21 | * modify it under the terms of the GNU General Public License 22 | * as published by the Free Software Foundation; either version 2 23 | * of the License, or (at your option) any later version. 24 | * 25 | * This program is distributed in the hope that it will be useful, 26 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 27 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 28 | * GNU General Public License for more details. 29 | * 30 | * You should have received a copy of the GNU General Public License 31 | * along with this program; if not, see 32 | * . 33 | */ 34 | 35 | #define SPACER " " 36 | #define FLAG_ARRAY 0x01 37 | #define FLAG_PRETTY 0x02 38 | #define FLAG_NOBOOL 0x04 39 | #define FLAG_BOOLEAN 0x08 40 | #define FLAG_NOSTDIN 0x10 41 | #define FLAG_SKIPNULLS 0x20 42 | #define FLAG_MASK (FLAG_ARRAY | FLAG_PRETTY | FLAG_NOBOOL | FLAG_BOOLEAN | FLAG_NOSTDIN | FLAG_SKIPNULLS) 43 | 44 | /* Size of buffer blocks for pipe slurping */ 45 | #define SLURP_BLOCK_SIZE 4096 46 | 47 | static JsonNode *pile; /* pile of nested objects/arrays */ 48 | 49 | #if defined(_WIN32) || defined(_AIX) 50 | #include 51 | #include 52 | static inline void err(int eval, const char *fmt, ...) { 53 | int errnum = errno; 54 | 55 | va_list ap; 56 | va_start(ap, fmt); 57 | 58 | fprintf(stderr, "jo: "); 59 | vfprintf(stderr, fmt, ap); 60 | fprintf(stderr, ": %s\n", strerror(errnum)); 61 | 62 | va_end(ap); 63 | exit(eval); 64 | } 65 | static inline void errx(int eval, const char *fmt, ...) { 66 | va_list ap; 67 | va_start(ap, fmt); 68 | 69 | fprintf(stderr, "jo: "); 70 | vfprintf(stderr, fmt, ap); 71 | fprintf(stderr, "\n"); 72 | 73 | va_end(ap); 74 | exit(eval); 75 | } 76 | #endif 77 | 78 | #if defined(_WIN32) && !defined(fseeko) 79 | # define fseeko fseek 80 | # define ftello ftell 81 | #endif 82 | 83 | #define TAG_TO_FLAGS(tag) ((FLAG_MASK + 1) * (tag)) 84 | #define TAG_FLAG_BOOL (TAG_TO_FLAGS(JSON_BOOL)) 85 | #define TAG_FLAG_STRING (TAG_TO_FLAGS(JSON_STRING)) 86 | #define TAG_FLAG_NUMBER (TAG_TO_FLAGS(JSON_NUMBER)) 87 | #define COERCE_MASK (TAG_FLAG_BOOL | TAG_FLAG_STRING | TAG_FLAG_NUMBER) 88 | 89 | JsonTag flags_to_tag(int flags) { 90 | return flags / (FLAG_MASK + 1); 91 | } 92 | 93 | void json_copy_to_object(JsonNode * obj, JsonNode * object_or_array, int clobber) 94 | { 95 | JsonNode *node, *node_child, *obj_child; 96 | 97 | if (obj->tag != JSON_OBJECT && obj->tag != JSON_ARRAY) 98 | return; 99 | 100 | json_foreach(node, object_or_array) { 101 | if (!clobber & (json_find_member(obj, node->key) != NULL)) 102 | continue; /* Don't clobber existing keys */ 103 | if (obj->tag == JSON_OBJECT) { 104 | if (node->tag == JSON_STRING) 105 | json_append_member(obj, node->key, json_mkstring(node->string_)); 106 | else if (node->tag == JSON_NUMBER) 107 | json_append_member(obj, node->key, json_mknumber(node->number_)); 108 | else if (node->tag == JSON_BOOL) 109 | json_append_member(obj, node->key, json_mkbool(node->bool_)); 110 | else if (node->tag == JSON_NULL) 111 | json_append_member(obj, node->key, json_mknull()); 112 | else if (node->tag == JSON_OBJECT) { 113 | /* Deep-copy existing object to new object */ 114 | json_append_member(obj, node->key, (obj_child = json_mkobject())); 115 | json_foreach(node_child, node) { 116 | json_copy_to_object(obj_child, node_child, clobber); 117 | } 118 | } else 119 | fprintf(stderr, "PANIC: unhandled JSON type %d\n", node->tag); 120 | } else if (obj->tag == JSON_ARRAY) { 121 | if (node->tag == JSON_STRING) 122 | json_append_element(obj, json_mkstring(node->string_)); 123 | if (node->tag == JSON_NUMBER) 124 | json_append_element(obj, json_mknumber(node->number_)); 125 | if (node->tag == JSON_BOOL) 126 | json_append_element(obj, json_mkbool(node->bool_)); 127 | if (node->tag == JSON_NULL) 128 | json_append_element(obj, json_mknull()); 129 | } 130 | } 131 | } 132 | 133 | int slurp(FILE *fp, char **bufp, off_t bufblk_sz, int eos_char, size_t *out_len, bool fold_newlines) 134 | { 135 | char *buf; 136 | int result = 0; 137 | size_t i = 0; 138 | int ch = EOF; 139 | size_t buffer_len = bufblk_sz; 140 | 141 | if ((buf = malloc(buffer_len)) == NULL) { 142 | result = -1; 143 | } else { 144 | while ((ch = fgetc(fp)) != eos_char && ch != EOF) { 145 | if (i == (buffer_len - 1)) { 146 | buffer_len += bufblk_sz; 147 | if ((buf = realloc(buf, buffer_len)) == NULL) { 148 | result = -1; 149 | break; 150 | } 151 | } 152 | if (ch != '\n' || !fold_newlines) { 153 | buf[i++] = ch; 154 | } 155 | } 156 | } 157 | if (result < 0) { 158 | free(buf); 159 | buf = NULL; 160 | } else { 161 | buf[i] = 0; 162 | } 163 | *out_len = i; 164 | *bufp = buf; 165 | return result; 166 | } 167 | 168 | char *slurp_file(const char* filename, size_t *out_len, bool fold_newlines) 169 | { 170 | char *buf; 171 | off_t buffer_len; 172 | FILE *fp; 173 | bool use_stdin = strcmp(filename, "-") == 0; 174 | 175 | if (use_stdin) fp = stdin; 176 | else if ((fp = fopen(filename, "r")) == NULL) { 177 | perror(filename); 178 | errx(1, "Cannot open %s for reading", filename); 179 | } 180 | if (fseeko(fp, 0, SEEK_END) != 0) { 181 | /* If we cannot seek, we're operating off a pipe and 182 | need to dynamically grow the buffer that we're 183 | reading into */ 184 | buffer_len = SLURP_BLOCK_SIZE; 185 | } else { 186 | buffer_len = ftello(fp) + 1; 187 | fseeko(fp, 0, SEEK_SET); 188 | } 189 | 190 | if (slurp(fp, &buf, buffer_len, EOF, out_len, fold_newlines) < 0) { 191 | errx(1, "File %s is too large to be read into memory", filename); 192 | } 193 | if (!use_stdin) fclose(fp); 194 | return buf; 195 | } 196 | 197 | char *slurp_line(FILE *fp, size_t *out_len) 198 | { 199 | char *buf; 200 | 201 | if (slurp(fp, &buf, SLURP_BLOCK_SIZE, '\n', out_len, false) < 0) { 202 | errx(1, "Line too large to be read into memory"); 203 | } 204 | return buf; 205 | } 206 | 207 | JsonNode *jo_mknull(JsonTag type) { 208 | switch (type) { 209 | case JSON_STRING: 210 | return json_mkstring(""); 211 | break; 212 | case JSON_NUMBER: 213 | return json_mknumber(0); 214 | break; 215 | case JSON_BOOL: 216 | return json_mkbool(false); 217 | break; 218 | default: 219 | return json_mknull(); 220 | break; 221 | } 222 | } 223 | 224 | JsonNode *jo_mkbool(bool b, JsonTag type) { 225 | switch (type) { 226 | case JSON_STRING: 227 | return json_mkstring(b ? "true" : "false"); 228 | break; 229 | case JSON_NUMBER: 230 | return json_mknumber(b ? 1 : 0); 231 | break; 232 | default: 233 | return json_mkbool(b); 234 | break; 235 | } 236 | } 237 | 238 | JsonNode *jo_mkstring(char *str, JsonTag type) { 239 | switch (type) { 240 | case JSON_NUMBER: 241 | /* Length of string */ 242 | return json_mknumber(strlen(str)); 243 | break; 244 | case JSON_BOOL: 245 | /* True if not empty */ 246 | return json_mkbool(strlen(str) > 0); 247 | break; 248 | default: 249 | return json_mkstring(str); 250 | break; 251 | } 252 | } 253 | 254 | JsonNode *jo_mknumber(char *str, JsonTag type) { 255 | /* ASSUMPTION: str already tested as valid number */ 256 | double n = strtod(str, NULL); 257 | 258 | switch (type) { 259 | case JSON_STRING: 260 | /* Just return the original representation */ 261 | return json_mkstring(str); 262 | break; 263 | case JSON_BOOL: 264 | return json_mkbool(n != 0); 265 | break; 266 | default: 267 | /* ASSUMPTION: str already tested as valid number */ 268 | return json_mknumber(n); 269 | break; 270 | } 271 | } 272 | 273 | /* 274 | * Attempt to "sniff" the type of data in `str' and return 275 | * a JsonNode of the correct JSON type. 276 | */ 277 | 278 | JsonNode *vnode(char *str, int flags) 279 | { 280 | JsonTag type = flags_to_tag(flags); 281 | 282 | if (strlen(str) == 0) { 283 | return (flags & FLAG_SKIPNULLS) ? (JsonNode *)NULL : jo_mknull(type); 284 | } 285 | 286 | /* If str begins with a double quote, keep it a string */ 287 | 288 | if (*str == '"') { 289 | #if 0 290 | char *bp = str + strlen(str) - 1; 291 | 292 | if (bp > str && *bp == '"') 293 | *bp = 0; /* Chop closing double quote */ 294 | return json_mkstring(str + 1); 295 | #endif 296 | return jo_mkstring(str, type); 297 | } 298 | 299 | char *endptr; 300 | double num = strtod(str, &endptr); 301 | 302 | if (!*endptr && isfinite(num)) { 303 | return jo_mknumber(str, type); 304 | } 305 | 306 | if (!(flags & FLAG_NOBOOL)) { 307 | if (strcmp(str, "true") == 0) { 308 | return jo_mkbool(true, type); 309 | } else if (strcmp(str, "false") == 0) { 310 | return jo_mkbool(false, type); 311 | } else if (strcmp(str, "null") == 0) { 312 | return jo_mknull(type); 313 | } 314 | } 315 | 316 | if (*str == '\\') { 317 | ++str; 318 | } else { 319 | if (*str == '@' || *str == '%' || *str == ':') { 320 | char *filename = str + 1, *content; 321 | bool binmode = (*str == '%'); 322 | bool jsonmode = (*str == ':'); 323 | size_t len = 0; 324 | JsonNode *j = NULL; 325 | 326 | if ((content = slurp_file(filename, &len, false)) == NULL) { 327 | errx(1, "Error reading file %s", filename); 328 | } 329 | 330 | if (binmode) { 331 | char *encoded; 332 | 333 | if ((encoded = base64_encode(content, len)) == NULL) { 334 | errx(1, "Cannot base64-encode file %s", filename); 335 | } 336 | 337 | j = json_mkstring(encoded); 338 | free(encoded); 339 | } else if (jsonmode) { 340 | j = json_decode(content); 341 | if (j == NULL) { 342 | errx(1, "Cannot decode JSON in file %s", filename); 343 | } 344 | } 345 | 346 | // If it got this far without valid JSON, just consider it a string 347 | if (j == NULL) { 348 | char *bp = content + strlen(content) - 1; 349 | 350 | if (*bp == '\n') *bp-- = 0; 351 | if (*bp == '\r') *bp = 0; 352 | j = json_mkstring(content); 353 | } 354 | free(content); 355 | return (j); 356 | } 357 | } 358 | 359 | if (*str == '{' || *str == '[') { 360 | if (type == JSON_STRING) { 361 | return json_mkstring(str); 362 | } 363 | JsonNode *obj = json_decode(str); 364 | 365 | if (obj == NULL) { 366 | /* JSON cannot be decoded; return the string */ 367 | // fprintf(stderr, "Cannot decode JSON from %s\n", str); 368 | 369 | obj = json_mkstring(str); 370 | } 371 | 372 | return (obj); 373 | } 374 | 375 | return jo_mkstring(str, type); 376 | } 377 | 378 | /* 379 | * Attempt to sniff `str' into a boolean; return a 380 | * corresponding JsonNode for it. 381 | */ 382 | 383 | JsonNode *boolnode(char *str) 384 | { 385 | if (strlen(str) == 0) { 386 | return json_mknull(); 387 | } 388 | 389 | if (tolower((unsigned char) *str) == 't') { 390 | return json_mkbool(1); 391 | } 392 | 393 | return json_mkbool(atoi(str)); 394 | } 395 | 396 | int usage(char *prog) 397 | { 398 | fprintf(stderr, "Usage: %s [-a] [-B] [-D] [-d keydelim] [-p] [-e] [-n] [-o outfile] [-v] [-V] [-f file] [--] [-s|-n|-b] [word...]\n", prog); 399 | fprintf(stderr, "\tword is key=value or key@value\n"); 400 | fprintf(stderr, "\t-a creates an array of words\n"); 401 | fprintf(stderr, "\t-B disable boolean true/false/null detection\n"); 402 | fprintf(stderr, "\t-D deduplicate object keys\n"); 403 | fprintf(stderr, "\t-d key will be object path separated by keydelim\n"); 404 | fprintf(stderr, "\t-f load file as JSON object or array\n"); 405 | fprintf(stderr, "\t-p pretty-prints JSON on output\n"); 406 | fprintf(stderr, "\t-e quit if stdin is empty do not wait for input\n"); 407 | fprintf(stderr, "\t-s coerce type guessing to string\n"); 408 | fprintf(stderr, "\t-b coerce type guessing to bool\n"); 409 | fprintf(stderr, "\t-n coerce type guessing to number\n"); 410 | fprintf(stderr, "\t-o output to the given file\n"); 411 | fprintf(stderr, "\t-v show version\n"); 412 | fprintf(stderr, "\t-V show version in JSON\n"); 413 | 414 | return (-1); 415 | } 416 | 417 | /* 418 | * Check whether we're being given nested arrays or objects. 419 | * `kv' contains the "key" such as "number" or "point[]" or 420 | * "geo[lat]". `value' the actual value for that element. 421 | * 422 | * Returns true if nesting is completely handled, otherwise: 423 | * *keyp -> remaining key for caller to insert "value" 424 | * *baseop -> object node in which caller should insert "value" 425 | */ 426 | 427 | bool resolve_nested(int flags, char **keyp, char key_delim, JsonNode *value, JsonNode **baseop) 428 | { 429 | char *member = NULL, *bo, *bc, *so; /* bracket open, close, sub-object */ 430 | JsonNode *op; 431 | int found = false; 432 | 433 | (void)flags; 434 | 435 | if (key_delim) { 436 | /* First construct nested object */ 437 | while ((so = strchr(*keyp, key_delim)) != NULL) { 438 | *so = 0; 439 | if ((op = json_find_member(*baseop, *keyp)) == NULL) { 440 | /* Add a nested object node */ 441 | op = json_mkobject(); 442 | json_append_member(*baseop, *keyp, op); 443 | } 444 | *baseop = op; 445 | *keyp = so + 1; 446 | } 447 | } 448 | 449 | /* Now check for trailing geo[] or geo[lat] */ 450 | if ((bo = strchr(*keyp, '[')) != NULL) { 451 | if (*(bo+1) == ']') { 452 | *bo = 0; 453 | } else if ((bc = strchr(bo + 1, ']')) == NULL) { 454 | fprintf(stderr, "missing closing bracket on %s\n", *keyp); 455 | return (false); 456 | } else { 457 | *bo = *bc = 0; 458 | member = bo + 1; 459 | } 460 | 461 | /* 462 | * *keyp is now `geo' for both `geo[]` and `geo[lat]` 463 | * member is null for the former and "lat" for the latter. 464 | * Find an existing object in *baseop for this member name 465 | * or create a new one if we don't have it. 466 | */ 467 | 468 | if ((op = json_find_member(*baseop, *keyp)) != NULL) { 469 | found = true; 470 | } else { 471 | op = (member == NULL) ? json_mkarray() : json_mkobject(); 472 | } 473 | 474 | if (member == NULL) { /* we're doing an array */ 475 | json_append_element(op, value); 476 | } else { /* we're doing an object */ 477 | json_append_member(op, member, value); 478 | } 479 | 480 | if (!found) { 481 | json_append_member(*baseop, *keyp, op); 482 | } 483 | 484 | return (true); 485 | } 486 | return (false); 487 | } 488 | 489 | int member_to_object(JsonNode *object, int flags, char key_delim, char *kv) 490 | { 491 | /* we expect key=value or key:value (boolean on last) */ 492 | char *p = strchr(kv, '='); 493 | char *q = strchr(kv, '@'); 494 | char *r = strchr(kv, ':'); 495 | 496 | if ((r && *(r+1) == '=') && !q) { 497 | char *filename = p + 1; 498 | char *content; 499 | size_t len; 500 | 501 | if ((content = slurp_file(filename, &len, false)) == NULL) { 502 | errx(1, "Error reading file %s", filename); 503 | } 504 | 505 | JsonNode *o = json_decode(content); 506 | free(content); 507 | 508 | if (o == NULL) { 509 | errx(1, "Cannot decode JSON in file %s", filename); 510 | } 511 | 512 | *r = 0; /* Chop at ":=" */ 513 | if (!resolve_nested(flags, &kv, key_delim, o, &object)) 514 | json_append_member(object, kv, o); 515 | return (0); 516 | } 517 | 518 | if (!p && !q && !r) { 519 | return (-1); 520 | } 521 | 522 | JsonNode *val; 523 | if (p) { 524 | *p = 0; 525 | val = vnode(p+1, flags); 526 | 527 | if (!resolve_nested(flags, &kv, key_delim, val, &object)) 528 | json_append_member(object, kv, val); 529 | } else { 530 | if (q) { 531 | *q = 0; 532 | val = boolnode(q+1); 533 | 534 | if (!resolve_nested(flags | FLAG_BOOLEAN, &kv, key_delim, val, &object)) 535 | json_append_member(object, kv, val); 536 | } 537 | } 538 | return (0); 539 | } 540 | 541 | /* 542 | * Append kv to the array or object. 543 | */ 544 | 545 | void append_kv(JsonNode *object_or_array, int flags, char key_delim, char *kv) 546 | { 547 | if (flags & FLAG_ARRAY) { 548 | json_append_element(object_or_array, vnode(kv, flags)); 549 | } else { 550 | if (member_to_object(object_or_array, flags, key_delim, kv) == -1) { 551 | fprintf(stderr, "Argument `%s' is neither k=v nor k@v\n", kv); 552 | } 553 | } 554 | } 555 | 556 | #ifdef _WIN32 557 | #include 558 | char* utf8_from_locale(const char *str, size_t len) 559 | { 560 | wchar_t* wcsp; 561 | char* mbsp; 562 | size_t mbssize, wcssize; 563 | 564 | if (len == 0) { 565 | return strdup(""); 566 | } 567 | if (len == (size_t)-1) { 568 | len = strlen(str); 569 | } 570 | wcssize = MultiByteToWideChar(GetACP(), 0, str, len, NULL, 0); 571 | wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t)); 572 | if (!wcsp) { 573 | return NULL; 574 | } 575 | wcssize = MultiByteToWideChar(GetACP(), 0, str, len, wcsp, wcssize + 1); 576 | wcsp[wcssize] = 0; 577 | 578 | mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL); 579 | mbsp = (char*) malloc((mbssize + 1)); 580 | if (!mbsp) { 581 | free(wcsp); 582 | return NULL; 583 | } 584 | mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL); 585 | mbsp[mbssize] = 0; 586 | free(wcsp); 587 | return mbsp; 588 | } 589 | # define utf8_free(p) free(p) 590 | 591 | char* locale_from_utf8(const char *utf8, size_t len) 592 | { 593 | wchar_t* wcsp; 594 | char* mbsp; 595 | size_t mbssize, wcssize; 596 | 597 | if (len == 0) { 598 | return strdup(""); 599 | } 600 | if (len == (size_t)-1) { 601 | len = strlen(utf8); 602 | } 603 | wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, 0); 604 | wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t)); 605 | if (!wcsp) { 606 | return NULL; 607 | } 608 | wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len, wcsp, wcssize + 1); 609 | wcsp[wcssize] = 0; 610 | mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL); 611 | mbsp = (char*) malloc((mbssize + 1)); 612 | if (!mbsp) { 613 | free(wcsp); 614 | return NULL; 615 | } 616 | mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL); 617 | mbsp[mbssize] = 0; 618 | free(wcsp); 619 | return mbsp; 620 | } 621 | # define locale_free(p) free(p) 622 | #else 623 | # define utf8_from_locale(p, l) (p) 624 | # define utf8_free(p) do {} while (0) 625 | # define locale_from_utf8(p, l) (p) 626 | # define locale_free(p) do {} while (0) 627 | #endif 628 | 629 | char *stringify(JsonNode *json, int flags) 630 | { 631 | int pretty = flags & FLAG_PRETTY; 632 | 633 | return json_stringify(json, (pretty) ? SPACER : NULL); 634 | } 635 | 636 | int version(int flags) 637 | { 638 | JsonNode *json = json_mkobject(); 639 | char *js; 640 | 641 | json_append_member(json, "program", json_mkstring("jo")); 642 | json_append_member(json, "author", json_mkstring("Jan-Piet Mens")); 643 | json_append_member(json, "repo", json_mkstring("https://github.com/jpmens/jo")); 644 | json_append_member(json, "version", json_mkstring(PACKAGE_VERSION)); 645 | 646 | if ((js = stringify(json, flags)) != NULL) { 647 | printf("%s\n", js); 648 | free(js); 649 | } 650 | json_delete(json); 651 | return (0); 652 | } 653 | 654 | int main(int argc, char **argv) 655 | { 656 | int c, key_delim = 0; 657 | bool showversion = false; 658 | char *kv, *js_string, *progname, *buf, *p; 659 | char *in_file = NULL, *in_str; 660 | char *out_file = NULL; 661 | FILE *out = stdout; 662 | size_t in_len = 0; 663 | int ttyin = isatty(fileno(stdin)); 664 | int ttyout = isatty(fileno(stdout)); 665 | int flags = 0; 666 | JsonNode *json, *op; 667 | 668 | #if HAVE_PLEDGE 669 | if (pledge("stdio rpath", NULL) == -1) { 670 | err(1, "pledge"); 671 | } 672 | #endif 673 | 674 | progname = (progname = strrchr(*argv, '/')) ? progname + 1 : *argv; 675 | 676 | while ((c = getopt(argc, argv, "aBDd:f:hpeno:vV")) != EOF) { 677 | switch (c) { 678 | case 'a': 679 | flags |= FLAG_ARRAY; 680 | break; 681 | case 'B': 682 | flags |= FLAG_NOBOOL; 683 | break; 684 | case 'D': 685 | json_dedup_members(true); 686 | break; 687 | case 'd': 688 | key_delim = optarg[0]; 689 | break; 690 | case 'f': 691 | in_file = optarg; 692 | break; 693 | case 'h': 694 | usage(progname); 695 | return (0); 696 | case 'p': 697 | flags |= FLAG_PRETTY; 698 | break; 699 | case 'e': 700 | flags |= FLAG_NOSTDIN; 701 | break; 702 | case 'n': 703 | flags |= FLAG_SKIPNULLS; 704 | break; 705 | case 'o': 706 | out_file = optarg; 707 | break; 708 | case 'v': 709 | printf("jo %s\n", PACKAGE_VERSION); 710 | exit(0); 711 | case 'V': 712 | showversion = true; 713 | break; 714 | default: 715 | exit(usage(progname)); 716 | } 717 | } 718 | 719 | if (showversion) { 720 | return(version(flags)); 721 | } 722 | 723 | argc -= optind; 724 | argv += optind; 725 | 726 | pile = json_mkobject(); 727 | if (in_file != NULL) { 728 | if ((in_str = slurp_file(in_file, &in_len, false)) == NULL) { 729 | errx(1, "Error reading file %s", in_file); 730 | } 731 | json = json_decode(in_str); 732 | if (json) { 733 | switch (json->tag) { 734 | case JSON_ARRAY: 735 | flags |= FLAG_ARRAY; 736 | break; 737 | case JSON_OBJECT: 738 | break; 739 | default: 740 | errx(1, "Input JSON not an array or object: %s", stringify(json, flags)); 741 | } 742 | } else 743 | json = (flags & FLAG_ARRAY) ? json_mkarray() : json_mkobject(); 744 | } else { 745 | json = (flags & FLAG_ARRAY) ? json_mkarray() : json_mkobject(); 746 | } 747 | 748 | if (argc == 0) { 749 | if (flags & FLAG_NOSTDIN) { 750 | return(0); 751 | } 752 | while ((buf = slurp_line(stdin, &in_len)) != NULL && in_len > 0) { 753 | p = ttyin ? utf8_from_locale(buf, -1) : buf; 754 | append_kv(json, flags, key_delim, p); 755 | if (ttyin) utf8_free(p); 756 | if (buf) free(buf); 757 | } 758 | } else { 759 | while ((kv = *argv++)) { 760 | if (kv[0] == '-' && !(flags & COERCE_MASK)) { 761 | /* Set one-shot coerce flag */ 762 | switch (kv[1]) { 763 | case 'b': 764 | flags |= TAG_FLAG_BOOL; 765 | break; 766 | case 's': 767 | flags |= TAG_FLAG_STRING; 768 | break; 769 | case 'n': 770 | flags |= TAG_FLAG_NUMBER; 771 | break; 772 | default: 773 | /* Treat as normal input */ 774 | p = utf8_from_locale(kv, -1); 775 | append_kv(json, flags, key_delim, p); 776 | utf8_free(p); 777 | /* Reset any one-shot coerce flags */ 778 | flags &= ~(COERCE_MASK); 779 | } 780 | } else { 781 | p = utf8_from_locale(kv, -1); 782 | append_kv(json, flags, key_delim, p); 783 | utf8_free(p); 784 | /* Reset any one-shot coerce flags */ 785 | flags &= ~(COERCE_MASK); 786 | } 787 | } 788 | } 789 | 790 | /* 791 | * See if we have any nested objects or arrays in the pile, 792 | * and copy these into our main object if so. 793 | */ 794 | 795 | json_foreach(op, pile) { 796 | JsonNode *o; 797 | 798 | if (op->tag == JSON_ARRAY) { 799 | o = json_mkarray(); 800 | } else if (op->tag == JSON_OBJECT) { 801 | o = json_mkobject(); 802 | } else { 803 | continue; 804 | } 805 | json_copy_to_object(o, op, 0); 806 | json_append_member(json, op->key, o); 807 | } 808 | 809 | 810 | if ((js_string = stringify(json, flags)) == NULL) { 811 | fprintf(stderr, "Invalid JSON\n"); 812 | exit(2); 813 | } 814 | 815 | if (out_file != NULL) { 816 | out = fopen(out_file, "w"); 817 | if (out == NULL) { 818 | perror(out_file); 819 | errx(1, "Cannot open %s for writing", out_file); 820 | } 821 | ttyout = isatty(fileno(out)); 822 | } 823 | p = ttyout ? locale_from_utf8(js_string, -1) : js_string; 824 | fprintf(out, "%s\n", p); 825 | if (ttyout) locale_free(p); 826 | free(js_string); 827 | json_delete(json); 828 | json_delete(pile); 829 | return (0); 830 | } 831 | -------------------------------------------------------------------------------- /jo.md: -------------------------------------------------------------------------------- 1 | # NAME 2 | 3 | jo - JSON output from a shell 4 | 5 | # SYNOPSIS 6 | 7 | jo \[-p\] \[-a\] \[-B\] \[-D\] \[-e\] \[-n\] \[-v\] \[-V\] \[-d 8 | keydelim\] \[-f file\] \[--\] \[ \[-s|-n|-b\] word ...\] 9 | 10 | # DESCRIPTION 11 | 12 | *jo* creates a JSON string on *stdout* from *word*s given it as 13 | arguments or read from *stdin*. If `-f` is specified, *jo* first loads 14 | the contents of *file* as a JSON object or array, then modifies it with 15 | subsequent *word*s before printing the final JSON string to *stdout*. 16 | *file* may be specified as `-` to read from *jo*'s standard input; this 17 | takes precedence over reading *word*s from *stdin*. 18 | 19 | Without option `-a` it generates an object whereby each *word* is a 20 | `key=value` (or `key@value`) pair with *key* being the JSON object 21 | element and *value* its value. *jo* attempts to guess the type of 22 | *value* in order to create number (using *strtod(3)*), string, or null 23 | values in JSON. 24 | 25 | A missing or empty *value* normally results in an element whose value is 26 | `null`. If `-n` is specified, this element is not created. 27 | 28 | *jo* normally treats *key* as a literal string value. If the `-d` option 29 | is specified, *key* will be interpreted as an *object path*, whose 30 | individual components are separated by the first character of 31 | *keydelim*. 32 | 33 | *jo* normally treats *value* as a literal string value, unless it begins 34 | with one of the following characters: 35 | 36 | | value | action | 37 | | ----- | ------------------------------------------------------------------- | 38 | | @file | substitute the contents of *file* as-is | 39 | | %file | substitute the contents of *file* in base64-encoded form | 40 | | :file | interpret the contents of *file* as JSON, and substitute the result | 41 | 42 | Escape the special character with a backslash to prevent this 43 | interpretation. 44 | 45 | *jo* treats `key@value` specifically as boolean JSON elements: if the 46 | value begins with `T`, `t`, or the numeric value is greater than zero, 47 | the result is `true`, else `false`. 48 | 49 | *jo* creates an array instead of an object when `-a` is specified. 50 | 51 | When the `:=` operator is used in a *word*, the name to the right of 52 | `:=` is a file containing JSON which is parsed and assigned to the key 53 | left of the operator. The file may be specified as `-` to read from 54 | *jo*'s standard input. 55 | 56 | # TYPE COERCION 57 | 58 | *jo*'s type guesses can be overridden on a per-word basis by prefixing 59 | *word* with `-s` for *string*, `-n` for *number*, or `-b` for *boolean*. 60 | The list of *word*s *must* be prefixed with `--`, to indicate to *jo* 61 | that there are no more global options. 62 | 63 | Type coercion works as follows: 64 | 65 | | word | \-s | \-n | \-b | default | 66 | | :--------- | :------------- | :-------- | :-------- | :------------- | 67 | | a= | "a":"" | "a":0 | "a":false | "a":null | 68 | | a=string | "a":"string" | "a":6 | "a":true | "a":"string" | 69 | | a="quoted" | "a":""quoted"" | "a":8 | "a":true | "a":""quoted"" | 70 | | a=12345 | "a":"12345" | "a":12345 | "a":true | "a":12345 | 71 | | a=true | "a":"true" | "a":1 | "a":true | "a":true | 72 | | a=false | "a":"false" | "a":0 | "a":false | "a":false | 73 | | a=null | "a":"" | "a":0 | "a":false | "a":null | 74 | 75 | Coercing a non-number string to number outputs the *length* of the 76 | string. 77 | 78 | Coercing a non-boolean string to boolean outputs `false` if the string 79 | is empty, `true` otherwise. 80 | 81 | Type coercion only applies to `key=value` words, and individual words in 82 | a `-a` array. Coercing other words has no effect. 83 | 84 | # EXAMPLES 85 | 86 | Create an object. Note how the incorrectly-formatted float value becomes 87 | a string: 88 | 89 | $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name="JP Mens" nada= coffee@T 90 | {"tst":1457081292,"lat":12.3456,"cc":"FR","badfloat":"3.14159.26","name":"JP Mens","nada":null,"coffee":true} 91 | 92 | Pretty-print an array with a list of files in the current directory: 93 | 94 | $ jo -p -a * 95 | [ 96 | "Makefile", 97 | "README.md", 98 | "jo.1", 99 | "jo.c", 100 | "jo.pandoc", 101 | "json.c", 102 | "json.h" 103 | ] 104 | 105 | Create objects within objects; this works because if the first character 106 | of value is an open brace or a bracket we attempt to decode the 107 | remainder as JSON. Beware spaces in strings ... 108 | 109 | $ jo -p name=JP object=$(jo fruit=Orange hungry@0 point=$(jo x=10 y=20 list=$(jo -a 1 2 3 4 5)) number=17) sunday@0 110 | { 111 | "name": "JP", 112 | "object": { 113 | "fruit": "Orange", 114 | "hungry": false, 115 | "point": { 116 | "x": 10, 117 | "y": 20, 118 | "list": [ 119 | 1, 120 | 2, 121 | 3, 122 | 4, 123 | 5 124 | ] 125 | }, 126 | "number": 17 127 | }, 128 | "sunday": false 129 | } 130 | 131 | Booleans as strings or as boolean (pay particular attention to *switch*; 132 | the `-B` option disables the default detection of the "`true`", 133 | "`false`", and "`null`" strings): 134 | 135 | $ jo switch=true morning@0 136 | {"switch":true,"morning":false} 137 | 138 | $ jo -B switch=true morning@0 139 | {"switch":"true","morning":false} 140 | 141 | Elements (objects and arrays) can be nested. The following example nests 142 | an array called *point* and an object named *geo*: 143 | 144 | $ jo -p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 145 | { 146 | "name": "Jane", 147 | "point": [ 148 | 1, 149 | 2 150 | ], 151 | "geo": { 152 | "lat": 10, 153 | "lon": 20 154 | } 155 | } 156 | 157 | The same example, using object paths: 158 | 159 | $ jo -p -d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 160 | { 161 | "name": "Jane", 162 | "point": [ 163 | 1, 164 | 2 165 | ], 166 | "geo": { 167 | "lat": 10, 168 | "lon": 20 169 | } 170 | } 171 | 172 | Without `-d`, a different object is generated: 173 | 174 | $ jo -p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 175 | { 176 | "name": "Jane", 177 | "point": [ 178 | 1, 179 | 2 180 | ], 181 | "geo.lat": 10, 182 | "geo.lon": 20 183 | } 184 | 185 | Create empty objects or arrays, intentionally or potentially: 186 | 187 | $ jo < /dev/null 188 | {} 189 | 190 | $ MY_ARRAY=(a=1 b=2) 191 | $ jo -a "${MY_ARRAY[@]}" < /dev/null 192 | ["a=1","b=2"] 193 | 194 | Type coercion: 195 | 196 | $ jo -p -- -s a=true b=true -s c=123 d=123 -b e="1" -b f="true" -n g="This is a test" -b h="This is a test" 197 | { 198 | "a": "true", 199 | "b": true, 200 | "c": "123", 201 | "d": 123, 202 | "e": true, 203 | "f": true, 204 | "g": 14, 205 | "h": true 206 | } 207 | 208 | $ jo -a -- -s 123 -n "This is a test" -b C_Rocks 456 209 | ["123",14,true,456] 210 | 211 | Read element values from files: a value which starts with `@` is read in 212 | plain whereas if it begins with a `%` it will be base64-encoded and if 213 | it starts with `:` the contents are interpreted as JSON: 214 | 215 | $ jo program=jo authors=@AUTHORS 216 | {"program":"jo","authors":"Jan-Piet Mens "} 217 | 218 | $ jo filename=AUTHORS content=%AUTHORS 219 | {"filename":"AUTHORS","content":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} 220 | 221 | $ jo nested=:nested.json 222 | {"nested":{"field1":123,"field2":"abc"}} 223 | 224 | These characters can be escaped to avoid interpretation: 225 | 226 | $ jo name="JP Mens" twitter='\@jpmens' 227 | {"name":"JP Mens","twitter":"@jpmens"} 228 | 229 | $ jo char=" " URIescape=\\%20 230 | {"char":" ","URIescape":"%20"} 231 | 232 | $ jo action="split window" vimcmd="\:split" 233 | {"action":"split window","vimcmd":":split"} 234 | 235 | Read element values from a file in order to overcome ARG\_MAX limits 236 | during object assignment: 237 | 238 | $ ls | jo -a > child.json 239 | $ jo files:=child.json 240 | {"files":["AUTHORS","COPYING","ChangeLog" .... 241 | 242 | $ ls *.c | jo -a > source.json; ls *.h | jo -a > headers.json 243 | $ jo -a :source.json :headers.json 244 | [["base64.c","jo.c","json.c"],["base64.h","json.h"]] 245 | 246 | Add elements to existing JSON: 247 | 248 | $ jo -f source.json 1 | jo -f - 2 3 249 | ["base64.c","jo.c","json.c",1,2,3] 250 | 251 | $ curl -s 'https://noembed.com/embed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ' | jo -f - status=Rickrolled 252 | { ...., "type":"video","author_url":"https://www.youtube.com/user/RickAstleyVEVO","status":"Rickrolled"} 253 | 254 | Deduplicate object keys (*jo* appends duplicate object keys by default): 255 | 256 | $ jo a=1 b=2 a=3 257 | {"a":1,"b":2,"a":3} 258 | $ jo -D a=1 b=2 a=3 259 | {"a":3,"b":2} 260 | 261 | # OPTIONS 262 | 263 | *jo* understands the following global options. 264 | 265 | - \-a 266 | Interpret the list of *words* as array values and produce an array 267 | instead of an object. 268 | - \-B 269 | By default, *jo* interprets the strings "`true`" and "`false`" as 270 | boolean elements `true` and `false` respectively, and "`null`" as 271 | `null`. Disable with this option. 272 | - \-D 273 | Deduplicate object keys. 274 | - \-e 275 | Ignore empty stdin (i.e. don't produce a diagnostic error when 276 | *stdin* is empty) 277 | - \-n 278 | Do not add keys with empty values. 279 | - \-p 280 | Pretty-print the JSON string on output instead of the terse one-line 281 | output it prints by default. 282 | - \-v 283 | Show version and exit. 284 | - \-V 285 | Show version as a JSON object and exit. 286 | 287 | # BUGS 288 | 289 | Probably. 290 | 291 | If a value given to *jo* expands to empty in the shell, then *jo* 292 | produces a `null` in object mode, and might appear to hang in array 293 | mode; it is not hanging, rather it's reading *stdin*. This is not a bug. 294 | 295 | Numeric values are converted to numbers which can produce undesired 296 | results. If you quote a numeric value, *jo* will make it a string. 297 | Compare the following: 298 | 299 | $ jo a=1.0 300 | {"a":1} 301 | $ jo a=\"1.0\" 302 | {"a":"1.0"} 303 | 304 | Omitting a closing bracket on a nested element causes a diagnostic 305 | message to print, but the output contains garbage anyway. This was 306 | designed thusly. 307 | 308 | # RETURN CODES 309 | 310 | *jo* exits with a code 0 on success and non-zero on failure after 311 | indicating what caused the failure. 312 | 313 | # AVAILABILITY 314 | 315 | 316 | 317 | # CREDITS 318 | 319 | - This program uses `json.[ch]`, by Joseph A. Adams. 320 | 321 | # SEE ALSO 322 | 323 | - 324 | - 325 | - 326 | - strtod(3) 327 | 328 | # AUTHOR 329 | 330 | Jan-Piet Mens 331 | -------------------------------------------------------------------------------- /jo.pandoc: -------------------------------------------------------------------------------- 1 | % JO(1) User Manuals 2 | 3 | # NAME 4 | 5 | jo - JSON output from a shell 6 | 7 | # SYNOPSIS 8 | 9 | jo [-p] [-a] [-B] [-D] [-e] [-n] [-v] [-V] [-d keydelim] [-f file] [--] [ [-s|-n|-b] word ...] 10 | 11 | # DESCRIPTION 12 | 13 | *jo* creates a JSON string on _stdout_ from *word*s given it as arguments or read from _stdin_. 14 | If `-f` is specified, *jo* first loads the contents of _file_ as a JSON object or array, then 15 | modifies it with subsequent *word*s before printing the final JSON string to _stdout_. 16 | _file_ may be specified as `-` to read from _jo_'s standard input; this takes precedence over 17 | reading *word*s from _stdin_. 18 | 19 | Without option `-a` it generates an object whereby each _word_ is a `key=value` (or `key@value`) 20 | pair with _key_ being the JSON object element and _value_ its value. *jo* attempts to 21 | guess the type of _value_ in order to create number (using _strtod(3)_), string, or null values in JSON. 22 | 23 | A missing or empty _value_ normally results in an element whose value is `null`. If `-n` is specified, this 24 | element is not created. 25 | 26 | *jo* normally treats _key_ as a literal string value. If the `-d` option is specified, _key_ will be 27 | interpreted as an _object path_, whose individual components are separated by the first character of _keydelim_. 28 | 29 | *jo* normally treats _value_ as a literal string value, unless it begins with one of the following characters: 30 | 31 | value action 32 | ----- ------ 33 | @file substitute the contents of _file_ as-is 34 | %file substitute the contents of _file_ in base64-encoded form 35 | :file interpret the contents of _file_ as JSON, and substitute the result 36 | 37 | Escape the special character with a backslash to prevent this interpretation. 38 | 39 | *jo* treats `key@value` specifically as boolean JSON elements: if the value begins with `T`, `t`, 40 | or the numeric value is greater than zero, the result is `true`, else `false`. 41 | 42 | *jo* creates an array instead of an object when `-a` is specified. 43 | 44 | When the `:=` operator is used in a _word_, the name to the right of `:=` is a file containing JSON which is parsed and assigned to the key left of the operator. The file may be specified as `-` to read from _jo_'s standard input. 45 | 46 | 47 | # TYPE COERCION 48 | 49 | *jo*'s type guesses can be overridden on a per-word basis by prefixing _word_ with `-s` for _string_, 50 | `-n` for _number_, or `-b` for _boolean_. The list of *word*s *must* be prefixed with `--`, to indicate 51 | to *jo* that there are no more global options. 52 | 53 | Type coercion works as follows: 54 | 55 | word -s -n -b default 56 | ------------ ---------------- ------------ --------- ---------------- 57 | a= "a":"" "a":0 "a":false "a":null 58 | a=string "a":"string" "a":6 "a":true "a":"string" 59 | a=\"quoted\" "a":"\"quoted\"" "a":8 "a":true "a":"\"quoted\"" 60 | a=12345 "a":"12345" "a":12345 "a":true "a":12345 61 | a=true "a":"true" "a":1 "a":true "a":true 62 | a=false "a":"false" "a":0 "a":false "a":false 63 | a=null "a":"" "a":0 "a":false "a":null 64 | 65 | Coercing a non-number string to number outputs the _length_ of the string. 66 | 67 | Coercing a non-boolean string to boolean outputs `false` if the string is empty, `true` otherwise. 68 | 69 | Type coercion only applies to `key=value` words, and individual words in a `-a` array. 70 | Coercing other words has no effect. 71 | 72 | # EXAMPLES 73 | 74 | Create an object. Note how the incorrectly-formatted float value becomes a string: 75 | 76 | $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name="JP Mens" nada= coffee@T 77 | {"tst":1457081292,"lat":12.3456,"cc":"FR","badfloat":"3.14159.26","name":"JP Mens","nada":null,"coffee":true} 78 | 79 | Pretty-print an array with a list of files in the current directory: 80 | 81 | $ jo -p -a * 82 | [ 83 | "Makefile", 84 | "README.md", 85 | "jo.1", 86 | "jo.c", 87 | "jo.pandoc", 88 | "json.c", 89 | "json.h" 90 | ] 91 | 92 | Create objects within objects; this works because if the first character of value is an open brace or a bracket we attempt to decode the remainder as JSON. Beware spaces in strings ... 93 | 94 | $ jo -p name=JP object=$(jo fruit=Orange hungry@0 point=$(jo x=10 y=20 list=$(jo -a 1 2 3 4 5)) number=17) sunday@0 95 | { 96 | "name": "JP", 97 | "object": { 98 | "fruit": "Orange", 99 | "hungry": false, 100 | "point": { 101 | "x": 10, 102 | "y": 20, 103 | "list": [ 104 | 1, 105 | 2, 106 | 3, 107 | 4, 108 | 5 109 | ] 110 | }, 111 | "number": 17 112 | }, 113 | "sunday": false 114 | } 115 | 116 | Booleans as strings or as boolean (pay particular attention to _switch_; the `-B` option disables the default detection of the "`true`", "`false`", and "`null`" strings): 117 | 118 | $ jo switch=true morning@0 119 | {"switch":true,"morning":false} 120 | 121 | $ jo -B switch=true morning@0 122 | {"switch":"true","morning":false} 123 | 124 | Elements (objects and arrays) can be nested. The following example nests an array called _point_ and an object named _geo_: 125 | 126 | $ jo -p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 127 | { 128 | "name": "Jane", 129 | "point": [ 130 | 1, 131 | 2 132 | ], 133 | "geo": { 134 | "lat": 10, 135 | "lon": 20 136 | } 137 | } 138 | 139 | The same example, using object paths: 140 | 141 | $ jo -p -d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 142 | { 143 | "name": "Jane", 144 | "point": [ 145 | 1, 146 | 2 147 | ], 148 | "geo": { 149 | "lat": 10, 150 | "lon": 20 151 | } 152 | } 153 | 154 | Without `-d`, a different object is generated: 155 | 156 | $ jo -p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 157 | { 158 | "name": "Jane", 159 | "point": [ 160 | 1, 161 | 2 162 | ], 163 | "geo.lat": 10, 164 | "geo.lon": 20 165 | } 166 | 167 | Create empty objects or arrays, intentionally or potentially: 168 | 169 | $ jo < /dev/null 170 | {} 171 | 172 | $ MY_ARRAY=(a=1 b=2) 173 | $ jo -a "${MY_ARRAY[@]}" < /dev/null 174 | ["a=1","b=2"] 175 | 176 | 177 | Type coercion: 178 | 179 | $ jo -p -- -s a=true b=true -s c=123 d=123 -b e="1" -b f="true" -n g="This is a test" -b h="This is a test" 180 | { 181 | "a": "true", 182 | "b": true, 183 | "c": "123", 184 | "d": 123, 185 | "e": true, 186 | "f": true, 187 | "g": 14, 188 | "h": true 189 | } 190 | 191 | $ jo -a -- -s 123 -n "This is a test" -b C_Rocks 456 192 | ["123",14,true,456] 193 | 194 | Read element values from files: a value which starts with `@` is read in plain whereas if it begins with a `%` it will be base64-encoded and if it starts with `:` the contents are interpreted as JSON: 195 | 196 | $ jo program=jo authors=@AUTHORS 197 | {"program":"jo","authors":"Jan-Piet Mens "} 198 | 199 | $ jo filename=AUTHORS content=%AUTHORS 200 | {"filename":"AUTHORS","content":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} 201 | 202 | $ jo nested=:nested.json 203 | {"nested":{"field1":123,"field2":"abc"}} 204 | 205 | These characters can be escaped to avoid interpretation: 206 | 207 | $ jo name="JP Mens" twitter='\@jpmens' 208 | {"name":"JP Mens","twitter":"@jpmens"} 209 | 210 | $ jo char=" " URIescape=\\%20 211 | {"char":" ","URIescape":"%20"} 212 | 213 | $ jo action="split window" vimcmd="\:split" 214 | {"action":"split window","vimcmd":":split"} 215 | 216 | Read element values from a file in order to overcome ARG_MAX limits during object assignment: 217 | 218 | $ ls | jo -a > child.json 219 | $ jo files:=child.json 220 | {"files":["AUTHORS","COPYING","ChangeLog" .... 221 | 222 | $ ls *.c | jo -a > source.json; ls *.h | jo -a > headers.json 223 | $ jo -a :source.json :headers.json 224 | [["base64.c","jo.c","json.c"],["base64.h","json.h"]] 225 | 226 | Add elements to existing JSON: 227 | 228 | $ jo -f source.json 1 | jo -f - 2 3 229 | ["base64.c","jo.c","json.c",1,2,3] 230 | 231 | $ curl -s 'https://noembed.com/embed?url=https://www.youtube.com/watch?v=dQw4w9WgXcQ' | jo -f - status=Rickrolled 232 | { ...., "type":"video","author_url":"https://www.youtube.com/user/RickAstleyVEVO","status":"Rickrolled"} 233 | 234 | Deduplicate object keys (*jo* appends duplicate object keys by default): 235 | 236 | $ jo a=1 b=2 a=3 237 | {"a":1,"b":2,"a":3} 238 | $ jo -D a=1 b=2 a=3 239 | {"a":3,"b":2} 240 | 241 | # OPTIONS 242 | 243 | *jo* understands the following global options. 244 | 245 | -a 246 | : Interpret the list of _words_ as array values and produce an array instead of 247 | an object. 248 | 249 | -B 250 | : By default, *jo* interprets the strings "`true`" and "`false`" as boolean elements 251 | `true` and `false` respectively, and "`null`" as `null`. Disable with this option. 252 | 253 | -D 254 | : Deduplicate object keys. 255 | 256 | -e 257 | : Ignore empty stdin (i.e. don't produce a diagnostic error when *stdin* 258 | is empty) 259 | 260 | -n 261 | : Do not add keys with empty values. 262 | 263 | -p 264 | : Pretty-print the JSON string on output instead of the terse one-line output it 265 | prints by default. 266 | 267 | -v 268 | : Show version and exit. 269 | 270 | -V 271 | : Show version as a JSON object and exit. 272 | 273 | # BUGS 274 | 275 | Probably. 276 | 277 | If a value given to *jo* expands to empty in the shell, then *jo* produces a `null` in object mode, and might appear to hang in array mode; it is not hanging, rather it's reading _stdin_. This is not a bug. 278 | 279 | Numeric values are converted to numbers which can produce undesired results. If you quote a numeric value, *jo* will make it a string. Compare the following: 280 | 281 | $ jo a=1.0 282 | {"a":1} 283 | $ jo a=\"1.0\" 284 | {"a":"1.0"} 285 | 286 | Omitting a closing bracket on a nested element causes a diagnostic message to print, but the output contains garbage anyway. This was designed thusly. 287 | 288 | # RETURN CODES 289 | 290 | *jo* exits with a code 0 on success and non-zero on failure after indicating what 291 | caused the failure. 292 | 293 | # AVAILABILITY 294 | 295 | 296 | 297 | # CREDITS 298 | 299 | * This program uses `json.[ch]`, by Joseph A. Adams. 300 | 301 | # SEE ALSO 302 | 303 | * 304 | * 305 | * 306 | * strtod(3) 307 | 308 | # AUTHOR 309 | 310 | Jan-Piet Mens 311 | 312 | -------------------------------------------------------------------------------- /jo.zsh: -------------------------------------------------------------------------------- 1 | #compdef jo 2 | 3 | # Completion function for zsh 4 | # Store this file in a directory listed in $fpath for it to be picked up 5 | # by compinit. It needs to be named with an initial underscore, e.g. _jo 6 | 7 | local curcontext="$curcontext" sep 8 | local -i aopt nm=$compstate[nmatches] 9 | local -a expl line state state_descr disp 10 | local -A opt_args 11 | 12 | _arguments -C -s -A "-*" \ 13 | '(-h)-p[pretty-print JSON on output]' \ 14 | '(-d -h)-a[create an array of words]' \ 15 | '(-v -V -h)-B[disable interpretation of true/false/null strings]' \ 16 | '(-v -V -h)-D[deduplicate object keys]' \ 17 | '(-v -V -h)-f+[first load file then modify it]:file:_files -g "*.json(-.)"' \ 18 | "(-v -V -h)-e[if stdin is empty don't wait for input - quit]" \ 19 | "(-v -V -h)-n[don't add keys with empty values]" \ 20 | '(-v -V -h)-o+[specify output file]:file:_files' \ 21 | '(- *)-v[show version information]' \ 22 | '(-a -B -e -h -v *)-V[show version in JSON]' \ 23 | '(-a -h -v -V)-d+[key will be object path separated by given delimiter]:key delimiter' \ 24 | '(- *)-h[show usage information]' \ 25 | '*::word:->words' 26 | 27 | if [[ -n $state ]]; then 28 | aopt=$+opt_args[-a] 29 | _arguments \ 30 | '*-s[coerce type guessing to string]: :->words' \ 31 | '*-b[coerce type guessing to bool]: :->words' \ 32 | '*-n[coerce type guessing to number]: :->words' \ 33 | '*: :->words' 34 | 35 | if (( aopt )); then 36 | _message -e words 'array element' 37 | elif compset -P 1 '*:='; then 38 | _alternative 'files:file:_files' 'operators:stdin:(-)' 39 | elif compset -P 1 '*='; then 40 | if compset -P '[@%:]'; then 41 | _files 42 | else 43 | _describe -t operators "file prefix" '( 44 | @:substitute\ file\ as-is 45 | %:substitute\ file\ in\ base64-encoded\ form 46 | \\::substitute\ file\ as\ JSON 47 | )' -S '' 48 | _message -e values value 49 | fi 50 | elif compset -P 1 '?*@'; then 51 | _description booleans expl 'boolean' 52 | compadd -M 'm:{a-zA-Z}={A-Za-z} m:{10}={TF}' "$expl[@]" True False 53 | else 54 | if compset -P '[^-]*'; then 55 | zstyle -s ":completion:${curcontext}:operators" list-separator sep || sep=-- 56 | disp=( 57 | "@ $sep boolean element" 58 | "= $sep value" 59 | ":= $sep substitute JSON file" 60 | "[] $sep array element" 61 | ) 62 | _description operators expl 'operator' 63 | compadd -S '' "$expl[@]" -ld disp '@' '=' ':=' '[]=' 64 | fi 65 | _message -e keys key 66 | fi 67 | fi 68 | 69 | [[ nm -ne compstate[nmatches] ]] 70 | -------------------------------------------------------------------------------- /json.c: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | #include "json.h" 25 | 26 | #include 27 | #include 28 | #include 29 | #include 30 | #include 31 | 32 | #define out_of_memory() do { \ 33 | fprintf(stderr, "Out of memory.\n"); \ 34 | exit(EXIT_FAILURE); \ 35 | } while (0) 36 | 37 | #if defined(_WIN32) || defined(_AIX) 38 | # define failx(e, n, f, ...) if (!(e)) { \ 39 | fprintf(stderr, "jo: JSON_ERR: " f "\n", __VA_ARGS__); \ 40 | exit(n); \ 41 | } 42 | #else 43 | # include 44 | # define failx(e, n, f, ...) if (!(e)) { \ 45 | errx(n, "JSON_ERR: " f, __VA_ARGS__); \ 46 | } 47 | #endif 48 | 49 | /* Sadly, strdup is not portable. */ 50 | static char *json_strdup(const char *str) 51 | { 52 | size_t n = strlen(str) + 1; 53 | char *ret = (char*) malloc(n); 54 | if (ret == NULL) 55 | out_of_memory(); 56 | #if HAVE_STRLCPY 57 | strlcpy(ret, str, n); 58 | #else 59 | strcpy(ret, str); 60 | #endif 61 | return ret; 62 | } 63 | 64 | /* String buffer */ 65 | 66 | typedef struct 67 | { 68 | char *cur; 69 | char *end; 70 | char *start; 71 | } SB; 72 | 73 | static void sb_init(SB *sb) 74 | { 75 | sb->start = (char*) malloc(17); 76 | if (sb->start == NULL) 77 | out_of_memory(); 78 | sb->cur = sb->start; 79 | sb->end = sb->start + 16; 80 | } 81 | 82 | /* sb and need may be evaluated multiple times. */ 83 | #define sb_need(sb, need) do { \ 84 | if ((sb)->end - (sb)->cur < (need)) \ 85 | sb_grow(sb, need); \ 86 | } while (0) 87 | 88 | static void sb_grow(SB *sb, int need) 89 | { 90 | size_t length = sb->cur - sb->start; 91 | size_t alloc = sb->end - sb->start; 92 | 93 | do { 94 | alloc *= 2; 95 | } while (alloc < length + need); 96 | 97 | sb->start = (char*) realloc(sb->start, alloc + 1); 98 | if (sb->start == NULL) 99 | out_of_memory(); 100 | sb->cur = sb->start + length; 101 | sb->end = sb->start + alloc; 102 | } 103 | 104 | static void sb_put(SB *sb, const char *bytes, int count) 105 | { 106 | sb_need(sb, count); 107 | memcpy(sb->cur, bytes, count); 108 | sb->cur += count; 109 | } 110 | 111 | #define sb_putc(sb, c) do { \ 112 | if ((sb)->cur >= (sb)->end) \ 113 | sb_grow(sb, 1); \ 114 | *(sb)->cur++ = (c); \ 115 | } while (0) 116 | 117 | static void sb_puts(SB *sb, const char *str) 118 | { 119 | sb_put(sb, str, strlen(str)); 120 | } 121 | 122 | static char *sb_finish(SB *sb) 123 | { 124 | *sb->cur = 0; 125 | assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); 126 | return sb->start; 127 | } 128 | 129 | static void sb_free(SB *sb) 130 | { 131 | free(sb->start); 132 | } 133 | 134 | /* 135 | * Unicode helper functions 136 | * 137 | * These are taken from the ccan/charset module and customized a bit. 138 | * Putting them here means the compiler can (choose to) inline them, 139 | * and it keeps ccan/json from having a dependency. 140 | */ 141 | 142 | /* 143 | * Type for Unicode codepoints. 144 | * We need our own because wchar_t might be 16 bits. 145 | */ 146 | typedef uint32_t js_uchar_t; 147 | 148 | /* 149 | * Validate a single UTF-8 character starting at @s. 150 | * The string must be null-terminated. 151 | * 152 | * If it's valid, return its length (1 thru 4). 153 | * If it's invalid or clipped, return 0. 154 | * 155 | * This function implements the syntax given in RFC3629, which is 156 | * the same as that given in The Unicode Standard, Version 6.0. 157 | * 158 | * It has the following properties: 159 | * 160 | * * All codepoints U+0000..U+10FFFF may be encoded, 161 | * except for U+D800..U+DFFF, which are reserved 162 | * for UTF-16 surrogate pair encoding. 163 | * * UTF-8 byte sequences longer than 4 bytes are not permitted, 164 | * as they exceed the range of Unicode. 165 | * * The sixty-six Unicode "non-characters" are permitted 166 | * (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF). 167 | */ 168 | static int utf8_validate_cz(const char *s) 169 | { 170 | unsigned char c = *s++; 171 | 172 | if (c <= 0x7F) { /* 00..7F */ 173 | return 1; 174 | } else if (c <= 0xC1) { /* 80..C1 */ 175 | /* Disallow overlong 2-byte sequence. */ 176 | return 0; 177 | } else if (c <= 0xDF) { /* C2..DF */ 178 | /* Make sure subsequent byte is in the range 0x80..0xBF. */ 179 | if (((unsigned char)*s++ & 0xC0) != 0x80) 180 | return 0; 181 | 182 | return 2; 183 | } else if (c <= 0xEF) { /* E0..EF */ 184 | /* Disallow overlong 3-byte sequence. */ 185 | if (c == 0xE0 && (unsigned char)*s < 0xA0) 186 | return 0; 187 | 188 | /* Disallow U+D800..U+DFFF. */ 189 | if (c == 0xED && (unsigned char)*s > 0x9F) 190 | return 0; 191 | 192 | /* Make sure subsequent bytes are in the range 0x80..0xBF. */ 193 | if (((unsigned char)*s++ & 0xC0) != 0x80) 194 | return 0; 195 | if (((unsigned char)*s++ & 0xC0) != 0x80) 196 | return 0; 197 | 198 | return 3; 199 | } else if (c <= 0xF4) { /* F0..F4 */ 200 | /* Disallow overlong 4-byte sequence. */ 201 | if (c == 0xF0 && (unsigned char)*s < 0x90) 202 | return 0; 203 | 204 | /* Disallow codepoints beyond U+10FFFF. */ 205 | if (c == 0xF4 && (unsigned char)*s > 0x8F) 206 | return 0; 207 | 208 | /* Make sure subsequent bytes are in the range 0x80..0xBF. */ 209 | if (((unsigned char)*s++ & 0xC0) != 0x80) 210 | return 0; 211 | if (((unsigned char)*s++ & 0xC0) != 0x80) 212 | return 0; 213 | if (((unsigned char)*s++ & 0xC0) != 0x80) 214 | return 0; 215 | 216 | return 4; 217 | } else { /* F5..FF */ 218 | return 0; 219 | } 220 | } 221 | 222 | /* Validate a null-terminated UTF-8 string. */ 223 | static bool utf8_validate(const char *s) 224 | { 225 | int len; 226 | 227 | for (; *s != 0; s += len) { 228 | len = utf8_validate_cz(s); 229 | if (len == 0) 230 | return false; 231 | } 232 | 233 | return true; 234 | } 235 | 236 | /* 237 | * Read a single UTF-8 character starting at @s, 238 | * returning the length, in bytes, of the character read. 239 | * 240 | * This function assumes input is valid UTF-8, 241 | * and that there are enough characters in front of @s. 242 | */ 243 | static int utf8_read_char(const char *s, js_uchar_t *out) 244 | { 245 | const unsigned char *c = (const unsigned char*) s; 246 | 247 | assert(utf8_validate_cz(s)); 248 | 249 | if (c[0] <= 0x7F) { 250 | /* 00..7F */ 251 | *out = c[0]; 252 | return 1; 253 | } else if (c[0] <= 0xDF) { 254 | /* C2..DF (unless input is invalid) */ 255 | *out = ((js_uchar_t)c[0] & 0x1F) << 6 | 256 | ((js_uchar_t)c[1] & 0x3F); 257 | return 2; 258 | } else if (c[0] <= 0xEF) { 259 | /* E0..EF */ 260 | *out = ((js_uchar_t)c[0] & 0xF) << 12 | 261 | ((js_uchar_t)c[1] & 0x3F) << 6 | 262 | ((js_uchar_t)c[2] & 0x3F); 263 | return 3; 264 | } else { 265 | /* F0..F4 (unless input is invalid) */ 266 | *out = ((js_uchar_t)c[0] & 0x7) << 18 | 267 | ((js_uchar_t)c[1] & 0x3F) << 12 | 268 | ((js_uchar_t)c[2] & 0x3F) << 6 | 269 | ((js_uchar_t)c[3] & 0x3F); 270 | return 4; 271 | } 272 | } 273 | 274 | /* 275 | * Write a single UTF-8 character to @s, 276 | * returning the length, in bytes, of the character written. 277 | * 278 | * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF. 279 | * 280 | * This function will write up to 4 bytes to @out. 281 | */ 282 | static int utf8_write_char(js_uchar_t unicode, char *out) 283 | { 284 | unsigned char *o = (unsigned char*) out; 285 | 286 | failx( 287 | unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF), 288 | 1, 289 | "Illegal Unicode codepoint 0x%08X found", 290 | unicode 291 | ); 292 | 293 | if (unicode <= 0x7F) { 294 | /* U+0000..U+007F */ 295 | *o++ = unicode; 296 | return 1; 297 | } else if (unicode <= 0x7FF) { 298 | /* U+0080..U+07FF */ 299 | *o++ = 0xC0 | unicode >> 6; 300 | *o++ = 0x80 | (unicode & 0x3F); 301 | return 2; 302 | } else if (unicode <= 0xFFFF) { 303 | /* U+0800..U+FFFF */ 304 | *o++ = 0xE0 | unicode >> 12; 305 | *o++ = 0x80 | (unicode >> 6 & 0x3F); 306 | *o++ = 0x80 | (unicode & 0x3F); 307 | return 3; 308 | } else { 309 | /* U+10000..U+10FFFF */ 310 | *o++ = 0xF0 | unicode >> 18; 311 | *o++ = 0x80 | (unicode >> 12 & 0x3F); 312 | *o++ = 0x80 | (unicode >> 6 & 0x3F); 313 | *o++ = 0x80 | (unicode & 0x3F); 314 | return 4; 315 | } 316 | } 317 | 318 | /* 319 | * Compute the Unicode codepoint of a UTF-16 surrogate pair. 320 | * 321 | * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF. 322 | * If they aren't, this function returns false. 323 | */ 324 | static bool from_surrogate_pair(uint16_t uc, uint16_t lc, js_uchar_t *unicode) 325 | { 326 | if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) { 327 | *unicode = 0x10000 + ((((js_uchar_t)uc & 0x3FF) << 10) | (lc & 0x3FF)); 328 | return true; 329 | } else { 330 | return false; 331 | } 332 | } 333 | 334 | /* 335 | * Construct a UTF-16 surrogate pair given a Unicode codepoint. 336 | * 337 | * @unicode must be U+10000..U+10FFFF. 338 | */ 339 | static void to_surrogate_pair(js_uchar_t unicode, uint16_t *uc, uint16_t *lc) 340 | { 341 | js_uchar_t n; 342 | 343 | failx( 344 | unicode >= 0x10000 && unicode <= 0x10FFFF, 345 | 1, 346 | "Cannot construct UTF-16 surrogate pair for Unicode codepoint 0x%08X", 347 | unicode 348 | ); 349 | 350 | n = unicode - 0x10000; 351 | *uc = ((n >> 10) & 0x3FF) | 0xD800; 352 | *lc = (n & 0x3FF) | 0xDC00; 353 | } 354 | 355 | #define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ') 356 | #define is_digit(c) ((c) >= '0' && (c) <= '9') 357 | 358 | static bool parse_value (const char **sp, JsonNode **out); 359 | static bool parse_string (const char **sp, char **out); 360 | static bool parse_number (const char **sp, double *out); 361 | static bool parse_array (const char **sp, JsonNode **out); 362 | static bool parse_object (const char **sp, JsonNode **out); 363 | static bool parse_hex16 (const char **sp, uint16_t *out); 364 | 365 | static bool expect_literal (const char **sp, const char *str); 366 | static void skip_space (const char **sp); 367 | 368 | static void emit_value (SB *out, const JsonNode *node); 369 | static void emit_value_indented (SB *out, const JsonNode *node, const char *space, int indent_level); 370 | static void emit_string (SB *out, const char *str); 371 | static void emit_number (SB *out, double num); 372 | static void emit_array (SB *out, const JsonNode *array); 373 | static void emit_array_indented (SB *out, const JsonNode *array, const char *space, int indent_level); 374 | static void emit_object (SB *out, const JsonNode *object); 375 | static void emit_object_indented (SB *out, const JsonNode *object, const char *space, int indent_level); 376 | 377 | static int write_hex16(char *out, uint16_t val); 378 | 379 | static JsonNode *mknode(JsonTag tag); 380 | static void append_node(JsonNode *parent, JsonNode *child); 381 | static void prepend_node(JsonNode *parent, JsonNode *child); 382 | static void insert_node(JsonNode *parent, JsonNode *child); 383 | static void append_member(JsonNode *object, char *key, JsonNode *value); 384 | 385 | static void (*append_member_node_fn)(JsonNode *parent, JsonNode *child) = append_node; 386 | 387 | /* Assertion-friendly validity checks */ 388 | static bool tag_is_valid(unsigned int tag); 389 | static bool number_is_valid(const char *num); 390 | 391 | JsonNode *json_decode(const char *json) 392 | { 393 | const char *s = json; 394 | JsonNode *ret; 395 | 396 | skip_space(&s); 397 | if (!parse_value(&s, &ret)) 398 | return NULL; 399 | 400 | skip_space(&s); 401 | if (*s != 0) { 402 | json_delete(ret); 403 | return NULL; 404 | } 405 | 406 | return ret; 407 | } 408 | 409 | char *json_encode(const JsonNode *node) 410 | { 411 | return json_stringify(node, NULL); 412 | } 413 | 414 | char *json_encode_string(const char *str) 415 | { 416 | SB sb; 417 | sb_init(&sb); 418 | 419 | emit_string(&sb, str); 420 | 421 | return sb_finish(&sb); 422 | } 423 | 424 | char *json_stringify(const JsonNode *node, const char *space) 425 | { 426 | SB sb; 427 | sb_init(&sb); 428 | 429 | if (space != NULL) 430 | emit_value_indented(&sb, node, space, 0); 431 | else 432 | emit_value(&sb, node); 433 | 434 | return sb_finish(&sb); 435 | } 436 | 437 | void json_delete(JsonNode *node) 438 | { 439 | if (node != NULL) { 440 | json_remove_from_parent(node); 441 | 442 | switch (node->tag) { 443 | case JSON_STRING: 444 | free(node->string_); 445 | break; 446 | case JSON_ARRAY: 447 | case JSON_OBJECT: 448 | { 449 | JsonNode *child, *next; 450 | for (child = node->children.head; child != NULL; child = next) { 451 | next = child->next; 452 | json_delete(child); 453 | } 454 | break; 455 | } 456 | default:; 457 | } 458 | 459 | free(node); 460 | } 461 | } 462 | 463 | bool json_validate(const char *json) 464 | { 465 | const char *s = json; 466 | 467 | skip_space(&s); 468 | if (!parse_value(&s, NULL)) 469 | return false; 470 | 471 | skip_space(&s); 472 | if (*s != 0) 473 | return false; 474 | 475 | return true; 476 | } 477 | 478 | JsonNode *json_find_element(JsonNode *array, int index) 479 | { 480 | JsonNode *element; 481 | int i = 0; 482 | 483 | if (array == NULL || array->tag != JSON_ARRAY) 484 | return NULL; 485 | 486 | json_foreach(element, array) { 487 | if (i == index) 488 | return element; 489 | i++; 490 | } 491 | 492 | return NULL; 493 | } 494 | 495 | JsonNode *json_find_member(JsonNode *object, const char *name) 496 | { 497 | JsonNode *member; 498 | 499 | if (object == NULL || object->tag != JSON_OBJECT) 500 | return NULL; 501 | 502 | json_foreach(member, object) 503 | if (strcmp(member->key, name) == 0) 504 | return member; 505 | 506 | return NULL; 507 | } 508 | 509 | JsonNode *json_first_child(const JsonNode *node) 510 | { 511 | if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT)) 512 | return node->children.head; 513 | return NULL; 514 | } 515 | 516 | static JsonNode *mknode(JsonTag tag) 517 | { 518 | JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode)); 519 | if (ret == NULL) 520 | out_of_memory(); 521 | ret->tag = tag; 522 | return ret; 523 | } 524 | 525 | JsonNode *json_mknull(void) 526 | { 527 | return mknode(JSON_NULL); 528 | } 529 | 530 | JsonNode *json_mkbool(bool b) 531 | { 532 | JsonNode *ret = mknode(JSON_BOOL); 533 | ret->bool_ = b; 534 | return ret; 535 | } 536 | 537 | static JsonNode *mkstring(char *s) 538 | { 539 | JsonNode *ret = mknode(JSON_STRING); 540 | ret->string_ = s; 541 | return ret; 542 | } 543 | 544 | JsonNode *json_mkstring(const char *s) 545 | { 546 | return mkstring(json_strdup(s)); 547 | } 548 | 549 | JsonNode *json_mknumber(double n) 550 | { 551 | JsonNode *node = mknode(JSON_NUMBER); 552 | node->number_ = n; 553 | return node; 554 | } 555 | 556 | JsonNode *json_mkarray(void) 557 | { 558 | return mknode(JSON_ARRAY); 559 | } 560 | 561 | JsonNode *json_mkobject(void) 562 | { 563 | return mknode(JSON_OBJECT); 564 | } 565 | 566 | static void append_node(JsonNode *parent, JsonNode *child) 567 | { 568 | if (!child) return; 569 | child->parent = parent; 570 | child->prev = parent->children.tail; 571 | child->next = NULL; 572 | 573 | if (parent->children.tail != NULL) 574 | parent->children.tail->next = child; 575 | else 576 | parent->children.head = child; 577 | parent->children.tail = child; 578 | } 579 | 580 | static void prepend_node(JsonNode *parent, JsonNode *child) 581 | { 582 | if (!child) return; 583 | child->parent = parent; 584 | child->prev = NULL; 585 | child->next = parent->children.head; 586 | 587 | if (parent->children.head != NULL) 588 | parent->children.head->prev = child; 589 | else 590 | parent->children.tail = child; 591 | parent->children.head = child; 592 | } 593 | 594 | static void insert_node(JsonNode *parent, JsonNode *child) 595 | { 596 | if (!child) return; 597 | JsonNode *this = parent->children.head; 598 | 599 | while (this != NULL && strcmp(this->key, child->key)) 600 | this = this->next; 601 | 602 | if (this != NULL) 603 | { 604 | /* we found a matching key, insert before this node */ 605 | if (this->prev == NULL) 606 | { 607 | prepend_node(parent, child); 608 | } 609 | else 610 | { 611 | child->parent = parent; 612 | child->next = this->next; 613 | child->prev = this->prev; 614 | this->prev->next = child; 615 | this->prev = child; 616 | } 617 | json_delete(this); 618 | } 619 | else 620 | append_node(parent, child); 621 | } 622 | 623 | static void append_member(JsonNode *object, char *key, JsonNode *value) 624 | { 625 | if (!value) return; 626 | value->key = key; 627 | append_member_node_fn(object, value); 628 | } 629 | 630 | void json_append_element(JsonNode *array, JsonNode *element) 631 | { 632 | if (!element) return; 633 | failx( 634 | array->tag == JSON_ARRAY, 635 | 1, 636 | "Cannot append %s to non-array %s", 637 | json_encode(element), 638 | json_encode(array) 639 | ); 640 | assert(element->parent == NULL); 641 | 642 | append_node(array, element); 643 | } 644 | 645 | void json_prepend_element(JsonNode *array, JsonNode *element) 646 | { 647 | if (!element) return; 648 | failx( 649 | array->tag == JSON_ARRAY, 650 | 1, 651 | "Cannot append %s to non-array %s", 652 | json_encode(element), 653 | json_encode(array) 654 | ); 655 | assert(element->parent == NULL); 656 | 657 | prepend_node(array, element); 658 | } 659 | 660 | void json_append_member(JsonNode *object, const char *key, JsonNode *value) 661 | { 662 | if (!value) return; 663 | failx( 664 | object->tag == JSON_OBJECT, 665 | 1, 666 | "Cannot add {\"%s\":%s} to non-object %s", 667 | key, 668 | json_encode(value), 669 | json_encode(object) 670 | ); 671 | assert(value->parent == NULL); 672 | 673 | append_member(object, json_strdup(key), value); 674 | } 675 | 676 | void json_prepend_member(JsonNode *object, const char *key, JsonNode *value) 677 | { 678 | if (!value) return; 679 | failx( 680 | object->tag == JSON_OBJECT, 681 | 1, 682 | "Cannot add {\"%s\":%s} to non-object %s", 683 | key, 684 | json_encode(value), 685 | json_encode(object) 686 | ); 687 | assert(value->parent == NULL); 688 | 689 | value->key = json_strdup(key); 690 | prepend_node(object, value); 691 | } 692 | 693 | void json_dedup_members(bool b) 694 | { 695 | append_member_node_fn = b ? insert_node : append_node; 696 | } 697 | 698 | void json_remove_from_parent(JsonNode *node) 699 | { 700 | JsonNode *parent = node->parent; 701 | 702 | if (parent != NULL) { 703 | if (node->prev != NULL) 704 | node->prev->next = node->next; 705 | else 706 | parent->children.head = node->next; 707 | if (node->next != NULL) 708 | node->next->prev = node->prev; 709 | else 710 | parent->children.tail = node->prev; 711 | 712 | free(node->key); 713 | 714 | node->parent = NULL; 715 | node->prev = node->next = NULL; 716 | node->key = NULL; 717 | } 718 | } 719 | 720 | static bool parse_value(const char **sp, JsonNode **out) 721 | { 722 | const char *s = *sp; 723 | 724 | switch (*s) { 725 | case 'n': 726 | if (expect_literal(&s, "null")) { 727 | if (out) 728 | *out = json_mknull(); 729 | *sp = s; 730 | return true; 731 | } 732 | return false; 733 | 734 | case 'f': 735 | if (expect_literal(&s, "false")) { 736 | if (out) 737 | *out = json_mkbool(false); 738 | *sp = s; 739 | return true; 740 | } 741 | return false; 742 | 743 | case 't': 744 | if (expect_literal(&s, "true")) { 745 | if (out) 746 | *out = json_mkbool(true); 747 | *sp = s; 748 | return true; 749 | } 750 | return false; 751 | 752 | case '"': { 753 | char *str; 754 | if (parse_string(&s, out ? &str : NULL)) { 755 | if (out) 756 | *out = mkstring(str); 757 | *sp = s; 758 | return true; 759 | } 760 | return false; 761 | } 762 | 763 | case '[': 764 | if (parse_array(&s, out)) { 765 | *sp = s; 766 | return true; 767 | } 768 | return false; 769 | 770 | case '{': 771 | if (parse_object(&s, out)) { 772 | *sp = s; 773 | return true; 774 | } 775 | return false; 776 | 777 | default: { 778 | double num; 779 | if (parse_number(&s, out ? &num : NULL)) { 780 | if (out) 781 | *out = json_mknumber(num); 782 | *sp = s; 783 | return true; 784 | } 785 | return false; 786 | } 787 | } 788 | } 789 | 790 | static bool parse_array(const char **sp, JsonNode **out) 791 | { 792 | const char *s = *sp; 793 | JsonNode *ret = out ? json_mkarray() : NULL; 794 | JsonNode *element; 795 | 796 | if (*s++ != '[') 797 | goto failure; 798 | skip_space(&s); 799 | 800 | if (*s == ']') { 801 | s++; 802 | goto success; 803 | } 804 | 805 | for (;;) { 806 | if (!parse_value(&s, out ? &element : NULL)) 807 | goto failure; 808 | skip_space(&s); 809 | 810 | if (out) 811 | json_append_element(ret, element); 812 | 813 | if (*s == ']') { 814 | s++; 815 | goto success; 816 | } 817 | 818 | if (*s++ != ',') 819 | goto failure; 820 | skip_space(&s); 821 | } 822 | 823 | success: 824 | *sp = s; 825 | if (out) 826 | *out = ret; 827 | return true; 828 | 829 | failure: 830 | json_delete(ret); 831 | return false; 832 | } 833 | 834 | static bool parse_object(const char **sp, JsonNode **out) 835 | { 836 | const char *s = *sp; 837 | JsonNode *ret = out ? json_mkobject() : NULL; 838 | char *key; 839 | JsonNode *value; 840 | 841 | if (*s++ != '{') 842 | goto failure; 843 | skip_space(&s); 844 | 845 | if (*s == '}') { 846 | s++; 847 | goto success; 848 | } 849 | 850 | for (;;) { 851 | if (!parse_string(&s, out ? &key : NULL)) 852 | goto failure; 853 | skip_space(&s); 854 | 855 | if (*s++ != ':') 856 | goto failure_free_key; 857 | skip_space(&s); 858 | 859 | if (!parse_value(&s, out ? &value : NULL)) 860 | goto failure_free_key; 861 | skip_space(&s); 862 | 863 | if (out) 864 | append_member(ret, key, value); 865 | 866 | if (*s == '}') { 867 | s++; 868 | goto success; 869 | } 870 | 871 | if (*s++ != ',') 872 | goto failure; 873 | skip_space(&s); 874 | } 875 | 876 | success: 877 | *sp = s; 878 | if (out) 879 | *out = ret; 880 | return true; 881 | 882 | failure_free_key: 883 | if (out) 884 | free(key); 885 | failure: 886 | json_delete(ret); 887 | return false; 888 | } 889 | 890 | bool parse_string(const char **sp, char **out) 891 | { 892 | const char *s = *sp; 893 | SB sb; 894 | char throwaway_buffer[4]; 895 | /* enough space for a UTF-8 character */ 896 | char *b; 897 | 898 | if (*s++ != '"') 899 | return false; 900 | 901 | if (out) { 902 | sb_init(&sb); 903 | sb_need(&sb, 4); 904 | b = sb.cur; 905 | } else { 906 | b = throwaway_buffer; 907 | } 908 | 909 | while (*s != '"') { 910 | unsigned char c = *s++; 911 | 912 | /* Parse next character, and write it to b. */ 913 | if (c == '\\') { 914 | c = *s++; 915 | switch (c) { 916 | case '"': 917 | case '\\': 918 | case '/': 919 | *b++ = c; 920 | break; 921 | case 'b': 922 | *b++ = '\b'; 923 | break; 924 | case 'f': 925 | *b++ = '\f'; 926 | break; 927 | case 'n': 928 | *b++ = '\n'; 929 | break; 930 | case 'r': 931 | *b++ = '\r'; 932 | break; 933 | case 't': 934 | *b++ = '\t'; 935 | break; 936 | case 'u': 937 | { 938 | uint16_t uc, lc; 939 | js_uchar_t unicode; 940 | 941 | if (!parse_hex16(&s, &uc)) 942 | goto failed; 943 | 944 | if (uc >= 0xD800 && uc <= 0xDFFF) { 945 | /* Handle UTF-16 surrogate pair. */ 946 | if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc)) 947 | goto failed; /* Incomplete surrogate pair. */ 948 | if (!from_surrogate_pair(uc, lc, &unicode)) 949 | goto failed; /* Invalid surrogate pair. */ 950 | } else if (uc == 0) { 951 | /* Disallow "\u0000". */ 952 | goto failed; 953 | } else { 954 | unicode = uc; 955 | } 956 | 957 | b += utf8_write_char(unicode, b); 958 | break; 959 | } 960 | default: 961 | /* Invalid escape */ 962 | goto failed; 963 | } 964 | } else if (c <= 0x1F) { 965 | /* Control characters are not allowed in string literals. */ 966 | goto failed; 967 | } else { 968 | /* Validate and echo a UTF-8 character. */ 969 | int len; 970 | 971 | s--; 972 | len = utf8_validate_cz(s); 973 | if (len == 0) 974 | goto failed; /* Invalid UTF-8 character. */ 975 | 976 | while (len--) 977 | *b++ = *s++; 978 | } 979 | 980 | /* 981 | * Update sb to know about the new bytes, 982 | * and set up b to write another character. 983 | */ 984 | if (out) { 985 | sb.cur = b; 986 | sb_need(&sb, 4); 987 | b = sb.cur; 988 | } else { 989 | b = throwaway_buffer; 990 | } 991 | } 992 | s++; 993 | 994 | if (out) 995 | *out = sb_finish(&sb); 996 | *sp = s; 997 | return true; 998 | 999 | failed: 1000 | if (out) 1001 | sb_free(&sb); 1002 | return false; 1003 | } 1004 | 1005 | /* 1006 | * The JSON spec says that a number shall follow this precise pattern 1007 | * (spaces and quotes added for readability): 1008 | * '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)? 1009 | * 1010 | * However, some JSON parsers are more liberal. For instance, PHP accepts 1011 | * '.5' and '1.'. JSON.parse accepts '+3'. 1012 | * 1013 | * This function takes the strict approach. 1014 | */ 1015 | bool parse_number(const char **sp, double *out) 1016 | { 1017 | const char *s = *sp; 1018 | 1019 | /* '-'? */ 1020 | if (*s == '-') 1021 | s++; 1022 | 1023 | /* (0 | [1-9][0-9]*) */ 1024 | if (*s == '0') { 1025 | s++; 1026 | } else { 1027 | if (!is_digit(*s)) 1028 | return false; 1029 | do { 1030 | s++; 1031 | } while (is_digit(*s)); 1032 | } 1033 | 1034 | /* ('.' [0-9]+)? */ 1035 | if (*s == '.') { 1036 | s++; 1037 | if (!is_digit(*s)) 1038 | return false; 1039 | do { 1040 | s++; 1041 | } while (is_digit(*s)); 1042 | } 1043 | 1044 | /* ([Ee] [+-]? [0-9]+)? */ 1045 | if (*s == 'E' || *s == 'e') { 1046 | s++; 1047 | if (*s == '+' || *s == '-') 1048 | s++; 1049 | if (!is_digit(*s)) 1050 | return false; 1051 | do { 1052 | s++; 1053 | } while (is_digit(*s)); 1054 | } 1055 | 1056 | if (out) 1057 | *out = strtod(*sp, NULL); 1058 | 1059 | *sp = s; 1060 | return true; 1061 | } 1062 | 1063 | static void skip_space(const char **sp) 1064 | { 1065 | const char *s = *sp; 1066 | while (is_space(*s)) 1067 | s++; 1068 | *sp = s; 1069 | } 1070 | 1071 | static void emit_value(SB *out, const JsonNode *node) 1072 | { 1073 | assert(tag_is_valid(node->tag)); 1074 | switch (node->tag) { 1075 | case JSON_NULL: 1076 | sb_puts(out, "null"); 1077 | break; 1078 | case JSON_BOOL: 1079 | sb_puts(out, node->bool_ ? "true" : "false"); 1080 | break; 1081 | case JSON_STRING: 1082 | emit_string(out, node->string_); 1083 | break; 1084 | case JSON_NUMBER: 1085 | emit_number(out, node->number_); 1086 | break; 1087 | case JSON_ARRAY: 1088 | emit_array(out, node); 1089 | break; 1090 | case JSON_OBJECT: 1091 | emit_object(out, node); 1092 | break; 1093 | default: 1094 | assert(false); 1095 | } 1096 | } 1097 | 1098 | void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level) 1099 | { 1100 | assert(tag_is_valid(node->tag)); 1101 | switch (node->tag) { 1102 | case JSON_NULL: 1103 | sb_puts(out, "null"); 1104 | break; 1105 | case JSON_BOOL: 1106 | sb_puts(out, node->bool_ ? "true" : "false"); 1107 | break; 1108 | case JSON_STRING: 1109 | emit_string(out, node->string_); 1110 | break; 1111 | case JSON_NUMBER: 1112 | emit_number(out, node->number_); 1113 | break; 1114 | case JSON_ARRAY: 1115 | emit_array_indented(out, node, space, indent_level); 1116 | break; 1117 | case JSON_OBJECT: 1118 | emit_object_indented(out, node, space, indent_level); 1119 | break; 1120 | default: 1121 | assert(false); 1122 | } 1123 | } 1124 | 1125 | static void emit_array(SB *out, const JsonNode *array) 1126 | { 1127 | const JsonNode *element; 1128 | 1129 | sb_putc(out, '['); 1130 | json_foreach(element, array) { 1131 | emit_value(out, element); 1132 | if (element->next != NULL) 1133 | sb_putc(out, ','); 1134 | } 1135 | sb_putc(out, ']'); 1136 | } 1137 | 1138 | static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level) 1139 | { 1140 | const JsonNode *element = array->children.head; 1141 | int i; 1142 | 1143 | if (element == NULL) { 1144 | sb_puts(out, "[]"); 1145 | return; 1146 | } 1147 | 1148 | sb_puts(out, "[\n"); 1149 | while (element != NULL) { 1150 | for (i = 0; i < indent_level + 1; i++) 1151 | sb_puts(out, space); 1152 | emit_value_indented(out, element, space, indent_level + 1); 1153 | 1154 | element = element->next; 1155 | sb_puts(out, element != NULL ? ",\n" : "\n"); 1156 | } 1157 | for (i = 0; i < indent_level; i++) 1158 | sb_puts(out, space); 1159 | sb_putc(out, ']'); 1160 | } 1161 | 1162 | static void emit_object(SB *out, const JsonNode *object) 1163 | { 1164 | const JsonNode *member; 1165 | 1166 | sb_putc(out, '{'); 1167 | json_foreach(member, object) { 1168 | emit_string(out, member->key); 1169 | sb_putc(out, ':'); 1170 | emit_value(out, member); 1171 | if (member->next != NULL) 1172 | sb_putc(out, ','); 1173 | } 1174 | sb_putc(out, '}'); 1175 | } 1176 | 1177 | static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level) 1178 | { 1179 | const JsonNode *member = object->children.head; 1180 | int i; 1181 | 1182 | if (member == NULL) { 1183 | sb_puts(out, "{}"); 1184 | return; 1185 | } 1186 | 1187 | sb_puts(out, "{\n"); 1188 | while (member != NULL) { 1189 | for (i = 0; i < indent_level + 1; i++) 1190 | sb_puts(out, space); 1191 | emit_string(out, member->key); 1192 | sb_puts(out, ": "); 1193 | emit_value_indented(out, member, space, indent_level + 1); 1194 | 1195 | member = member->next; 1196 | sb_puts(out, member != NULL ? ",\n" : "\n"); 1197 | } 1198 | for (i = 0; i < indent_level; i++) 1199 | sb_puts(out, space); 1200 | sb_putc(out, '}'); 1201 | } 1202 | 1203 | void emit_string(SB *out, const char *str) 1204 | { 1205 | bool escape_unicode = false; 1206 | const char *s = str; 1207 | char *b; 1208 | 1209 | assert(utf8_validate(str)); 1210 | 1211 | /* 1212 | * 14 bytes is enough space to write up to two 1213 | * \uXXXX escapes and two quotation marks. 1214 | */ 1215 | sb_need(out, 14); 1216 | b = out->cur; 1217 | 1218 | *b++ = '"'; 1219 | while (*s != 0) { 1220 | unsigned char c = *s++; 1221 | 1222 | /* Encode the next character, and write it to b. */ 1223 | switch (c) { 1224 | case '"': 1225 | *b++ = '\\'; 1226 | *b++ = '"'; 1227 | break; 1228 | case '\\': 1229 | *b++ = '\\'; 1230 | *b++ = '\\'; 1231 | break; 1232 | case '\b': 1233 | *b++ = '\\'; 1234 | *b++ = 'b'; 1235 | break; 1236 | case '\f': 1237 | *b++ = '\\'; 1238 | *b++ = 'f'; 1239 | break; 1240 | case '\n': 1241 | *b++ = '\\'; 1242 | *b++ = 'n'; 1243 | break; 1244 | case '\r': 1245 | *b++ = '\\'; 1246 | *b++ = 'r'; 1247 | break; 1248 | case '\t': 1249 | *b++ = '\\'; 1250 | *b++ = 't'; 1251 | break; 1252 | default: { 1253 | int len; 1254 | 1255 | s--; 1256 | len = utf8_validate_cz(s); 1257 | 1258 | if (len == 0) { 1259 | /* 1260 | * Handle invalid UTF-8 character gracefully in production 1261 | * by writing a replacement character (U+FFFD) 1262 | * and skipping a single byte. 1263 | * 1264 | * This should never happen when assertions are enabled 1265 | * due to the assertion at the beginning of this function. 1266 | */ 1267 | assert(false); 1268 | if (escape_unicode) { 1269 | #if HAVE_STRLCPY 1270 | strlcpy(b, "\\uFFFD", out->end - out->start ); 1271 | #else 1272 | strcpy(b, "\\uFFFD"); 1273 | #endif 1274 | b += 6; 1275 | } else { 1276 | *b++ = (char)0xEF; 1277 | *b++ = (char)0xBF; 1278 | *b++ = (char)0xBD; 1279 | } 1280 | s++; 1281 | } else if (c < 0x1F || (c >= 0x80 && escape_unicode)) { 1282 | /* Encode using \u.... */ 1283 | uint32_t unicode; 1284 | 1285 | s += utf8_read_char(s, &unicode); 1286 | 1287 | if (unicode <= 0xFFFF) { 1288 | *b++ = '\\'; 1289 | *b++ = 'u'; 1290 | b += write_hex16(b, unicode); 1291 | } else { 1292 | /* Produce a surrogate pair. */ 1293 | uint16_t uc, lc; 1294 | assert(unicode <= 0x10FFFF); 1295 | to_surrogate_pair(unicode, &uc, &lc); 1296 | *b++ = '\\'; 1297 | *b++ = 'u'; 1298 | b += write_hex16(b, uc); 1299 | *b++ = '\\'; 1300 | *b++ = 'u'; 1301 | b += write_hex16(b, lc); 1302 | } 1303 | } else { 1304 | /* Write the character directly. */ 1305 | while (len--) 1306 | *b++ = *s++; 1307 | } 1308 | 1309 | break; 1310 | } 1311 | } 1312 | 1313 | /* 1314 | * Update *out to know about the new bytes, 1315 | * and set up b to write another encoded character. 1316 | */ 1317 | out->cur = b; 1318 | sb_need(out, 14); 1319 | b = out->cur; 1320 | } 1321 | *b++ = '"'; 1322 | 1323 | out->cur = b; 1324 | } 1325 | 1326 | static void emit_number(SB *out, double num) 1327 | { 1328 | /* 1329 | * This isn't exactly how JavaScript renders numbers, 1330 | * but it should produce valid JSON for reasonable numbers 1331 | * preserve precision well enough, and avoid some oddities 1332 | * like 0.3 -> 0.299999999999999988898 . 1333 | */ 1334 | char buf[64]; 1335 | snprintf(buf, sizeof(buf), "%.16g", num); 1336 | 1337 | if (number_is_valid(buf)) 1338 | sb_puts(out, buf); 1339 | else 1340 | sb_puts(out, "null"); 1341 | } 1342 | 1343 | static bool tag_is_valid(unsigned int tag) 1344 | { 1345 | return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT); 1346 | } 1347 | 1348 | static bool number_is_valid(const char *num) 1349 | { 1350 | return (parse_number(&num, NULL) && *num == '\0'); 1351 | } 1352 | 1353 | static bool expect_literal(const char **sp, const char *str) 1354 | { 1355 | const char *s = *sp; 1356 | 1357 | while (*str != '\0') 1358 | if (*s++ != *str++) 1359 | return false; 1360 | 1361 | *sp = s; 1362 | return true; 1363 | } 1364 | 1365 | /* 1366 | * Parses exactly 4 hex characters (capital or lowercase). 1367 | * Fails if any input chars are not [0-9A-Fa-f]. 1368 | */ 1369 | static bool parse_hex16(const char **sp, uint16_t *out) 1370 | { 1371 | const char *s = *sp; 1372 | uint16_t ret = 0; 1373 | uint16_t i; 1374 | uint16_t tmp; 1375 | char c; 1376 | 1377 | for (i = 0; i < 4; i++) { 1378 | c = *s++; 1379 | if (c >= '0' && c <= '9') 1380 | tmp = c - '0'; 1381 | else if (c >= 'A' && c <= 'F') 1382 | tmp = c - 'A' + 10; 1383 | else if (c >= 'a' && c <= 'f') 1384 | tmp = c - 'a' + 10; 1385 | else 1386 | return false; 1387 | 1388 | ret <<= 4; 1389 | ret += tmp; 1390 | } 1391 | 1392 | if (out) 1393 | *out = ret; 1394 | *sp = s; 1395 | return true; 1396 | } 1397 | 1398 | /* 1399 | * Encodes a 16-bit number into hexadecimal, 1400 | * writing exactly 4 hex chars. 1401 | */ 1402 | static int write_hex16(char *out, uint16_t val) 1403 | { 1404 | const char *hex = "0123456789ABCDEF"; 1405 | 1406 | *out++ = hex[(val >> 12) & 0xF]; 1407 | *out++ = hex[(val >> 8) & 0xF]; 1408 | *out++ = hex[(val >> 4) & 0xF]; 1409 | *out++ = hex[ val & 0xF]; 1410 | 1411 | return 4; 1412 | } 1413 | 1414 | bool json_check(const JsonNode *node, char errmsg[256]) 1415 | { 1416 | #define problem(...) do { \ 1417 | if (errmsg != NULL) \ 1418 | snprintf(errmsg, 256, __VA_ARGS__); \ 1419 | return false; \ 1420 | } while (0) 1421 | 1422 | if (node->key != NULL && !utf8_validate(node->key)) 1423 | problem("key contains invalid UTF-8"); 1424 | 1425 | if (!tag_is_valid(node->tag)) 1426 | problem("tag is invalid (%u)", node->tag); 1427 | 1428 | if (node->tag == JSON_BOOL) { 1429 | if (node->bool_ != false && node->bool_ != true) 1430 | problem("bool_ is neither false (%d) nor true (%d)", (int)false, (int)true); 1431 | } else if (node->tag == JSON_STRING) { 1432 | if (node->string_ == NULL) 1433 | problem("string_ is NULL"); 1434 | if (!utf8_validate(node->string_)) 1435 | problem("string_ contains invalid UTF-8"); 1436 | } else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) { 1437 | JsonNode *head = node->children.head; 1438 | JsonNode *tail = node->children.tail; 1439 | 1440 | if (head == NULL || tail == NULL) { 1441 | if (head != NULL) 1442 | problem("tail is NULL, but head is not"); 1443 | if (tail != NULL) 1444 | problem("head is NULL, but tail is not"); 1445 | } else { 1446 | JsonNode *child; 1447 | JsonNode *last = NULL; 1448 | 1449 | if (head->prev != NULL) 1450 | problem("First child's prev pointer is not NULL"); 1451 | 1452 | for (child = head; child != NULL; last = child, child = child->next) { 1453 | if (child == node) 1454 | problem("node is its own child"); 1455 | if (child->next == child) 1456 | problem("child->next == child (cycle)"); 1457 | if (child->next == head) 1458 | problem("child->next == head (cycle)"); 1459 | 1460 | if (child->parent != node) 1461 | problem("child does not point back to parent"); 1462 | if (child->next != NULL && child->next->prev != child) 1463 | problem("child->next does not point back to child"); 1464 | 1465 | if (node->tag == JSON_ARRAY && child->key != NULL) 1466 | problem("Array element's key is not NULL"); 1467 | if (node->tag == JSON_OBJECT && child->key == NULL) 1468 | problem("Object member's key is NULL"); 1469 | 1470 | if (!json_check(child, errmsg)) 1471 | return false; 1472 | } 1473 | 1474 | if (last != tail) 1475 | problem("tail does not match pointer found by starting at head and following next links"); 1476 | } 1477 | } 1478 | 1479 | return true; 1480 | 1481 | #undef problem 1482 | } 1483 | -------------------------------------------------------------------------------- /json.h: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) 3 | All rights reserved. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | */ 23 | 24 | #ifndef CCAN_JSON_H 25 | #define CCAN_JSON_H 26 | 27 | #include 28 | #include 29 | 30 | typedef enum { 31 | JSON_NULL, 32 | JSON_BOOL, 33 | JSON_STRING, 34 | JSON_NUMBER, 35 | JSON_ARRAY, 36 | JSON_OBJECT, 37 | } JsonTag; 38 | 39 | typedef struct JsonNode JsonNode; 40 | 41 | struct JsonNode 42 | { 43 | /* only if parent is an object or array (NULL otherwise) */ 44 | JsonNode *parent; 45 | JsonNode *prev, *next; 46 | 47 | /* only if parent is an object (NULL otherwise) */ 48 | char *key; /* Must be valid UTF-8. */ 49 | 50 | JsonTag tag; 51 | union { 52 | /* JSON_BOOL */ 53 | bool bool_; 54 | 55 | /* JSON_STRING */ 56 | char *string_; /* Must be valid UTF-8. */ 57 | 58 | /* JSON_NUMBER */ 59 | double number_; 60 | 61 | /* JSON_ARRAY */ 62 | /* JSON_OBJECT */ 63 | struct { 64 | JsonNode *head, *tail; 65 | } children; 66 | }; 67 | }; 68 | 69 | /*** Encoding, decoding, and validation ***/ 70 | 71 | JsonNode *json_decode (const char *json); 72 | char *json_encode (const JsonNode *node); 73 | char *json_encode_string (const char *str); 74 | char *json_stringify (const JsonNode *node, const char *space); 75 | void json_delete (JsonNode *node); 76 | 77 | bool json_validate (const char *json); 78 | 79 | /*** Lookup and traversal ***/ 80 | 81 | JsonNode *json_find_element (JsonNode *array, int index); 82 | JsonNode *json_find_member (JsonNode *object, const char *key); 83 | 84 | JsonNode *json_first_child (const JsonNode *node); 85 | 86 | #define json_foreach(i, object_or_array) \ 87 | for ((i) = json_first_child(object_or_array); \ 88 | (i) != NULL; \ 89 | (i) = (i)->next) 90 | 91 | /*** Construction and manipulation ***/ 92 | 93 | JsonNode *json_mknull(void); 94 | JsonNode *json_mkbool(bool b); 95 | JsonNode *json_mkstring(const char *s); 96 | JsonNode *json_mknumber(double n); 97 | JsonNode *json_mkarray(void); 98 | JsonNode *json_mkobject(void); 99 | 100 | void json_append_element(JsonNode *array, JsonNode *element); 101 | void json_prepend_element(JsonNode *array, JsonNode *element); 102 | void json_append_member(JsonNode *object, const char *key, JsonNode *value); 103 | void json_prepend_member(JsonNode *object, const char *key, JsonNode *value); 104 | void json_dedup_members(bool b); 105 | 106 | void json_remove_from_parent(JsonNode *node); 107 | 108 | /*** Debugging ***/ 109 | 110 | /* 111 | * Look for structure and encoding problems in a JsonNode or its descendents. 112 | * 113 | * If a problem is detected, return false, writing a description of the problem 114 | * to errmsg (unless errmsg is NULL). 115 | */ 116 | bool json_check(const JsonNode *node, char errmsg[256]); 117 | 118 | #endif 119 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project('jo', 'c', 2 | version: '1.9', 3 | license: 'GPL-2.0-or-later', 4 | meson_version: '>=0.57.0', 5 | default_options: ['warning_level=3', 'optimization=2']) 6 | 7 | 8 | PACKAGE_VERSION = meson.project_version() 9 | 10 | cc = meson.get_compiler('c') 11 | 12 | headers = [ 13 | 'stddef.h', 14 | 'stdint.h', 15 | 'stdlib.h', 16 | 'string.h', 17 | 'unistd.h', 18 | 'stdbool.h' 19 | ] 20 | 21 | functions = [ 22 | 'strchr', 23 | 'strrchr', 24 | 'strlcpy', 25 | 'strlcat', 26 | 'snprintf', 27 | 'pledge', 28 | 'err', 29 | 'errx' 30 | ] 31 | 32 | foreach h: headers 33 | cc.has_header(h, required: true) 34 | endforeach 35 | foreach f: functions 36 | add_project_arguments( 37 | '-DHAVE_@0@='.format(f.to_upper()) + 38 | cc.has_function(f).to_int().to_string(), 39 | language: 'c') 40 | endforeach 41 | 42 | add_project_arguments('-DPACKAGE_VERSION="@0@"'.format(PACKAGE_VERSION), 43 | language: 'c') 44 | 45 | 46 | pandoc = find_program('pandoc', required: false) 47 | if not pandoc.found() 48 | warning('pandoc not found, man pages rebuild will not be possible') 49 | jo1 = 'jo.1' 50 | else 51 | pandoc_commands = [pandoc, '-s', '-w', 'man', '-f', 'markdown', '-o'] 52 | jo1 = custom_target('jo.1', 53 | output: 'jo.1', 54 | input: 'jo.pandoc', 55 | build_always_stale: true, 56 | command: [pandoc_commands, '@OUTPUT@', '@INPUT@']).full_path() 57 | run_command(pandoc_commands, 58 | join_paths(meson.current_build_dir(), 'jo.1'), 59 | join_paths(meson.current_source_dir(), 'jo.pandoc'), 60 | check: false) 61 | custom_target('jo.md', 62 | output: 'jo.md', 63 | input: 'jo.pandoc', 64 | build_always_stale: true, 65 | command: [pandoc, '-s', '-w', 'gfm', '-f', 'markdown-smart', '-o', '@OUTPUT@', '@INPUT@']) 66 | endif 67 | 68 | install_man(jo1) 69 | 70 | bashcomp = dependency('bash-completion', required: false) 71 | if bashcomp.found() 72 | bashcompdir = bashcomp.get_variable(pkgconfig: 'completionsdir') 73 | else 74 | bashcompdir = join_paths(get_option('sysconfdir'), 'bash_completion.d') 75 | endif 76 | 77 | install_data('jo.bash', install_dir: bashcompdir) 78 | 79 | m_dep = cc.find_library('m', required : false) 80 | 81 | executable('jo', 82 | 'jo.c', 83 | 'base64.c', 84 | 'base64.h', 85 | 'json.c', 86 | dependencies: m_dep, 87 | install: true) 88 | 89 | summary({'Prefix': get_option('prefix'), 90 | 'C compiler': cc.get_id(), 91 | 'Pandoc': pandoc, 92 | 'Bash completion': join_paths(bashcompdir, 'jo.bash'), 93 | }) 94 | -------------------------------------------------------------------------------- /press.md: -------------------------------------------------------------------------------- 1 | ## "Press" reports 2 | 3 | * [Hacker News](https://news.ycombinator.com/item?id=11230023) 4 | * [Lobsters](https://lobste.rs/s/tyehi1/a_shell_command_to_create_json_jo) 5 | * [reddit](https://www.reddit.com/r/programming/comments/49sx6x/a_shell_command_to_create_json_jo) 6 | * [Hacker News](https://news.ycombinator.com/item?id=11272678) 7 | * [Trivium](http://chneukirchen.org/trivium/2016-05-13) 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /rpm-build/jo.spec: -------------------------------------------------------------------------------- 1 | Name: jo 2 | Version: 1.9 3 | Release: 2%{?dist} 4 | Summary: jo is a small utility to create JSON objects 5 | 6 | License: GPL2 7 | URL: https://github.com/jpmens/jo 8 | Source0: https://github.com/jpmens/jo/releases/download/%{version}/jo-%{version}.tar.gz 9 | 10 | BuildRequires: autoconf 11 | BuildRequires: pandoc 12 | 13 | %description 14 | jo is a small utility to create JSON objects 15 | 16 | %prep 17 | %setup -q 18 | 19 | 20 | %build 21 | %configure 22 | make %{?_smp_mflags} 23 | make check 24 | 25 | %install 26 | rm -rf $RPM_BUILD_ROOT 27 | %make_install 28 | 29 | 30 | %files 31 | %doc 32 | %{_bindir}/* 33 | %{_mandir}/man1/* 34 | %if 0%{?suse_version} 35 | %{_datadir}/bash-completion/completions 36 | %else 37 | %{_sysconfdir}/bash_completion.d/%{name}.bash 38 | %endif 39 | 40 | 41 | %changelog 42 | * Thu Nov 04 2022 JP Mens 1.9 43 | - bump version -- see Changelog 44 | * Thu Nov 04 2022 JP Mens 1.8 45 | - bump version -- see Changelog 46 | * Sat Oct 29 2022 JP Mens 1.7 47 | - bump version -- see Changelog 48 | * Sat Jul 18 2020 JP Mens 1.4 49 | - bump version -- see Changelog 50 | * Tue Apr 28 2020 Christian Albrecht 1.3-2 51 | - Fix broken download url 52 | - Make bash completion work on RHEL based distros 53 | * Tue Apr 7 2020 Kilian Cavalotti 1.3-1 54 | - Bumped to 1.3 release version 55 | - Include bash-completion file in package 56 | * Thu May 18 2017 Fabian Arrotin 1.1-1 57 | - Bumped to 1.1 release version 58 | * Wed Mar 15 2017 Fabian Arrotin 1.0-1 59 | - initial spec 60 | -------------------------------------------------------------------------------- /snapcraft.yaml: -------------------------------------------------------------------------------- 1 | name: jo 2 | version: "1.9" 3 | summary: jo 4 | description: | 5 | This is jo, a small utility to create JSON objects or arrays. 6 | 7 | confinement: strict 8 | grade: stable 9 | base: core20 10 | 11 | apps: 12 | jo: 13 | command: usr/local/bin/jo 14 | plugs: [home, removable-media] 15 | 16 | parts: 17 | jo: 18 | plugin: autotools 19 | source-type: git 20 | source: https://github.com/jpmens/jo 21 | build-packages: 22 | - pkg-config 23 | -------------------------------------------------------------------------------- /tests/jo-creator.txt: -------------------------------------------------------------------------------- 1 | Jan-Piet Mens 2 | -------------------------------------------------------------------------------- /tests/jo-large1.json: -------------------------------------------------------------------------------- 1 | {"timezone":"Asia/Kolkata","cities":["Abhayāpuri","Abohar","Abrama","Achalpur","Achhnera","Adampur","Addanki","Adirampattinam","Adra","Adūr","Afzalgarh","Afzalpur","Agar","Agartala","Agra","Ahmadnagar","Ahmadpur","Ahmedabad","Ahraura","Airoli","Aistala","Aizawl","Ajmer","Ajnāla","Ajodhya","Ajra","Akalkot","Akaltara","Akbarpur","Akbarpur","Akivīdu","Akkarampalle","Aklera","Akola","Akot","Aland","Alandi","Alandur","Alangad","Alangāyam","Alappuzha","Allahābād","Almora","Alnāvar","Along","Alot","Aluva","Alwar","Alwaye","Alībāg","Alīganj","Alīgarh","Alīpur","Alīpur Duār","Amalner","Amalāpuram","Amaravati","Amarnāth","Amarpur","Amarpātan","Ambad","Ambasamudram","Ambattūr","Ambikāpur","Ambur","Ambāh","Ambājogāi","Ambāla","Amet","Amla","Amod","Amreli","Amritsar","Amroha","Amroli","Amrāvati","Amudālavalasa","Anakāpalle","Anamalais","Anand","Anandpur","Anantapur","Anantnag","Andol","Anekal","Angamāli","Angul","Anjad","Anjangaon","Anjār","Ankleshwar","Annigeri","Annur","Anshing","Anta","Anthiyur","Anūpgarh","Anūppur","Anūpshahr","Aonla","Arakkonam","Arang","Arantāngi","Arcot","Ariyalūr","Arkalgūd","Arni","Aroor","Arrah","Arsikere","Arukutti","Arumuganeri","Aruppukkottai","Arāmbāgh","Arāria","Ashoknagar","Ashoknagar Kalyangarh","Ashta","Ashta","Asifābād","Atarra","Athni","Atmakūr","Atraulī","Attili","Attingal","Attur","Auraiya","Aurangabad","Aurangābād","Aurād","Ausa","Avanigadda","Avinashi","Ayakudi","Azamgarh","Azhiyūr","Baberu","Babrāla","Babīna","Bachhraon","Bada Barabīl","Badagara","Baddi","Badlapur","Badnāwar","Badvel","Badūria","Bagaha","Bagaha","Bagalkot","Bagar","Bagasra","Bagdogra","Bagulā","Baharampur","Baheri","Bahjoi","Bahraigh","Bahula","Bahādurganj","Bahādurgarh","Baidyabāti","Baihar","Bail-Hongal","Bairāgnia","Bakhtiyārpur","Balarāmpur","Balasore","Ballālpur","Balod","Baloda Bāzār","Balrāmpur","Balāngīr","Banat","Banda","Bandipura","Banga","Banganapalle","Bangaon","Bangaon","Bangarapet","Bankra","Banmankhi","Bannūr","Bantvāl","Banūr","Bar Bigha","Bara Uchāna","Baranagar","Barauli","Baraut","Barbil","Barddhamān","Bareilly","Bargarh","Bargi","Barhiya","Bari Sādri","Barjala","Barki Saria","Barkā Kānā","Barnāla","Barpeta","Barpeta Road","Barpāli","Baruipur","Barwāla","Barwāni","Basavakalyān","Basavana Bāgevādi","Basi","Basi","Basi","Basmat","Basni","Bastī","Baswa","Bathinda","Batāla","Baud","Bawāna","Bayāna","Bedi","Begamganj","Begusarai","Begūn","Behat","Behror","Bela","Beldānga","Belgaum","Bellampalli","Bellary","Belonia","Belsand","Belūr","Bemetāra","Bengaluru","Beohāri","Berasia","Beri Khās","Betamcherla","Bettiah","Betūl","Bewar","Beypore","Beāwar","Bhabhua","Bhachāu","Bhadaur","Bhadohi","Bhadrakh","Bhadreswar","Bhadrāchalam","Bhadrāvati","Bhainsdehi","Bhaisa","Bhandāra","Bhanjanagar","Bharatpur","Bharthana","Bharwāri","Bharūch","Bhasāwar","Bhatkal","Bhattiprolu","Bhavnagar","Bhavāni","Bhawanipur","Bhawāni Mandi","Bhawānipatna","Bhawānīgarh","Bhayandar","Bhikangaon","Bhilai","Bhind","Bhindār","Bhinga","Bhitarwār","Bhiwadi","Bhiwandi","Bhiwāni","Bhogpur","Bhongaon","Bhongīr","Bhopāl","Bhor","Bhuban","Bhubaneshwar","Bhudgaon","Bhuj","Bhusāval","Bhādra","Bhādāsar","Bhāgalpur","Bhālki","Bhānder","Bhānpura","Bhānpurī","Bhānvad","Bhātpāra","Bhātāpāra","Bhāyāvadar","Bhīkhi","Bhīlwāra","Bhīmavaram","Bhīmunipatnam","Bhīnmāl","Bhūm","Biaora","Bidhūna","Bihār Sharīf","Bihārīganj","Bijapur","Bijbehara","Bijnor","Bijāwar","Bikramganj","Bilgi","Bilgrām","Bilhaur","Bilimora","Bilsanda","Bilsi","Bilthra","Bilāra","Bilāri","Bilāsipāra","Bilāspur","Bindki","Binka","Birmitrapur","Birpara","Birūr","Bisauli","Bishnupur","Bissāu","Biswān","Bobbili","Bodhan","Bodināyakkanūr","Boisar","Bokajān","Bokāro","Bolpur","Bongaigaon","Borivli","Borsad","Botād","Brahmapur","Brājarājnagar","Budaun","Buddh Gaya","Budge Budge","Budhlāda","Budhāna","Bulandshahr","Buldāna","Burhar","Burhānpur","Burla","Buxar","Byndoor","Byādgi","Bābai","Bābra","Bādāmi","Bāgepalli","Bāgha Purāna","Bāghpat","Bāh","Bāli","Bāli","Bālotra","Bālugaon","Bālurghāt","Bālāchor","Bālāghāt","Bālāpur","Bāmor Kalān","Bānapur","Bānda","Bāndīkūi","Bāngarmau","Bānka","Bānki","Bānkura","Bānsbāria","Bānsdīh","Bānsi","Bānswāda","Bānswāra","Bāntva","Bāpatla","Bāramūla","Bārdoli","Bārh","Bāri","Bārmer","Bārsi","Bāruni","Bārākpur","Bārāmati","Bārān","Bārāsat","Bāsoda","Bāsudebpur","Bāzpur","Bīdar","Bīkaner","Bīlāspur","Bīrpur","Bīsalpur","Būndi","Būndu","Calangute","Canning","Chakapara","Chaklāsi","Chakradharpur","Chaksu","Challakere","Challapalle","Chalāla","Chamba","Chamrajnagar","Chandannagar","Chandauli","Chanderi","Chandrakona","Chanduasi","Chandīgarh","Changanācheri","Channagiri","Channapatna","Channarāyapatna","Charkhi Dādri","Charkhāri","Charthāwal","Chatrapur","Chatrā","Chemmumiahpet","Chengalpattu","Chengam","Chengannūr","Chennai","Chennimalai","Cherpulassery","Cherthala","Chetput","Chettipālaiyam","Chetwayi","Cheyyar","Chhabra","Chhala","Chhaprauli","Chharra","Chhatarpur","Chhibrāmau","Chhindwāra","Chhota Udepur","Chhoti Sādri","Chhāpar","Chhāta","Chhātāpur","Chicholi","Chidambaram","Chidawa","Chik Ballāpur","Chikhli","Chikmagalūr","Chiknāyakanhalli","Chikodi","Chilakalūrupet","Chillupār","Chincholi","Chinna Salem","Chinnachowk","Chinnamanūr","Chintāmani","Chiplūn","Chirmiri","Chitradurga","Chittaranjan","Chittaurgarh","Chittūr","Chodavaram","Chopda","Chotila","Chunār","Churāchāndpur","Chākan","Chākia","Chālisgaon","Chāmpa","Chānasma","Chānda","Chāndor","Chāndpur","Chāndur","Chāndūr","Chāndūr Bāzār","Chāpar","Chāpra","Chās","Chāībāsa","Chīpurupalle","Chīrāla","Chītāpur","Chūru","Clement Town","Closepet","Cochin","Coimbatore","Colachel","Colgong","Colonelganj","Contai","Coondapoor","Cuddalore","Cuddapah","Cumbum","Cumbum","Cuncolim","Curchorem","Cuttack","Dabhoi","Daboh","Dabra","Dabwāli","Dahegām","Dalkola","Dalsingh Sarai","Daltonganj","Dam Dam","Damoh","Damān","Dandeli","Darbhanga","Darsi","Daryāpur","Dasnapur","Dasūya","Datia","Dattāpur","Daudnagar","Daund","Dausa","Davangere","Dehra Dūn","Dehri","Delhi","Denkanikota","Deoband","Deogarh","Deoghar","Deoli","Deoli","Deoli","Deolāli","Deoraniān","Deori Khās","Deoria","Depālpur","Deshnoke","Devakottai","Devanhalli","Devarkonda","Devgadh Bāriya","Devgarh","Dewas","Deūlgaon Rāja","Dhamtari","Dhanaula","Dhanaura","Dhanbad","Dhandhuka","Dhanera","Dharampur","Dharamsala","Dharangaon","Dharapuram","Dharmadam","Dharmanagar","Dharmapuri","Dharmavaram","Dharmābād","Dhaulpur","Dhaurahra","Dhekiajuli","Dhenkānāl","Dhing","Dholka","Dhone","Dhorāji","Dhrol","Dhrāngadhra","Dhuburi","Dhulagari","Dhuliān","Dhupgāri","Dhāka","Dhāmnod","Dhāmpur","Dhār","Dhāri","Dhāriwāl","Dhāruhera","Dhārūr","Dhūlia","Dhūri","Diamond Harbour","Dibai","Dibrugarh","Dicholi","Digboi","Dighwāra","Digras","Dimāpur","Dinapore","Dindigul","Dindori","Diphu","Diu","Doda","Doddaballapura","Dohad","Dombivli","Dondaicha","Dongargarh","Dornakal","Dorāha","Dubrājpur","Dugda","Duliāgaon","Dum Duma","Dumjor","Dumka","Dumra","Dumraon","Durg","Durgapur","Durgāpur","Dwārka","Dādri","Dāhānu","Dākor","Dāmnagar","Dārjiling","Dārwha","Dāsna","Dātāganj","Dīdwāna","Dīg","Dīglūr","Dīnhāta","Dīnānagar","Dīsa","Dūngarpur","Egra","Elamanchili","Ellenabad","Ellore","Elūr","Emmiganūr","Erandol","Erode","Erraguntla","Erāttupetta","Etāwa","Etāwah","Faizpur","Farakka","Faridabad","Farrukhnagar","Farrukhābād","Farīdkot","Farīdpur","Fatehganj West","Fatehgarh Chūriān","Fatehpur","Fatehpur","Fatehpur","Fatehpur Sīkri","Fatehābād","Fatehābād","Fatwa","Ferokh","Ferozepore","Forbesganj","Fort Gloster","French Rocks","Fyzābād","Fālākāta","Fāzilka","Fīrozpur Jhirka","Fīrozābād","Gadag","Gadag-Betageri","Gaddi Annaram","Gadhada","Gadhinglaj","Gadwāl","Gajendragarh","Gajraula","Gajuwaka","Gandevi","Gandhinagar","Gangoh","Gangolli","Gangtok","Gangākher","Gangānagar","Gangāpur","Gangāpur","Gangāpur","Gangārāmpur","Gangāwati","Ganj Dundwāra","Gannavaram","Garhmuktesar","Garhshankar","Garhwa","Garhākota","Gariadhar","Garui","Gauribidanur","Gauripur","Gaya","Gevrai","Gharaunda","Ghatkesar","Ghazīpur","Ghosī","Ghoti Budrukh","Ghugus","Ghātampur","Ghātanji","Ghātsīla","Ghātāl","Ghāziābād","Giddalūr","Giddarbāha","Gingee","Gobichettipalayam","Gobindpur","Gobārdānga","Godda","Godhra","Gohadi","Gohāna","Gokak","Gokarna","Gokavaram","Gola Gokarannāth","Golāghāt","Gomoh","Gondal","Gondiā","Gondā City","Gopālganj","Gorakhpur","Gorakhpur","Gorantla","Gosāba","Govardhan","Goyerkāta","Goālpāra","Greater Noida","Gubbi","Gudalur","Gudivāda","Gudiyatham","Gulbarga","Guledagudda","Gulābpura","Gulāothi","Gumia","Gumlā","Gummidipundi","Guna","Gundlupēt","Gunnaur","Guntakal Junction","Guntur","Gunupur","Gurgaon","Gurmatkāl","Gursahāiganj","Gursarāi","Guru Har Sahāi","Guruvāyūr","Guskhara","Guwahati","Gwalior","Gyānpur","Gādarwāra","Gāndarbal","Gāndhīdhām","Gīrīdīh","Gūduvāncheri","Gūdūr","Hadagalli","Hadgāon","Hailākāndi","Haldaur","Haldia","Haldwani","Haliyal","Halvad","Hamīrpur","Hamīrpur","Handiā","Hanumāngarh","Harda Khās","Hardoī","Haridwar","Harihar","Harpanahalli","Harpālpur","Harsūd","Harūr","Hasanpur","Hassan","Hastināpur","Hatta","Hazāribāgh","Hilsa","Himatnagar","Hindaun","Hindoria","Hindupur","Hinganghāt","Hingoli","Hinjilikatu","Hirekerūr","Hiriyūr","Hisar","Hisuā","Hodal","Hojāi","Holalkere","Hole Narsipur","Homnābād","Honavar","Honnāli","Hosdurga","Hoshangābād","Hoshiārpur","Hoskote","Hospet","Hosūr","Howli","Hubli","Hugli","Hukeri","Hungund","Hunsūr","Husainābād","Hyderābād","Hābra","Hāflong","Hājo","Hājīpur","Hālol","Hālīsahar","Hāngal","Hānsi","Hāora","Hāpur","Hārij","Hāsimāra","Hāthras","Hāveri","Hīrākud","Ichalkaranji","Ichchāpuram","Idappadi","Igatpuri","Ilkal","Imphāl","Indergarh","Indi","Indore","Indri","Indāpur","Ingrāj Bāzār","Injambakkam","Iringal","Irinjālakuda","Irugūr","Islāmnagar","Islāmpur","Islāmpur","Itimādpur","Itānagar","Itārsi","Jabalpur","Jagalūr","Jagatsinghapur","Jagdalpur","Jagdīshpur","Jagdīspur","Jaggayyapeta","Jagraon","Jagtiāl","Jagādhri","Jahāngīrābād","Jahānābād","Jahāzpur","Jaigaon","Jainagar","Jaipur","Jais","Jaisalmer","Jaisingpur","Jaito","Jaitāran","Jalalpore","Jalandhar","Jalesar","Jaleshwar","Jalgaon","Jalgaon Jamod","Jalor","Jalpāiguri","Jalālpur","Jalālābad","Jalālābād","Jalālābād","Jalālī","Jalārpet","Jambusar","Jamkhandi","Jammalamadugu","Jammu","Jamnagar","Jamshedpur","Jamālpur","Jamūī","Jandiāla","Jangaon","Jangipur","Jaorā","Jarwal","Jasdan","Jashpurnagar","Jasidih","Jaspur","Jaswantnagar","Jatani","Jatāra","Jaunpur","Jayamkondacholapuram","Jaynagar Majilpur","Jetpur","Jevargi","Jewar","Jeypore","Jhajjar","Jhalidā","Jhanjhārpur","Jharia","Jharsuguda","Jhinjhāna","Jhumri Telaiya","Jhunjhunūn","Jhā-Jhā","Jhābua","Jhālrapātan","Jhālu","Jhālāwār","Jhānsi","Jhārgrām","Jhīnjhak","Jhūsi","Jintūr","Jodhpur","Jodhpur","Jodiya Bandar","Jogbani","Jora","Jorhāt","Jugsālai","Junnar","Jājpur","Jālaun","Jālna","Jāmadoba","Jāmai","Jāmtāra","Jāmuria","Jānjgīr","Jānsath","Jāwad","Jīnd","Jūnāgadh","Jūnāgarh","Kabrāi","Kachhwa","Kadakkavoor","Kadayanallur","Kadi","Kadiri","Kadod","Kadūr","Kaikalūr","Kailāras","Kailāshahar","Kaimganj","Kaimori","Kairāna","Kaithal","Kakching","Kakdwip","Kakrāla","Kalakkādu","Kalamassery","Kalamb","Kalamnūri","Kalavoor","Kalghatgi","Kallakkurichchi","Kallakurichi","Kallidaikurichi","Kalmeshwar","Kalpatta","Kalugumalai","Kalyandurg","Kalyani","Kalyān","Kalānaur","Kamalganj","Kampli","Kanchipuram","Kandukūr","Kangayam","Kanigiri","Kankauli","Kannad","Kannauj","Kanniyākumāri","Kannod","Kannur","Kanpur","Kantābānji","Kanuru","Kapadvanj","Kapūrthala","Karamsad","Karanpur","Karauli","Kareli","Karera","Karhal","Karjat","Karmāla","Karnāl","Karol Bāgh","Kartārpur","Karur","Karwar","Karād","Karīmganj","Karīmnagar","Kasba","Kashipur","Kasrāwad","Katangi","Katangi","Katghora","Kathua","Katihar","Kattanam","Kattivākkam","Kawardha","Kayalpattinam","Keelakarai","Kekri","Kemrī","Kenda","Kendrāparha","Kerūr","Keshod","Keshorai Pātan","Kesinga","Khada","Khadki","Khagaria","Khagaul","Khair","Khairābād","Khairāgarh","Khairāgarh","Khajuraho Group of Monuments","Khalīlābād","Khamaria","Khambhāliya","Khambhāt","Khammam","Khandela","Khandwa","Khanna","Kharagpur","Kharagpur","Kharakvasla","Kharar","Khardah","Khargone","Kharkhauda","Kharsia","Khatauli","Khatīma","Kheda","Khedbrahma","Khekra","Kheri","Kherālu","Khetia","Khetri","Khilchipur","Khirkiyān","Khopoli","Khowai","Khuldābād","Khunti","Khurai","Khurda","Khurja","Khāchrod","Khāmgaon","Khānāpur","Khāpa","Khārupatia","Khātegaon","Khātra","Khūtār","Kichha","Kinwat","Kirandul","Kiraoli","Kishanganj","Kishangarh","Kishtwār","Kithor","Kizhake Chālakudi","Koch Bihār","Kodaikānāl","Kodarmā","Kodoli","Kodungallūr","Kodār","Kodīnar","Koelwār","Kohīma","Kokrajhar","Kolasib","Kolhāpur","Kolkata","Kollam","Kollegāl","Kolār","Kolāras","Konch","Kondagaon","Kondapalle","Konnagar","Konnūr","Konārka","Koothanallur","Kopargaon","Koppal","Kopāganj","Koratla","Korba","Koregaon","Korwai","Korāput","Kosamba","Kosi","Kosigi","Kota","Kotagiri","Kotamangalam","Kotapārh","Kotdwāra","Kotkapura","Kotma","Kotputli","Kottagūdem","Kottayam","Kottūru","Kotā","Kovilpatti","Kovvūr","Kovūr","Koynanagar","Kozhikode","Koāth","Krishnagiri","Krishnanagar","Krishnarājpet","Kuchaiburi","Kuchera","Kuchāman","Kudachi","Kuju","Kukshi","Kulgam","Kulittalai","Kulpahār","Kulti","Kulu","Kumarapalayam","Kumbakonam","Kumbalam","Kumbhrāj","Kumhāri","Kumta","Kunda","Kundarkhi","Kundgol","Kundla","Kunigal","Kunnamangalam","Kunnamkulam","Kuppam","Kurandvād","Kurduvādi","Kurinjippādi","Kurnool","Kushtagi","Kutiatodu","Kutiyāna","Kuttampuzha","Kuzhithurai","Kyathampalle","Kāgal","Kākināda","Kākori","Kālimpong","Kāliyāganj","Kālka","Kālna","Kālol","Kālpi","Kālānwāli","Kālāvad","Kāman","Kāmthi","Kāmākhyānagar","Kāmāreddi","Kāmārhāti","Kānchrāpāra","Kāndhla","Kāndi","Kāndla","Kānke","Kānker","Kānkānhalli","Kānnangād","Kānt","Kānth","Kāpren","Kāraikkudi","Kāraikāl","Kāramadai","Kāranja","Kārkala","Kārsiyāng","Kāsaragod","Kāsganj","Kāthor","Kātol","Kātoya","Kātpādi","Kātrās","Kāvali","Kāyankulam","Kīl Bhuvanagiri","Kīratpur","Kūdligi","Kūkatpalli","Kūmher","Lachhmangarh Sīkar","Lahār","Lakhnādon","Lakhyabad","Lakhīmpur","Laksar","Lakshmeshwar","Lal Bahadur Nagar","Lalgudi","Lalitpur","Lar","Latur","Laungowāl","Leh","Leteri","Limbdi","Lingsugūr","Lohārdagā","Lonar","Lonavla","Loni","Losal","Luckeesarai","Lucknow","Ludhiāna","Lumding Railway Colony","Lunglei","Lādnūn","Lādwa","Lāharpur","Lākheri","Lālganj","Lālganj","Lālgola","Lālpur","Lālsot","Lātehār","Lāthi","Lāwar Khās","Lūnāvāda","Machhlīshahr","Machilīpatnam","Madambakkam","Madanapalle","Maddūr","Madgaon","Madhipura","Madhubani","Madhugiri","Madhupur","Madhyamgram","Madikeri","Madipakkam","Madukkarai","Madukkūr","Madurai","Madurāntakam","Maghar","Maham","Mahbūbnagar","Mahbūbābād","Mahemdāvād","Mahendragarh","Maheshtala","Maheshwar","Mahgawān","Mahiari","Mahmudābād","Mahobā","Maholi","Mahudha","Mahwah","Mahād","Mahālingpur","Mahārāganj","Mahārājgani","Mahāsamund","Mahē","Mahīshādal","Maihar","Mainpuri","Maināguri","Mairwa","Majalgaon","Makrāna","Maksi","Malakanagiri","Malappuram","Malaut","Malavalli","Malkajgiri","Malkapur","Malkāpur","Mallasamudram","Malpe","Malīhābād","Manali","Manamadurai","Manapparai","Mancherāl","Mandamarri","Mandapam","Mandapeta","Mandi","Mandideep","Mandlā","Mandsaur","Mandya","Mandāwar","Maner","Mangalagiri","Mangaldai","Mangalore","Manglaur","Mangrūl Pīr","Maniar","Manihāri","Manipal","Manjeri","Manjhanpur","Mankāchar","Manmād","Mannargudi","Mannārakkāt","Manoharpur","Manthani","Manuguru","Manāsa","Manāwar","Marakkanam","Marayur","Margherita","Marhaura","Mariāhu","Mariāni","Masaurhi Buzurg","Mathura","Mattanur","Mau","Mau","Mau Aimma","Maudaha","Mauganj","Maur","Mavoor","Mawāna","Mayiladuthurai","Mayāng Imphāl","Medak","Medinīpur","Meerut","Mehkar","Mehndāwal","Melur","Memāri","Mendarda","Merta","Mettupalayam","Mettur","Mhāsvād","Mihona","Milak","Miriālgūda","Mirzāpur","Misrikh","Modāsa","Moga","Mohali","Moirāng","Mokameh","Mokokchūng","Mon","Monghyr","Monoharpur","Moram","Morbi","Morena","Morigaon","Morinda","Mormugao","Morsi","Morwa","Morādābād","Morār","Mothīhāri","Mubarakpur","Muddebihāl","Mudgal","Mudhol","Mudkhed","Mughal Sarāi","Muhammadābād","Muhammadābād","Muhammadābād","Mukeriān","Mukher","Muktsar","Mulbāgal","Mulgund","Multai","Mulugu","Muluppilagadu","Mumbai","Mundargi","Mundgod","Mundra","Mungaoli","Mungeli","Munnar","Murbād","Murlīganj","Murshidābād","Murtajāpur","Murudeshwara","Murwāra","Murādnagar","Mushābani","Musiri","Mussoorie","Muttupet","Muvattupuzha","Muzaffarnagar","Muzaffarpur","Mysore","Mācherla","Māchhīwāra","Māgadi","Mākum","Mālegaon","Māler Kotla","Mālpura","Mālvan","Mālūr","Māndal","Māndalgarh","Māndvi","Māndvi","Māngrol","Māngrol","Mānsa","Mānsa","Mānvi","Mānwat","Mānāvadar","Māpuca","Mārahra","Mārkāpur","Mātābhānga","Māvelikara","Mīnjūr","Mīrganj","Mīrānpur","Mīrānpur Katra","Mūdbidri","Mūl","Mūlki","Mūndwa","Mūvattupula","Nabīnagar","Nadiād","Naduvannūr","Nagar","Nagari","Nagda","Nagpur","Nagīna","Naharlagun","Nahorkatiya","Naihāti","Naini Tāl","Nainpur","Nainwa","Najafgarh","Najībābād","Nakodar","Naksalbāri","Nakūr","Naldurg","Nalgonda","Nalhāti","Nambiyūr","Nanauta","Nanded","Nandigāma","Nandikotkūr","Nandurbar","Nandyāl","Nangal","Nanjangūd","Napāsar","Naraina","Naraini","Narasannapeta","Narasapur","Narasaraopet","Narauli","Naraura","Naregal","Narela","Nargund","Narsimhapur","Narsinghgarh","Narsīpatnam","Narwar","Narwāna","Narāyangarh","Nashik","Nasrullāhganj","Nasīrābād","Nattam","Naugachhia","Nautanwa","Navadwīp","Navalgund","Navi Mumbai","Navsari","Nawalgarh","Nawanshahr","Nawābganj","Nawābganj","Nawābganj","Nawāda","Nayāgarh","Nedumangād","Neelankarai","Neem ka Thana","Negapatam","Nelamangala","Nellikkuppam","Nellore","Nepānagar","Neral","New Delhi","Neyveli","Neyyāttinkara","Nichlaul","Nidadavole","Nihtaur","Nilakottai","Nilanga","Nimāparha","Nipāni","Nirmal","Nirmāli","Niwai","Nizāmābād","Nohar","Noida","Nokha","Nongstoin","North Guwāhāti","North Lakhimpur","Nowrangapur","Noāmundi","Nābha","Nādbai","Nādāpuram","Nāgamangala","Nāgar Karnūl","Nāgaur","Nāgercoil","Nāgod","Nāhan","Nāmagiripettai","Nāmakkal","Nāmrup","Nāndgaon","Nāndūra Buzurg","Nāngloi Jāt","Nānpāra","Nāravārikuppam","Nārnaul","Nārnaund","Nārāyanpet","Nāspur","Nāsriganj","Nāthdwāra","Nāwa","Nāyudupet","Nīlgiri","Nīlokheri","Nīlēshwar","Nīmbāhera","Nīmāj","Nūrpur","Nūzvīd","Obra","Okha","Ongole","Ooty","Orai","Osmanabad","Ottappālam","Ozar","Pachperwa","Padam","Padampur","Padampur","Padmanābhapuram","Padra","Padrauna","Pahāsu","Paithan","Pakur","Palani","Palera","Paliā Kalān","Palladam","Pallappatti","Pallikondai","Pallippatti","Pallāvaram","Palmaner","Palwal","Palwancha","Palāsa","Panaji","Panchkula","Pandharpur","Pandua","Panna","Panruti","Panvel","Panāgar","Papanasam","Paramagudi","Paravūr Tekkumbhāgam","Parbhani","Pariyāpuram","Parli Vaijnāth","Parlākimidi","Parola","Partūr","Parvatsar","Parādīp Garh","Parāsia","Parīchhatgarh","Pasān","Patancheru","Pataudi","Pathalgaon","Pathanāmthitta","Patharia","Pathānkot","Patiāla","Patna","Patnāgarh","Patti","Pattukkottai","Patāmundai","Pauri","Pawni","Pawāyan","Payyannūr","Pedana","Peddapalli","Peddāpuram","Pehowa","Pen","Pennādam","Pennāgaram","Penugonda","Penukonda","Perambalur","Peranāmpattu","Peravurani","Periyakulam","Periyanayakkanpalaiyam","Perumbavoor","Perumpāvūr","Perundurai","Perungudi","Petlād","Phagwāra","Phalauda","Phalodi","Phaltan","Phaphūnd","Phek","Phillaur","Phirangipuram","Phulbāni","Phulera","Phulpur","Phusro","Pihānī","Pilibangan","Pilkhua","Pilāni","Pimpri","Pindwāra","Pinjaur","Pināhat","Pipili","Pipraich","Piravam","Piriyāpatna","Piro","Pithampur","Pithorāgarh","Pithāpuram","Pokaran","Polasara","Polavaram","Pollachi","Polūr","Ponda","Ponmana","Ponneri","Ponnur","Ponnāni","Ponnūru","Poonamalle","Porbandar","Porsa","Port Blair","Porur","Powai","Pratāpgarh","Proddatūr","Puducherry","Pudukkottai","Pujali","Pukhrāyān","Pulgaon","Pulivendla","Puliyangudi","Pulwama","Punalūr","Pune","Punganūru","Punjai Puliyampatti","Punāsa","Pupri","Puri","Purnia","Puruliya","Purwā","Pusad","Pushkar","Puttūr","Puttūr","Pāchora","Pākāla","Pālakkodu","Pālakollu","Pālanpur","Pālghar","Pālghāt","Pāli","Pāli","Pālitāna","Pālkonda","Pāloncha","Pānchla","Pāndhurnā","Pānihāti","Pānīpat","Pāonta Sāhib","Pāppinisshēri","Pārdi","Pārvatipuram","Pāsighāt","Pātan","Pāthardi","Pāthardih","Pāthri","Pātūr","Pāvugada","Pīlibhīt","Pīpri","Pīpār","Pūnch","Pūndri","Pūnāhāna","Pūranpur","Pūrna","Quthbullapur","Qādiān","Rabkavi","Raebareli","Rafiganj","Raghunathpur","Rahimatpur","Raigarh","Raipur","Raipur","Raisen","Rajaori","Rajapalaiyam","Rajpur","Rajpur","Rajpur Sonarpur","Ramagundam","Ramanathapuram","Ramanayyapeta","Rameswaram","Rampachodavaram","Rampur Hat","Ranchi","Rangia","Rangāpāra","Rasipuram","Rasrā","Ratangarh","Ratanpur","Ratia","Ratlām","Ratnagiri","Raurkela","Raxaul","Raybag","Rehli","Remuna","Renigunta","Renukūt","Reoti","Repalle","Revelganj","Rewa","Rewāri","Richha","Rishra","Rishīkesh","Risod","Robertsganj","Robertsonpet","Roha","Rohini","Rohtak","Ron","Roorkee","Ropar","Rura","Rusera","Rādhanpur","Rāghogarh","Rāhatgarh","Rāhuri","Rāichūr","Rāiganj","Rāikot","Rāipur","Rāisinghnagar","Rāj-Nāndgaon","Rājahmundry","Rājaldesar","Rājgarh","Rājgarh","Rājgarh","Rājgarh","Rājgurunagar","Rājgīr","Rājkot","Rājmahal","Rājpura","Rājpīpla","Rājsamand","Rājula","Rājākhera","Rājūra","Rāmachandrapuram","Rāmganj Mandi","Rāmgarh","Rāmgarh","Rāmgundam","Rāmjībanpur","Rāmnagar","Rāmnagar","Rāmnagar","Rāmpur","Rāmpur","Rāmpura","Rāmpura","Rāmtek","Rāmāpuram","Rānia","Rānikhet","Rānipet","Rānāghāt","Rānāvāv","Rānībennur","Rānīganj","Rānīpur","Rāpar","Rāth","Rāver","Rāwatbhāta","Rāwatsār","Rāya","Rāyachoti","Rāyadrug","Rāzampeta","Rāzām","Rīngas","Rūdarpur","Sabalgarh","Sadalgi","Sadashivpet","Sadābād","Safidon","Safīpur","Sagauli","Saharsa","Sahaspur","Sahaswān","Sahāranpur","Sahāwar","Saidpur","Saiha","Saint Thomas Mount","Sainthia","Sakleshpur","Saktī","Salem","Salāya","Sambalpur","Sambhal","Samdari","Samrāla","Samthar","Samālkha","Samāstipur","Sanaur","Sancoale","Sandīla","Sandūr","Sangamner","Sangariā","Sangod","Sangrūr","Sangāreddi","Sankeshwar","Sanāwad","Saoner","Saraipali","Sarauli","Sardhana","Sardulgarh","Sardārshahr","Sarkhej","Sarwār","Sarāi Mīr","Sarāi Ākil","Satara","Sathupalli","Sathyamangalam","Satna","Sattenapalle","Sattur","Satānā","Saugor","Saundatti","Sausar","Savanūr","Savarkundla","Sawāi Mādhopur","Secunderabad","Sehore","Selu","Sendhwa","Seohāra","Seondha","Seoni","Seoni Mālwa","Seram","Serchhīp","Serilingampalle","Shahbazpur","Shahdol","Shamsābād","Shamsābād","Shegaon","Sheikhpura","Sheoganj","Sheohar","Sheopur","Sherghāti","Sherkot","Shertallai","Shiggaon","Shikohābād","Shikārpur","Shikārpūr","Shillong","Shimla","Shimoga","Shiraguppi","Shirdi","Shirhatti","Shirpur","Shivaji Nagar","Shivpuri","Sholinghur","Shorāpur","Shrīgonda","Shrīrangapattana","Shrīrāmpur","Shujālpur","Shyamnagar","Shāhganj","Shāhi","Shāhjānpur","Shāhpur","Shāhpur","Shāhpur","Shāhpur","Shāhpura","Shāhpura","Shāhābād","Shāhābād","Shāhābād","Shāhābād","Shāhāda","Shājāpur","Shāmgarh","Shāmli","Shāntipur","Shīshgarh","Shōranūr","Sibsāgar","Siddhapur","Siddipet","Sidhaulī","Sidhi","Sidlaghatta","Sihor","Sihorā","Sijua","Sikandarpur","Sikandarābād","Sikandra Rao","Sikka","Silao","Silapathar","Silchar","Siliguri","Sillod","Silvassa","Simdega","Sindgi","Sindhnūr","Singarāyakonda","Singrauli","Singur","Singānallūr","Singāpur","Sinnar","Sirhind","Sirohi","Sironj","Sirsa","Sirsi","Sirsi","Sirsilla","Sirsāganj","Siruguppa","Sirumugai","Sirūr","Sisauli","Siswā Bāzār","Sitārganj","Siuri","Sivaganga","Sivagiri","Sivagiri","Sivakasi","Siwān","Siwāna","Sohna","Sohāgpur","Sojat","Sojītra","Solan","Solāpur","Someshwar","Sompeta","Sonepur","Songadh","Sonāmukhi","Sonāri","Sonīpat","Sopur","Sorada","Soro","Soron","Soygaon","Soyībug","Sri Dūngargarh","Sri Mādhopur","Srikakulam","Srinagar","Srirāmpur","Srivaikuntam","Srivilliputhur","Srīnagar","Srīnivāspur","Srīperumbūdūr","Srīrāmnagar","Srīsailain","Srīvardhan","Suket","Sultanpur","Sultānpur","Sulur","Sulya","Sundargarh","Sundarnagar","Sunel","Sunām","Supaul","Surat","Surendranagar","Suriānwān","Suriāpet","Suār","Sādri","Sāgar","Sāhibganj","Sālūmbar","Sālūr","Sāmalkot","Sāmba","Sāmbhar","Sānand","Sānchor","Sāndi","Sāngli","Sāngola","Sānkrāil","Sārangpur","Sāsvad","Sāvantvādi","Sāvda","Sāyla","Sīkar","Sīra","Sīrkāzhi","Sītāmarhi","Sītāpur","Sūjāngarh","Sūlūru","Sūrajgarh","Sūrandai","Sūratgarh","Takhatgarh","Takhatpur","Talegaon Dābhāde","Taleigao","Talipparamba","Taloda","Talwandi Bhai","Talwāra","Talāja","Tambaram","Tamlūk","Tanakpur","Tanjore","Tanuku","Tarakeswar","Tarikere","Tarn Tāran","Tarāna","Teghra","Tehri","Tekkalakote","Tekkali","Tekāri","Telhāra","Tellicherry","Teni","Teonthar","Terdāl","Tezpur","Thakurdwara","Tharangambadi","Tharād","Thenkasi","Thiruthani","Thiruvananthapuram","Thiruvarur","Thoothukudi","Thoubāl","Thrissur","Thākurganj","Thān","Thāna Bhawan","Thāne","Thānesar","Thāsra","Tijāra","Tilhar","Tindivanam","Tinnanūr","Tinsukia","Tiptūr","Tiruchchendur","Tiruchengode","Tiruchirappalli","Tirukkoyilur","Tirumala","Tirunelveli","Tirupati","Tirupparangunram","Tiruppur","Tiruppuvanam","Tirur","Tiruttangal","Tiruvalla","Tiruvallur","Tiruvannāmalai","Tiruvottiyūr","Tisaiyanvilai","Titlāgarh","Titāgarh","Todabhim","Todaraisingh","Tohāna","Tondi","Tonk","Tuensang","Tufānganj","Tuljāpur","Tulsīpur","Tumkūr","Tumsar","Tuni","Tura","Turaiyūr","Tādepalle","Tādepallegūdem","Tādpatri","Tājpur","Tāki","Tālcher","Tālīkota","Tānda","Tāndā","Tāndūr","Tāoru","Tāramangalam","Tārānagar","Tāsgaon","Tīkamgarh","Tīrthahalli","Tūndla","Udaipur","Udaipur","Udaipur","Udaipura","Udalguri","Udangudi","Udgīr","Udhampur","Udumalaippettai","Udupi","Ujhāni","Ujjain","Ulhasnagar","Ullal","Umarga","Umaria","Umarkhed","Umarkot","Umred","Umreth","Un","Una","Una","Unhel","Unjha","Unnāo","Upleta","Uppal Kalan","Uran","Uravakonda","Usehat","Usilampatti","Utraula","Uttamapālaiyam","Uttarkāshi","Uttiramerūr","V.S.K.Valasai (Dindigul-Dist.)","Vadakku Valliyūr","Vadakku Viravanallur","Vadamadurai","Vadigenhalli","Vadlapūdi","Vadnagar","Vadodara","Vaijāpur","Vaikam","Valabhīpur","Vallabh Vidyanagar","Valparai","Valsād","Vandavāsi","Vaniyambadi","Vapi","Varanasi","Varangaon","Varkala","Vasa","Vasco da Gama","Vasind","Vattalkundu","Vayalār","Vedaraniyam","Vejalpur","Vellore","Velur","Vemalwāda","Venkatagiri","Vepagunta","Verāval","Vetapālem","Vettaikkaranpudur","Vettūr","Vidisha","Vijayawada","Vijāpur","Vikārābād","Villupuram","Vinukonda","Virajpet","Virudunagar","Virār","Visakhapatnam","Visnagar","Vite","Vizianagaram","Vriddhāchalam","Vrindāvan","Vuyyūru","Vyāra","Vāda","Vādippatti","Vāsudevanallūr","Vīsāvadar","Wai","Walajapet","Wani","Wanparti","Warangal","Wardha","Warora","Warud","Wazīrganj","Wellington","Wer","Wokha","Wādi","Wānkāner","Wāris Alīganj","Wārāseonī","Wāshīm","Yamunānagar","Yanam","Yanamalakuduru","Yavatmāl","Yelahanka","Yellandu","Yellāpur","Yeola","Yādgīr","Yāval","Zahirābād","Zaidpur","Zamānia","Zira","Zunheboto","Ābu","Ābu Road","Ādilābād","Ādoni","Ālangulam","Āmli","Āmlāgora","Āmta","Āndippatti","Ārangaon","Āron","Ārvi","Āsandh","Āsansol","Āsika","Āsind","Āthagarh","Āvadi","Ūn"]} 2 | -------------------------------------------------------------------------------- /tests/jo-large2.json: -------------------------------------------------------------------------------- 1 | {"timezone":"America/New_York","cities":["Aberdeen","Abington","Abington","Acton","Acworth","Adams Morgan","Adelphi","Agawam","Aiken","Akron","Alafaya","Albany","Albany","Albemarle","Alexandria","Allapattah","Allentown","Alliance","Allison Park","Alpharetta","Altamonte Springs","Altoona","Americus","Amesbury","Amherst","Amherst","Amherst Center","Amsterdam","Anderson","Annandale","Annapolis","Ansonia","Apex","Apopka","Arbutus","Arlington","Arlington","Arnold","Asbury Park","Ashburn","Asheboro","Asheville","Ashland","Ashland","Ashland","Ashtabula","Aspen Hill","Astoria","Athens","Athens","Atlanta","Atlantic City","Attleboro","Auburn","Auburn","Auburn","Auburndale","Augusta","Augusta","Aurora","Austintown","Avenel","Aventura","Avon","Avon Center","Avon Lake","Back Mountain","Baileys Crossroads","Baldwin","Baldwin","Ballenger Creek","Baltimore","Bangor","Barberton","Barnstable","Barrington","Bartow","Basking Ridge","Batavia","Bath Beach","Bay Shore","Bay Village","Baychester","Bayonet Point","Bayonne","Bayshore Gardens","Bayside","Bayville","Bear","Beavercreek","Beckley","Bedford","Bel Air North","Bel Air South","Belle Glade","Belleville","Bellmore","Belmont","Beltsville","Belvedere Park","Bensalem","Bensonhurst","Berea","Bergenfield","Bethel Park","Bethesda","Bethlehem","Bethpage","Beverly","Beverly Cove","Biddeford","Billerica","Binghamton","Blacksburg","Bloomfield","Bloomingdale","Bluffton","Boardman","Boca Del Mar","Boca Raton","Bon Air","Bonita Springs","Boone","Borough Park","Boston","Bowie","Bowling Green","Boynton Beach","Bradenton","Braintree","Brandon","Branford","Brentwood","Briarwood","Bridgeport","Bridgeton","Bridgewater","Brighton","Brighton Beach","Bristol","Bristol","Bristol","Bristol","Broadview Heights","Brockton","Brook Park","Brookhaven","Brookline","Brooklyn","Brooklyn Heights","Brownsville","Brownsville","Brunswick","Brunswick","Brunswick","Buckhall","Buenaventura Lakes","Buffalo","Burke","Burlington","Burlington","Burlington","Burlington","Bushwick","Calhoun","Calverton","Cambria Heights","Cambridge","Camden","Camp Springs","Canarsie","Candler-McAfee","Canton","Canton","Canton","Cape Coral","Carlisle","Carney","Carol City","Carrboro","Carrollton","Carrollwood","Carrollwood Village","Carteret","Cartersville","Cary","Casselberry","Catonsville","Cave Spring","Center City","Centereach","Centerville","Central Falls","Central Islip","Centreville","Chambersburg","Chamblee","Chantilly","Chapel Hill","Charleston","Charleston","Charlotte","Charlottesville","Chattanooga","Cheektowaga","Chelmsford","Chelsea","Cherry Hill","Cherry Hill","Chesapeake","Cheshire","Chester","Chester","Chicopee","Chillicothe","Chillum","Chinatown","Christiansburg","Cicero","Cincinnati","Citrus Park","City of Milford (balance)","Clark-Fulton","Clarksburg","Clay","Clayton","Clearwater","Clemmons","Clemson","Clermont","Cleveland","Cleveland","Cleveland Heights","Cliffside Park","Clifton","Clifton Park","Clinton","Cloverly","Cockeysville","Cocoa","Coconut Creek","Coconut Grove","Cohoes","Colchester","College Park","College Point","Collinwood","Colonia","Colonial Heights","Columbia","Columbia","Columbus","Columbus","Commack","Concord","Concord","Concord","Coney Island","Conway","Conyers","Cooper City","Copiague","Coral Gables","Coral Springs","Coral Terrace","Coram","Cornelius","Corona","Cortland","Cortlandt Manor","Country Club","Country Walk","Coventry","Covington","Cranberry Township","Cranford","Cranston","Crofton","Culpeper","Cumberland","Cumberland","Cutler","Cutler Bay","Cutler Ridge","Cuyahoga Falls","Cypress Hills","Dale City","Dalton","Damascus","Danbury","Dania Beach","Danvers","Danville","Danville","Darien","Davie","Dayton","Daytona Beach","DeBary","DeLand","Decatur","Dedham","Deer Park","Deerfield Beach","Defiance","Delaware","Delray Beach","Deltona","Denville","Depew","Derry","Derry Village","Detroit-Shoreway","Dix Hills","Doral","Douglasville","Dover","Dover","Dover","Dracut","Drexel Hill","Dublin","Dublin","Duluth","Dumont","Dundalk","Dunedin","Dunwoody","Durham","Duxbury","Dyker Heights","Easley","East Amherst","East Brainerd","East Brunswick","East Chattanooga","East Cleveland","East Concord","East Elmhurst","East Flatbush","East Hampton","East Harlem","East Hartford","East Haven","East Lake","East Lake-Orient Park","East Longmeadow","East Massapequa","East Meadow","East Naples","East New York","East Northport","East Norwalk","East Orange","East Patchogue","East Point","East Providence","East Ridge","East Riverdale","East Setauket","East Tremont","East Village","Eastchester","Easthampton","Eastlake","Easton","Easton","Easton","Eden","Edgewater","Edgewood","Edison","Eggertsville","Egypt Lake-Leto","Eldersburg","Elizabeth","Elizabeth City","Elizabethtown","Elkridge","Elkton","Ellicott City","Elmhurst","Elmira","Elmont","Elmwood Park","Elyria","Emerson Hill","Enfield","Englewood","Erie","Erlanger","Essex","Estero","Euclid","Eustis","Evans","Everett","Ewing","Fair Lawn","Fairborn","Fairfax","Fairfield","Fairfield","Fairhaven","Fairland","Fairmont","Fairview Park","Fall River","Far Rockaway","Farmington","Farmingville","Farragut","Fayetteville","Fayetteville","Ferndale","Financial District","Findlay","Fitchburg","Flagami","Flatbush","Flatlands","Fleming Island","Floral Park","Florence","Florence","Florida Ridge","Fordham","Fords","Forest Hills","Forest Park","Forest Park","Fort Bragg","Fort Hamilton","Fort Hunt","Fort Lauderdale","Fort Lee","Fort Myers","Fort Pierce","Fort Thomas","Fort Washington","Fountainebleau","Four Corners","Framingham","Framingham Center","Franconia","Frankfort","Franklin","Franklin Square","Frederick","Fredericksburg","Freeport","Fremont","Fresh Meadows","Front Royal","Fruit Cove","Fuquay-Varina","Gahanna","Gainesville","Gainesville","Gaithersburg","Garden City","Gardner","Garfield","Garfield Heights","Garner","Gastonia","Gates-North Gates","Georgetown","Germantown","Glassboro","Glassmanor","Glastonbury","Glen Burnie","Glen Cove","Glendale","Glenvar Heights","Glenville","Gloucester","Gloversville","Golden Gate","Golden Glades","Goldsboro","Goose Creek","Grafton","Gramercy Park","Grand Island","Graniteville","Gravesend","Great Falls","Great Kills","Greater Northdale","Greater Upper Marlboro","Green","Green Haven","Greenacres City","Greenbelt","Greenburgh","Greeneville","Greenfield","Greenpoint","Greensboro","Greenville","Greenville","Greenwood","Greer","Griffin","Grove City","Guilford","Gwynn Oak","Hackensack","Hagerstown","Haines City","Hallandale Beach","Hamden","Hamilton","Hampton","Hanahan","Hanover","Hanover","Hanover","Harlem","Harrisburg","Harrison","Harrison","Harrisonburg","Hartford","Hauppauge","Havelock","Haverhill","Hawthorne","Hazleton","Hell's Kitchen","Hempstead","Henderson","Henrietta","Hermitage","Herndon","Hialeah","Hialeah Gardens","Hickory","Hicksville","High Point","Highland Springs","Hillcrest Heights","Hilliard","Hillsborough","Hillside","Hillside","Hilton Head","Hilton Head Island","Hinesville","Hoboken","Holbrook","Holden","Holiday","Hollis","Holly Springs","Hollywood","Holtsville","Holyoke","Homestead","Hopatcong Hills","Hope Mills","Hopewell","Hough","Howard Beach","Huber Heights","Hudson","Hunt Valley","Huntersville","Huntington","Huntington","Huntington Station","Hunts Point","Hyattsville","Hybla Valley","Idylwood","Ilchester","Immokalee","Independence","Indian Trail","Iona","Irondequoit","Ironville","Irvington","Iselin","Islip","Ithaca","Ives Estates","Jackson","Jackson Heights","Jacksonville","Jacksonville","Jacksonville Beach","Jamaica","Jamaica Plain","Jamestown","Jasmine Estates","Jersey City","Johns Creek","Johnson City","Johnston","Johnstown","Jupiter","Kannapolis","Kearny","Keene","Kendale Lakes","Kendall","Kendall West","Kenmore","Kennesaw","Kensington","Kent","Kernersville","Kettering","Kew Gardens","Kew Gardens Hills","Key West","Keystone","Killingly Center","King of Prussia","Kings Bridge","Kings Park","Kingsland","Kingsport","Kingston","Kinston","Kiryas Joel","Kissimmee","Knoxville","LaGrange","Lackawanna","Laconia","Lake Butler","Lake Magdalene","Lake Mary","Lake Ridge","Lake Ronkonkoma","Lake Shore","Lake Wales","Lake Worth","Lake Worth Corridor","Lakeland","Lakeside","Lakewood","Lakewood","Lancaster","Lancaster","Land O' Lakes","Landover","Langley Park","Lanham-Seabrook","Lansdale","Largo","Latham","Lauderdale Lakes","Lauderhill","Laurel","Laurel","Laurelton","Laurinburg","Lawrence","Lawrenceville","Lealman","Lebanon","Lebanon","Ledyard","Leesburg","Leesburg","Lehigh Acres","Leisure City","Leland","Lenoir","Leominster","Levittown","Levittown","Lewiston","Lexington","Lexington","Lexington","Lexington","Lexington-Fayette","Lima","Limerick","Lincoln","Lincolnia","Linden","Lindenhurst","Lindenwold","Linton Hall","Lithia Springs","Livingston","Lochearn","Lockport","Lodi","Long Beach","Long Branch","Long Island City","Longmeadow","Lorain","Lorton","Lowell","Ludlow","Lumberton","Lutherville-Timonium","Lutz","Lynbrook","Lynchburg","Lyndhurst","Lynn","Mableton","Macon","Madison","Madison","Mahwah","Maitland","Malden","Mamaroneck","Manassas","Manassas Park","Manchester","Manchester","Manhattan","Mansfield","Mansfield","Mansfield City","Maple Heights","Maple Shade","Maplewood","Marblehead","Marco Island","Margate","Marietta","Mariners Harbor","Marion","Marlboro","Marlborough","Martinez","Martinsburg","Maryland City","Marysville","Maryville","Mason","Maspeth","Massapequa","Massapequa Park","Massillon","Mastic","Matthews","Mauldin","Mayfield Heights","McDonough","McKeesport","McLean","Meadow Woods","Meadowbrook","Meads","Mechanicsville","Medford","Medford","Medina","Melbourne","Melrose","Melrose","Melville","Mentor","Mercerville-Hamilton Square","Meriden","Merrick","Merrifield","Merrimack","Merritt Island","Methuen","Miami","Miami Beach","Miami Gardens","Miami Lakes","Miamisburg","Middle River","Middle Village","Middleborough","Middleburg Heights","Middletown","Middletown","Middletown","Middletown","Middletown","Middletown","Milford","Milford","Milford Mill","Milledgeville","Millville","Milton","Milton","Mineola","Mint Hill","Miramar","Monroe","Monroeville","Monsey","Montclair","Montclair","Montgomery Village","Montville Center","Mooresville","Morganton","Morgantown","Morningside Heights","Morris Heights","Morrisania","Morristown","Morristown","Morrisville","Mott Haven","Mount Laurel","Mount Lebanon","Mount Pleasant","Mount Vernon","Mount Vernon","Murrysville","Myrtle Beach","Nanuet","Naples","Narragansett","Nashua","Natick","Naugatuck","Needham","New Bedford","New Bern","New Britain","New Brunswick","New Canaan","New Castle","New City","New Haven","New London","New Milford","New Philadelphia","New Port Richey","New Rochelle","New Smyrna Beach","New Springville","New York City","Newark","Newark","Newark","Newburgh","Newburyport","Newington","Newnan","Newport","Newport","Newport News","Newton","Niagara Falls","Nicetown-Tioga","Nicholasville","Niles","Norcross","Norfolk","Norland","Norristown","North Amityville","North Andover","North Arlington","North Atlanta","North Attleborough Center","North Augusta","North Babylon","North Bay Shore","North Bel Air","North Bellmore","North Bergen","North Bethesda","North Canton","North Charleston","North Chicopee","North Decatur","North Druid Hills","North Fort Myers","North Haven","North Kingstown","North Lauderdale","North Massapequa","North Miami","North Miami Beach","North Myrtle Beach","North Olmsted","North Plainfield","North Port","North Potomac","North Providence","North Ridgeville","North Royalton","North Stamford","North Tonawanda","North Valley Stream","Northampton","Northdale","Norton","Norwalk","Norwalk","Norwich","Norwood","Norwood","Nutley","Oak Hill","Oak Ridge","Oak Ridge","Oakland Park","Oakleaf Plantation","Oakton","Ocala","Ocean Acres","Oceanside","Ocoee","Odenton","Ojus","Old Bridge","Olney","Opa-locka","Orange","Oregon","Orlando","Ormond Beach","Ossining","Oswego","Oviedo","Owings Mills","Oxford","Oxon Hill","Oxon Hill-Glassmanor","Ozone Park","Painesville","Palisades Park","Palm Bay","Palm Beach Gardens","Palm City","Palm Coast","Palm Harbor","Palm River-Clair Mel","Palm Springs","Palm Valley","Palmer","Palmetto Bay","Paramus","Park Slope","Parkchester","Parkersburg","Parkland","Parkville","Parma","Parma Heights","Parole","Parsippany","Pasadena","Passaic","Pataskala","Paterson","Pawtucket","Peabody","Peachtree City","Peachtree Corners","Pearl River","Peekskill","Pembroke Pines","Penn Hills","Pennsauken","Pennsport","Perry","Perry Hall","Perrysburg","Perth Amboy","Petersburg","Phenix City","Philadelphia","Phoenixville","Pickerington","Pikesville","Pine Hills","Pinecrest","Pinehurst","Pinellas Park","Pinewood","Piqua","Piscataway","Pittsburgh","Pittsfield","Plainfield","Plainfield","Plainview","Plainville","Plant City","Plantation","Plattsburgh","Pleasantville","Plum","Poinciana","Point Pleasant","Pompano Beach","Ponte Vedra Beach","Pooler","Port Charlotte","Port Chester","Port Orange","Port Richmond","Port Saint Lucie","Port Washington","Portland","Portsmouth","Portsmouth","Portsmouth","Portsmouth","Portsmouth Heights","Potomac","Pottstown","Poughkeepsie","Princeton","Princeton","Providence","Punta Gorda","Punta Gorda Isles","Queens","Queens Village","Queensbury","Quincy","Radcliff","Radford","Radnor","Rahway","Raleigh","Ramsey","Randallstown","Randolph","Randolph","Reading","Reading","Redan","Redland","Rego Park","Reisterstown","Reston","Revere","Reynoldsburg","Richmond","Richmond","Richmond Hill","Richmond West","Ridgewood","Ridgewood","Riverdale","Riverside","Riverview","Riviera Beach","Roanoke","Roanoke Rapids","Rochester","Rochester","Rock Hill","Rockland","Rockledge","Rockville","Rockville Centre","Rocky Mount","Rocky River","Rome","Rome","Ronkonkoma","Roosevelt","Rose Hill","Rosedale","Rosedale","Roselle","Rossville","Rossville","Roswell","Rotterdam","Royal Palm Beach","Ruskin","Rutherford","Rutland","Rye","Saco","Safety Harbor","Saint Andrews","Saint Charles","Saint Cloud","Salem","Salem","Salem","Salisbury","Salisbury","San Carlos Park","Sandalfoot Cove","Sandusky","Sandy Springs","Sanford","Sanford","Sanford","Sarasota","Saratoga Springs","Saugus","Savannah","Sayreville","Sayreville Junction","Sayville","Scaggsville","Scarsdale","Schenectady","Scotch Plains","Scranton","Seabrook","Seaford","Sebastian","Secaucus","Selden","Seminole","Setauket-East Setauket","Seven Oaks","Severn","Severna Park","Sevierville","Sewell","Seymour","Shaker Heights","Shaw","Sheepshead Bay","Shelby","Shelbyville","Shelton","Shirley","Short Pump","Shrewsbury","Sicklerville","Sidney","Silver Spring","Simpsonville","Smithfield","Smithtown","Smyrna","Snellville","Socastee","Solon","Somerset","Somerset","Somerville","South Bel Air","South Boston","South Bradenton","South Burlington","South Euclid","South Gate","South Hadley","South Kingstown","South Laurel","South Miami Heights","South Old Bridge","South Orange","South Ozone Park","South Peabody","South Plainfield","South Portland","South Portland Gardens","South Riding","South River","South Suffolk","South Vineland","South Windsor","Southbridge","Southbury","Southchase","Sparta","Spartanburg","Spring Hill","Spring Valley","Springboro","Springfield","Springfield","Springfield","Springfield","Springfield Gardens","St. Charles","St. Johns","St. Marys","St. Petersburg","Stallings","Stamford","State College","Staten Island","Statesboro","Statesville","Staunton","Sterling","Steubenville","Stockbridge","Stonecrest","Stoneham","Storrs","Stoughton","Stow","Stratford","Streetsboro","Strongsville","Stuart","Sudbury","Sudley","Suffolk","Sugar Hill","Suitland","Suitland-Silver Hill","Summerville","Summit","Sumter","Sun City Center","Sunny Isles Beach","Sunnyside","Sunrise","Sunset","Sunset Park","Suwanee","Swansea","Sweetwater","Sylvania","Syosset","Syracuse","Takoma Park","Tallahassee","Tallmadge","Tamarac","Tamiami","Tampa","Tarpon Springs","Taunton","Tavares","Taylors","Teaneck","Temple Terrace","Terrace Heights","Tewksbury","The Acreage","The Bronx","The Crossings","The Hammocks","The Villages","Thomasville","Thomasville","Three Lakes","Throgs Neck","Tiffin","Tifton","Tinton Falls","Titusville","Toledo","Toms River","Torrington","Town 'n' Country","Towson","Tremont","Trenton","Trotwood","Troy","Troy","Trumbull","Tuckahoe","Tucker","Twinsburg","Tysons Corner","Union","Union City","Union City","Uniondale","Unionport","University","University Heights","University Park","Upper Arlington","Upper Saint Clair","Utica","Valdosta","Valley Stream","Valrico","Van Nest","Vandalia","Venice","Vero Beach","Vero Beach South","Vestal","Vienna","Vincentown","Vineland","Virginia Beach","Voorhees","Wade Hampton","Wadsworth","Wake Forest","Wakefield","Wakefield","Waldorf","Wallingford","Wallingford Center","Waltham","Wantagh","Warner Robins","Warren","Warren Township","Warwick","Washington","Washington Heights","Waterbury","Waterford","Watertown","Watertown","Waterville","Wayne","Wayne","Waynesboro","Weirton","Weirton Heights","Wekiwa Springs","Wellesley","Wellington","Wesley Chapel","West Albany","West Babylon","West Chester","West Columbia","West Elkridge","West Falls Church","West Hartford","West Haven","West Hempstead","West Hollywood","West Islip","West Little River","West Lynchburg","West Melbourne","West Mifflin","West Milford","West New York","West Orange","West Palm Beach","West Park","West Raleigh","West Scarborough","West Seneca","West Springfield","West Springfield","West Torrington","West Warwick","West and East Lealman","Westbrook","Westbury","Westchase","Westchester","Westerly","Westerville","Westfield","Westfield","Westford","Westlake","Westminster","Weston","Westport","Wethersfield","Weymouth","Wharton","Wheaton","Wheeling","White Oak","White Oak","White Plains","Whitehall","Whitehall Township","Whitestone","Whitman","Wilkes-Barre","Wilkinsburg","Williamsburg","Williamsburg","Williamsport","Williamstown","Willimantic","Willingboro","Willoughby","Willow Grove","Wilmington","Wilmington","Wilmington","Wilmington Island","Wilson","Wilton","Winchester","Winchester","Winchester","Winder","Windham","Windsor","Winston-Salem","Winter Garden","Winter Haven","Winter Park","Winter Springs","Winthrop","Woburn","Wolcott","Wolf Trap","Woodbridge","Woodhaven","Woodlawn","Woodlawn","Woodmere","Woodrow","Woodside","Woodstock","Woonsocket","Wooster","Worcester","Wyckoff","Xenia","Yarmouth","Yonkers","York","Youngstown","Zanesville"]} 2 | -------------------------------------------------------------------------------- /tests/jo-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jpmens/jo/d7b01392cc1a6bc381741cdbc062b06403bbdb91/tests/jo-logo.png -------------------------------------------------------------------------------- /tests/jo.01.exp: -------------------------------------------------------------------------------- 1 | ["jo"] 2 | -------------------------------------------------------------------------------- /tests/jo.01.sh: -------------------------------------------------------------------------------- 1 | # basic logo 2 | ${JO:-jo} -a jo 3 | -------------------------------------------------------------------------------- /tests/jo.02.exp: -------------------------------------------------------------------------------- 1 | ["jo"] 2 | -------------------------------------------------------------------------------- /tests/jo.02.sh: -------------------------------------------------------------------------------- 1 | # basic logo (stdin) 2 | echo jo | ${JO:-jo} -a 3 | -------------------------------------------------------------------------------- /tests/jo.03.exp: -------------------------------------------------------------------------------- 1 | {"pass":true,"type":"text"} 2 | -------------------------------------------------------------------------------- /tests/jo.03.sh: -------------------------------------------------------------------------------- 1 | # basic two values 2 | ${JO:-jo} pass=true type=text 3 | -------------------------------------------------------------------------------- /tests/jo.04.exp: -------------------------------------------------------------------------------- 1 | {"name":"Jane Jolie","data":{"age":null,"country":"ES"}} 2 | -------------------------------------------------------------------------------- /tests/jo.04.sh: -------------------------------------------------------------------------------- 1 | # nested with executable 2 | ${JO:-jo} name="Jane Jolie" data="$(${JO:-jo} age= country=ES)" 3 | 4 | # the double quotes around data are required for OpenBSD 5.8 5 | # which mucks up the input with its pdksh otherwise 6 | -------------------------------------------------------------------------------- /tests/jo.05.exp: -------------------------------------------------------------------------------- 1 | {"a":[1,2],"geo":{"lat":111,"lon":222}} 2 | -------------------------------------------------------------------------------- /tests/jo.05.sh: -------------------------------------------------------------------------------- 1 | # nested native 2 | ${JO:-jo} a[]=1 a[]=2 geo[lat]=111 geo[lon]=222 3 | -------------------------------------------------------------------------------- /tests/jo.06.exp: -------------------------------------------------------------------------------- 1 | { 2 | "artist": "Vanessa Paradis", 3 | "song": "Joe le taxi", 4 | "year": 1987 5 | } 6 | -------------------------------------------------------------------------------- /tests/jo.06.sh: -------------------------------------------------------------------------------- 1 | # strings and numbers; pretty (Vanessa) 2 | ${JO:-jo} -p artist="Vanessa Paradis" song="Joe le taxi" year=1987 3 | -------------------------------------------------------------------------------- /tests/jo.07.sh.in: -------------------------------------------------------------------------------- 1 | # version check 2 | [ "$(${JO:-jo} -v)" = "jo @PACKAGE_VERSION@" ] 3 | -------------------------------------------------------------------------------- /tests/jo.08.exp: -------------------------------------------------------------------------------- 1 | {"program":"jo","authors":"Jan-Piet Mens "} 2 | {"foo":"bar","baz":""} 3 | -------------------------------------------------------------------------------- /tests/jo.08.sh: -------------------------------------------------------------------------------- 1 | # data from file: text 2 | ${JO:-jo} program="jo" authors=@${srcdir:=.}/tests/jo-creator.txt 3 | ${JO:-jo} foo="bar" baz=@/dev/null 4 | -------------------------------------------------------------------------------- /tests/jo.09.exp: -------------------------------------------------------------------------------- 1 | {"program":"jo","authors":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} 2 | -------------------------------------------------------------------------------- /tests/jo.09.sh: -------------------------------------------------------------------------------- 1 | # data from file: base64-encoded 2 | ${JO:-jo} program="jo" authors=%${srcdir:=.}/tests/jo-creator.txt 3 | -------------------------------------------------------------------------------- /tests/jo.10.exp: -------------------------------------------------------------------------------- 1 | ["spring","summer","autumn","winter"] 2 | -------------------------------------------------------------------------------- /tests/jo.10.sh: -------------------------------------------------------------------------------- 1 | # array: simple 2 | ${JO:-jo} -a spring summer autumn winter 3 | -------------------------------------------------------------------------------- /tests/jo.11.exp: -------------------------------------------------------------------------------- 1 | [true,false,null,"\"true\"","\"false\"","\"null\""] 2 | -------------------------------------------------------------------------------- /tests/jo.11.sh: -------------------------------------------------------------------------------- 1 | # array: true,false,null (native and string) 2 | ${JO:-jo} -a true false null '"true"' '"false"' '"null"' 3 | -------------------------------------------------------------------------------- /tests/jo.12.exp: -------------------------------------------------------------------------------- 1 | { 2 | "type": "location", 3 | "cog": 120, 4 | "t": "u", 5 | "lat": 48.85833, 6 | "lon": 2.29513, 7 | "acc": 5, 8 | "tid": "JJ", 9 | "tst": 1457767154 10 | } 11 | -------------------------------------------------------------------------------- /tests/jo.12.sh: -------------------------------------------------------------------------------- 1 | # object: geo 2 | ${JO:-jo} -p type=location cog=120 t=u lat=48.85833 lon=2.29513 acc=5 tid=JJ tst=1457767154 3 | -------------------------------------------------------------------------------- /tests/jo.13.exp: -------------------------------------------------------------------------------- 1 | { 2 | "name": "This is jo", 3 | "px": [ 4 | 300, 5 | 300 6 | ], 7 | "face": "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", 8 | "meta": { 9 | "width": 300, 10 | "height": 300, 11 | "bytes": 8082, 12 | "type": "png", 13 | "creator": "JP" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/jo.13.sh: -------------------------------------------------------------------------------- 1 | # object: face card jo logo 2 | ${JO:-jo} -p name="This is jo" px[]=300 face=%${srcdir:=.}/tests/jo-logo.png meta[width]=300 px[]=300 meta[height]=300 meta[bytes]=8082 meta[type]=png meta[creator]=JP 3 | -------------------------------------------------------------------------------- /tests/jo.14.exp: -------------------------------------------------------------------------------- 1 | {"form":"=ok","this":"==sure"} 2 | -------------------------------------------------------------------------------- /tests/jo.14.sh: -------------------------------------------------------------------------------- 1 | # values: with equals signs in them 2 | ${JO:-jo} form==ok this===sure 3 | -------------------------------------------------------------------------------- /tests/jo.15.exp: -------------------------------------------------------------------------------- 1 | {"name":"Jane","obj":["eX","whY","Zed"]} 2 | -------------------------------------------------------------------------------- /tests/jo.15.sh: -------------------------------------------------------------------------------- 1 | # object from file 2 | tmp=/tmp/jo.$$ 3 | trap "rm -f $tmp; exit" 0 1 2 15 4 | 5 | ${JO:-jo} -a eX whY Zed > $tmp 6 | ${JO:-jo} name=Jane obj:=$tmp 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/jo.16.exp: -------------------------------------------------------------------------------- 1 | {"msg":"\"All's Well\", she said."} 2 | -------------------------------------------------------------------------------- /tests/jo.16.sh: -------------------------------------------------------------------------------- 1 | # quotes in quotes 2 | tmp=/tmp/jo.$$ 3 | trap "rm -f $tmp; exit" 0 1 2 15 4 | 5 | ${JO:-jo} msg='"All'\''s Well", she said.' 6 | 7 | 8 | -------------------------------------------------------------------------------- /tests/jo.17.exp: -------------------------------------------------------------------------------- 1 | {"s":"","n":0,"b":false,"a":null} 2 | {"s":"string","n":6,"b":true,"a":"string"} 3 | {"s":"\"quoted\"","n":8,"b":true,"a":"\"quoted\""} 4 | {"s":"12345","n":12345,"b":true,"a":12345} 5 | {"s":"true","n":1,"b":true,"a":true} 6 | {"s":"false","n":0,"b":false,"a":false} 7 | {"s":"","n":0,"b":false,"a":null} 8 | ["123",14,true,456] 9 | ["-s",2,true] 10 | ["--test","--toast"] 11 | {"--test":"--toast"} 12 | [true,"--toast","--test","--toast",6,"--toast"] 13 | {"s":false,"n":false,"b":false,"a":false} 14 | {"s":true,"n":true,"b":true,"a":true} 15 | {"s":"Jan-Piet Mens ","n":"Jan-Piet Mens ","b":"Jan-Piet Mens ","a":"Jan-Piet Mens "} 16 | {"s":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","n":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","b":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","a":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} 17 | -------------------------------------------------------------------------------- /tests/jo.17.sh: -------------------------------------------------------------------------------- 1 | # type coercion 2 | 3 | # coerce key=val 4 | for v in "" string \"quoted\" 12345 true false null; do 5 | ${JO:-jo} -- -s s="$v" -n n="$v" -b b="$v" a="$v" 6 | done 7 | 8 | # coerce array items 9 | ${JO:-jo} -a -- -s 123 -n "This is a test" -b C_Rocks 456 10 | 11 | # coercion flag strings should be usable as inputs, when they aren't flags 12 | ${JO:-jo} -a -- -s -s -n -n -b -b 13 | 14 | # non-flag strings should be read as normal strings, even if they begin with "-" 15 | ${JO:-jo} -a -- --test --toast 16 | ${JO:-jo} -- --test=--toast 17 | 18 | # coercion is one-shot, so all "--toast" strings are normal input 19 | ${JO:-jo} -a -- -b --test --toast -s --test --toast -n --test --toast 20 | 21 | ### These should NOT be coerced 22 | 23 | # @ booleans 24 | for v in 0 1; do 25 | ${JO:-jo} -- -s s@"$v" -n n@"$v" -b b@"$v" a@"$v" 26 | done 27 | 28 | # @/% file inclusions 29 | ${JO:-jo} -- -s s=@${srcdir:=.}/tests/jo-creator.txt -n n=@${srcdir:=.}/tests/jo-creator.txt -b b=@${srcdir:=.}/tests/jo-creator.txt a=@${srcdir:=.}/tests/jo-creator.txt 30 | ${JO:-jo} -- -s s=%${srcdir:=.}/tests/jo-creator.txt -n n=%${srcdir:=.}/tests/jo-creator.txt -b b=%${srcdir:=.}/tests/jo-creator.txt a=%${srcdir:=.}/tests/jo-creator.txt 31 | -------------------------------------------------------------------------------- /tests/jo.18.exp: -------------------------------------------------------------------------------- 1 | {"a.b":0,"a.c.d":1,"a.d.e":[2,"sam"],"a.c":{"f":true},"b.e":["hi"]} 2 | {"a":{"b":0,"c":{"d":1,"f":true},"d":{"e":[2,"sam"]}},"b":{"e":["hi"]}} 3 | {"a":{"b":0,"c":{"d":1,"f":true},"d":{"e":[2,"sam"]}},"b":{"e":["hi"]}} 4 | -------------------------------------------------------------------------------- /tests/jo.18.sh: -------------------------------------------------------------------------------- 1 | # nested objects with user-specified delimiter 2 | 3 | # without delimiter 4 | ${JO:-jo} a.b=0 a.c.d=1 a.d.e[]=2 a.d.e[]=sam a.c[f]@1 b.e[]g=hi 5 | # with delimiter 6 | ${JO:-jo} -d. a.b=0 a.c.d=1 a.d.e[]=2 a.d.e[]=sam a.c[f]@1 b.e[]g=hi 7 | # with more complex delimiter 8 | ${JO:-jo} -d\|first_char_only a\|b=0 a\|c\|d=1 a\|d\|e[]=2 a\|d\|e[]=sam a\|c[f]@1 b\|e[]g=hi 9 | -------------------------------------------------------------------------------- /tests/jo.19.exp: -------------------------------------------------------------------------------- 1 | {"foo":["hello world"]} 2 | -------------------------------------------------------------------------------- /tests/jo.19.sh: -------------------------------------------------------------------------------- 1 | # read from pipe 2 | 3 | echo '["hello world"]' | ${JO:-jo} foo:=- 4 | -------------------------------------------------------------------------------- /tests/jo.20.sh: -------------------------------------------------------------------------------- 1 | # read json array elements 2 | 3 | echo '{"a":1,"b":"val"}' > $$.1 4 | echo '{"a":478,"b":"other"}' > $$.2 5 | 6 | ${JO:-jo} -a :$$.1 :$$.2 7 | 8 | rm -f $$.1 9 | rm -f $$.2 10 | 11 | # read large json array elements 12 | ${JO:-jo} -a < ${srcdir:=.}/tests/jo-large1.json 13 | ${JO:-jo} -a :${srcdir:=.}/tests/jo-large2.json :${srcdir:=.}/tests/jo-large1.json 14 | -------------------------------------------------------------------------------- /tests/jo.21.exp: -------------------------------------------------------------------------------- 1 | {"nested":{"a":1,"b":"val"}} 2 | {"top":{"obj1":{"c":3,"d":"key"},"obj2":{"a":1,"b":"val"}}} 3 | -------------------------------------------------------------------------------- /tests/jo.21.sh: -------------------------------------------------------------------------------- 1 | # read nested json elements 2 | 3 | echo '{"a":1,"b":"val"}' > $$.1 4 | 5 | ${JO:-jo} nested=:$$.1 6 | 7 | # nested json within object path 8 | ${JO:-jo} -d . top.obj1.c=3 top.obj1.d="key" top.obj2=:$$.1 9 | 10 | rm -f $$.1 11 | -------------------------------------------------------------------------------- /tests/jo.22.exp: -------------------------------------------------------------------------------- 1 | {"key":"@timestamp"} 2 | -------------------------------------------------------------------------------- /tests/jo.22.sh: -------------------------------------------------------------------------------- 1 | # avoid reading from file if escaped \@ 2 | 3 | ${JO:-jo} key="\@timestamp" 4 | 5 | -------------------------------------------------------------------------------- /tests/jo.23.exp: -------------------------------------------------------------------------------- 1 | {"foo":null} 2 | {} 3 | {"foo":1,"bar":null,"baz":3} 4 | {"foo":1,"baz":3} 5 | {"list":[1,null]} 6 | {"list":[1]} 7 | -------------------------------------------------------------------------------- /tests/jo.23.sh: -------------------------------------------------------------------------------- 1 | # disable creation of null-valued keys 2 | 3 | ${JO:-jo} foo= 4 | ${JO:-jo} -n foo= 5 | ${JO:-jo} foo=1 bar= baz=3 6 | ${JO:-jo} -n foo=1 bar= baz=3 7 | nothing= 8 | ${JO:-jo} list[]=1 list[]=$nothing 9 | ${JO:-jo} -n list[]=1 list[]=$nothing 10 | 11 | -------------------------------------------------------------------------------- /tests/jo.24.exp: -------------------------------------------------------------------------------- 1 | [1,2,3,4,6,8] 2 | {"a":1,"b":2,"c":42,"d":3} 3 | {"a":1,"b":2,"c":42,"d":3,"stage":{"1":"a","2":"b"}} 4 | {"a":1,"b":2,"c":42,"d":3,"stage":{"1":"a","2":"b"}} 5 | {"a":1,"b":2,"c":42,"d":3,"stage":{"1":"a","2":"b"}} 6 | {"a":1,"b":2,"stage":{"1":"a","2":"b"}} 7 | {"people":"need people"} 8 | {"people":"need people"} 9 | -------------------------------------------------------------------------------- /tests/jo.24.sh: -------------------------------------------------------------------------------- 1 | # jo as filter 2 | 3 | # filter array 4 | echo "[1,2,3,4]" | ${JO:-jo} -f - 6 8 5 | 6 | # filter object 7 | ${JO:-jo} a=1 b=2 | ${JO:-jo} -f - c=42 d=3 8 | 9 | # multi-stage pipeline 10 | ${JO:-jo} a=1 b=2 | ${JO:-jo} -f - c=42 d=3 | ${JO:-jo} -f - -d . stage.1=a stage.2=b 11 | 12 | # filter from file 13 | tmp=/tmp/jo.filter.$$ 14 | trap "rm -f $tmp; exit" 0 1 2 15 15 | 16 | ${JO:-jo} a=1 b=2 > $tmp 17 | ${JO:-jo} -f $tmp c=42 d=3 | ${JO:-jo} -f - -d . stage.1=a stage.2=b 18 | 19 | # take initial object from file, and mods from stdin 20 | echo "c=42 21 | d=3" | ${JO:-jo} -f $tmp | ${JO:-jo} -f - -d . stage.1=a stage.2=b 22 | 23 | # this command should NOT output keys "c" and "d" 24 | ${JO:-jo} a=1 b=2 | ${JO:-jo} -f - c=42 d=3 | ${JO:-jo} -f $tmp -d . stage.1=a stage.2=b 25 | 26 | # filter non-collections (input basically ignored) 27 | echo hi | tee $tmp | ${JO:-jo} -f - people="need people" 28 | ${JO:-jo} -f $tmp people="need people" 29 | -------------------------------------------------------------------------------- /tests/jo.25.exp: -------------------------------------------------------------------------------- 1 | {"a":1,"b":2,"a":3} 2 | {"a":3,"b":2} 3 | {"stage":{"1":"a","2":"b","3":"c","2":"x","3":"y","4":"d","1":"h"},"down":"up"} 4 | {"stage":{"1":"h","2":"x","3":"y","4":"d"},"down":"up"} 5 | {"name":"aaa"} 6 | {"name":"cba","another_name":"abc"} 7 | -------------------------------------------------------------------------------- /tests/jo.25.sh: -------------------------------------------------------------------------------- 1 | # overwrite values of existing object keys 2 | 3 | ${JO:-jo} a=1 b=2 a=3 4 | ${JO:-jo} -D a=1 b=2 a=3 5 | 6 | tmp=`${JO:-jo} 1=a 2=b 3=c` 7 | ${JO:-jo} -d . stage="$tmp" down=up stage.2=x stage\[3\]=y stage.4=d stage\[1\]=h 8 | ${JO:-jo} -D -d . stage="$tmp" down=up stage.2=x stage\[3\]=y stage.4=d stage\[1\]=h 9 | 10 | # dedup filter input too 11 | tmpf=$$.json 12 | trap 'rm -f "$tmpf"' 0 1 2 15 13 | ${JO:-jo} name=aaa name=aaa | tee $tmpf | ${JO:-jo} -D -f - name=aaa 14 | ${JO:-jo} -D -f $tmpf another_name=abc name=cba 15 | -------------------------------------------------------------------------------- /tests/jo.26.exp: -------------------------------------------------------------------------------- 1 | {"file":"stdin","jo":true} 2 | -------------------------------------------------------------------------------- /tests/jo.26.sh: -------------------------------------------------------------------------------- 1 | # read from stdin 2 | echo '{"file":"stdin", "jo": true}' | ${JO:-jo} -f - 3 | -------------------------------------------------------------------------------- /tests/jo.27.exp: -------------------------------------------------------------------------------- 1 | {"b":[1,2]} 2 | jo: JSON_ERR: Cannot add {"a":3} to non-object [1] 3 | Test 2 should fail 4 | {"d":{"m":10,"n":20}} 5 | jo: JSON_ERR: Cannot append 20 to non-array {"m":10} 6 | Test 4 should fail 7 | -------------------------------------------------------------------------------- /tests/jo.27.sh: -------------------------------------------------------------------------------- 1 | # user-friendly errors 2 | ${JO:-jo} b[]=1 b[]=2 3 | ${JO:-jo} b[]=1 b[a]=3 2>&1 || echo "Test 2 should fail" 4 | 5 | ${JO:-jo} d[m]=10 d[n]=20 6 | ${JO:-jo} d[m]=10 d[]=20 2>&1 || echo "Test 4 should fail" 7 | -------------------------------------------------------------------------------- /tests/jo.test: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # input file (jo.??.in) is a shell script; the first line must 4 | # be a comment and is used as the name of the test: 5 | # 6 | # # basic logo 7 | # $JO -a jo 8 | # 9 | # the expect file (jo.??.exp) is a file which will be diff'd 10 | # against the output of the corresponding .in file: 11 | # 12 | # ["jo"] 13 | # 14 | 15 | 16 | JO="$(pwd)/jo" 17 | TESTDIR="${0%/*}" 18 | NTESTS=$(expr $(ls $TESTDIR/jo.??.sh 2>/dev/null | wc -l)) 19 | 20 | export JO 21 | export TESTDIR 22 | 23 | echo "1..$NTESTS" # Number of tests to be executed. 24 | 25 | n=0 26 | for t in $TESTDIR/jo.??.sh; do 27 | n=$(expr $n + 1) 28 | input=$t 29 | expected="$TESTDIR/$(basename $t .sh).exp" 30 | output=$(mktemp /tmp/jo.XXXXXX) 31 | 32 | title=$(head -1 $input | sed -e 's/^#//') 33 | sh $input > $output 34 | RC=$? 35 | if [ $RC -ne 0 ]; then 36 | echo "not ok $n - $title" 37 | rm -f $output 38 | continue 39 | fi 40 | 41 | # if self-contained test (i.e. no 'expected' file exists) 42 | # use the previous RC 43 | if test -f "$expected"; then 44 | diff "$output" "$expected" > /dev/null 45 | RC=$? 46 | fi 47 | 48 | status="ok" 49 | if [ $RC -ne 0 ]; then 50 | status="not ok" 51 | fi 52 | 53 | echo "$status $n - $title" 54 | rm -f $output 55 | done 56 | --------------------------------------------------------------------------------