pax_global_header00006660000000000000000000000064137046205260014517gustar00rootroot0000000000000052 comment=dd9c49e32f36ebbfa341d2e23b540e40bcd418a8 jo-1.4/000077500000000000000000000000001370462052600117735ustar00rootroot00000000000000jo-1.4/.gitignore000066400000000000000000000006701370462052600137660ustar00rootroot00000000000000*.dSYM .DS_Store *.o *.so *.la *.lai *.so.* # Autotools junk .libs .deps .dirstamp libtool *.log stamp-h1 config.log config.status autom4te.cache INSTALL Makefile jo-*.tar.gz configure aclocal.m4 Makefile.in version.h config.cache m4/libtool.m4 m4/ltoptions.m4 m4/ltsugar.m4 m4/ltversion.m4 m4/lt~obsolete.m4 *.trs install-sh depcomp missing build-aux/compile build-aux/depcomp build-aux/install-sh build-aux/missing tests/jo.07.sh jo g jo-1.4/.travis.yml000066400000000000000000000003351370462052600141050ustar00rootroot00000000000000os: - linux - osx sudo: false language: c compiler: - gcc - clang before_install: - echo $TRAVIS_OS_NAME - uname -a - echo $LANG - echo $LC_ALL before_script: - autoreconf -i - ./configure script: - make check jo-1.4/AUTHORS000066400000000000000000000000411370462052600130360ustar00rootroot00000000000000Jan-Piet Mens jo-1.4/COPYING000066400000000000000000000037401370462052600130320ustar00rootroot00000000000000/* * Copyright (C) 2016 Jan-Piet Mens * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #-------------------------------------------------------------------- json.[ch] /* Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ jo-1.4/ChangeLog000066400000000000000000000040521370462052600135460ustar00rootroot000000000000002020-07-18 1.4 - FIX: Coercion flag logic now permits getopt(3) double-dash - FIX: Documentation clarifies special characters - FIX: Jo builds on snap builds (#110) - FIX: Jo builds on systems with slightly older pkg-config (#107) 2019-11-04 1.3 - FIX: Escaped @ ("\@") is treated as "@" (#42, #103) - NEW: Support reading JSON array elements (#91) - UPD: Add home and removable-media interfaces to snap (#94) - FIX: fix unlikely crash after malloc fail when base64 encoding. - NEW: Support reading nested data from pipes (#82) 2018-12-10 1.2 - NEW: Dockerfile (#76) - UPD: add examples of empty arrays/objects to manual (#74) - NEW: support -e to ignore empty stdin; contributed by Robi Karp - NEW: object-path support (#57) 2017-05-18 1.1 - NEW: type coercion (#55) - FIX: quotes in quotes and double quotes at begin of string (#47) - FIX: catch null value in assignmen (#46) - NEW: support for key:=file.json for reading object values from a file (#43) - NEW: PPA contributed by Ross Duggan in #32 - FIX: "null" is now handled like we handle "true" and "false"; disable with -B - NEW: more tests in the test suite 2016-03-11 1.0 - NEW: read JSON element values from files (#22) - FIX: usage diagnostic - NEW: add support for OpenBSD pledge(2) (#21) 2016-03-10 0.9 - UPD: revert support for $JO_PRETTY et. al; it was a bad idea 2016-03-10 0.8 - UPD: new test suite - NEW: support for nested elements (#19) - NEW: if $JO_PRETTY is set, jo will always pretty-print - NEW: Define $JO_SPACER to any desired number of spaces or tabs for pretty-printing (#18) 2016-03-09 0.7 - NEW: strings "true"/"false" now default to booleans; avoid with -B (#17) - FIX: test.sh get quotes to prevent failures with pdksh (#16) - FIX: pretty-print Version if requested (#15) - FIX: Add cast to suppress warning when compiling with GCC 4.8.4 (#14) 2016-03-08 0.6 - FIX: make build work on CentOS 6 (#13) - NEW: JSONy version with -V 2016-03-08 0.5 - FIX: fileno error (#12) 2016-03-08 0.4 - NEW: Win32 support for CJK contributed by @mattn 2016-03-08 0.3 - NEW: autotools support - NEW: option -v jo-1.4/Dockerfile000066400000000000000000000003721370462052600137670ustar00rootroot00000000000000FROM alpine AS builder RUN apk -U add automake autoconf build-base make pkgconf COPY . /src WORKDIR /src RUN autoreconf -i && ./configure && make check && make install FROM alpine COPY --from=builder /usr/local/bin/jo /bin/jo ENTRYPOINT ["/bin/jo"] jo-1.4/Makefile.am000066400000000000000000000030631370462052600140310ustar00rootroot00000000000000 AM_CFLAGS = -Wall -O2 bin_PROGRAMS = jo jo_SOURCES = jo.c json.c json.h base64.c base64.h jo_EXTRA = jo.pandoc dist_man_MANS = jo.1 jo_LDADD = -lm bashcompdir = @bashcompdir@ dist_bashcomp_DATA = jo.bash if USE_PANDOC # Add targets to rebuild pages jo.1: jo.pandoc @test -n "$(PANDOC)" || \ { echo 'pandoc' not found during configure.; exit 1; } $(PANDOC) -s -w man+simple_tables -o $@ $< jo.md: jo.pandoc @test -n "$(PANDOC)" || \ { echo 'pandoc' not found during configure.; exit 1; } $(PANDOC) -s -w markdown+simple_tables -o $@ $< endif # docdir = $(datadir)/doc/@PACKAGE@ # doc_DATA = README.md # If on OS/X, fail if $COPYFILE_DISABLE is not in the environment # so that tar doesn't bundle the AppleDouble attributes dist-hook: if test $$(uname -s) = "Darwin" -a "x$$COPYFILE_DISABLE" = "x"; then echo "Set COPYFILE_DISABLE before making dist" >&2; exit 2; fi TEST_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) \ $(top_srcdir)/build-aux/tap-driver.sh TESTS = tests/jo.test EXTRA_DIST = $(jo_EXTRA) \ $(TESTS) \ tests/jo.01.sh tests/jo.01.exp \ tests/jo.02.sh tests/jo.02.exp \ tests/jo.03.sh tests/jo.03.exp \ tests/jo.04.sh tests/jo.04.exp \ tests/jo.05.sh tests/jo.05.exp \ tests/jo.06.sh tests/jo.06.exp \ tests/jo.07.sh.in \ tests/jo.08.sh tests/jo.08.exp \ tests/jo.09.sh tests/jo.09.exp \ tests/jo.10.sh tests/jo.10.exp \ tests/jo.11.sh tests/jo.11.exp \ tests/jo.12.sh tests/jo.12.exp \ tests/jo.13.sh tests/jo.13.exp tests/jo-logo.png \ tests/jo.14.sh tests/jo.14.exp jo-1.4/NEWS000066400000000000000000000000001370462052600124600ustar00rootroot00000000000000jo-1.4/README000077700000000000000000000000001370462052600141252README.mdustar00rootroot00000000000000jo-1.4/README.md000066400000000000000000000037041370462052600132560ustar00rootroot00000000000000# jo ![jo logo](tests/jo-logo.png) This is `jo`, a small utility to create JSON objects ```bash $ jo -p name=jo n=17 parser=false { "name": "jo", "n": 17, "parser": false } ``` or arrays ```bash $ seq 1 10 | jo -a [1,2,3,4,5,6,7,8,9,10] ``` 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/). ## Build from Release tarball 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). ```bash tar xvzf jo-1.3.tar.gz cd jo-1.3 autoreconf -i ./configure make check make install ``` ## Build from Github [![Build Status](https://api.travis-ci.org/jpmens/jo.svg?branch=master)](https://travis-ci.org/jpmens/jo) To install from the repository, you will need a C compiler as well as a relatively recent version of _automake_ and _autoconf_. ```bash git clone git://github.com/jpmens/jo.git cd jo autoreconf -i ./configure make check make install ``` ## Homebrew ```bash brew install jo ``` ## Ubuntu ``` apt-get install jo ``` ## Gentoo ``` emerge jo ``` ## Snap 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. ## Others * [voidlinux](https://github.com/voidlinux/void-packages/tree/master/srcpkgs/jo) * [ArchLinux](https://aur.archlinux.org/packages/jo/) * [OpenBSD](http://openports.se/textproc/jo) * [FreeBSD](https://www.freshports.org/textproc/jo) * [pkgsrc](http://pkgsrc.se/textproc/jo) * [repology.org](https://repology.org/metapackage/jo/versions) ## See also * [gjo](https://github.com/skanehira/gjo) * [rjo](https://github.com/dskkato/rjo) * [jjo](https://github.com/memoryhole/jjo) ## Credits * `json.[ch]` by 2011 Joseph A. Adams (joeyadams3.14159@gmail.com). jo-1.4/base64.c000066400000000000000000000045451370462052600132330ustar00rootroot00000000000000/* This code is public domain software. */ #include "base64.h" #include #include // base64 encoding // // buf: binary input data // size: size of input (bytes) // return: base64-encoded string (null-terminated) // memory for output will be allocated here, free it later // char* base64_encode(const void* buf, size_t size) { static const char base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; char* str = (char*) malloc((size+3)*4/3 + 1); char* p = str; const unsigned char* q = (const unsigned char*) buf; size_t i = 0; if (str == NULL) { return NULL; } while (i < size) { int c = q[i++]; c *= 256; if (i < size) c += q[i]; i++; c *= 256; if (i < size) c += q[i]; i++; *p++ = base64[(c & 0x00fc0000) >> 18]; *p++ = base64[(c & 0x0003f000) >> 12]; if (i > size + 1) *p++ = '='; else *p++ = base64[(c & 0x00000fc0) >> 6]; if (i > size) *p++ = '='; else *p++ = base64[c & 0x0000003f]; } *p = 0; return str; } // single base64 character conversion // static int POS(char c) { if (c>='A' && c<='Z') return c - 'A'; if (c>='a' && c<='z') return c - 'a' + 26; if (c>='0' && c<='9') return c - '0' + 52; if (c == '+') return 62; if (c == '/') return 63; if (c == '=') return -1; return -2; } // base64 decoding // // s: base64 string, must be null-terminated // data: output buffer for decoded data // data_len size of decoded data // return: allocated data buffer // void* base64_decode(const char* s, size_t *data_len) { const char *p; unsigned char *q, *data; int n[4] = { 0, 0, 0, 0 }; size_t len = strlen(s); if (len % 4) return NULL; data = (unsigned char*) malloc(len/4*3); q = (unsigned char*) data; for (p = s; *p; ) { n[0] = POS(*p++); n[1] = POS(*p++); n[2] = POS(*p++); n[3] = POS(*p++); if (n[0] == -2 || n[1] == -2 || n[2] == -2 || n[3] == -2) return NULL; if (n[0] == -1 || n[1] == -1) return NULL; if (n[2] == -1 && n[3] != -1) return NULL; q[0] = (n[0] << 2) + (n[1] >> 4); if (n[2] != -1) q[1] = ((n[1] & 15) << 4) + (n[2] >> 2); if (n[3] != -1) q[2] = ((n[2] & 3) << 6) + n[3]; q += 3; } *data_len = q-data - (n[2]==-1) - (n[3]==-1); return data; } jo-1.4/base64.h000066400000000000000000000002651370462052600132330ustar00rootroot00000000000000#ifndef BASE64_H_GUARD #define BASE64_H_GUARD #include char* base64_encode(const void* buf, size_t size); void* base64_decode(const char* s, size_t *data_len); #endif jo-1.4/build-aux/000077500000000000000000000000001370462052600136655ustar00rootroot00000000000000jo-1.4/build-aux/tap-driver.sh000077500000000000000000000460721370462052600163120ustar00rootroot00000000000000#! /bin/sh # Copyright (C) 2011-2013 Free Software Foundation, Inc. # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2, or (at your option) # any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # As a special exception to the GNU General Public License, if you # distribute this file as part of a program that contains a # configuration script generated by Autoconf, you may include it under # the same distribution terms that you use for the rest of that program. # This file is maintained in Automake, please report # bugs to or send patches to # . scriptversion=2011-12-27.17; # UTC # Make unconditional expansion of undefined variables an error. This # helps a lot in preventing typo-related bugs. set -u me=tap-driver.sh fatal () { echo "$me: fatal: $*" >&2 exit 1 } usage_error () { echo "$me: $*" >&2 print_usage >&2 exit 2 } print_usage () { cat < # trap : 1 3 2 13 15 if test $merge -gt 0; then exec 2>&1 else exec 2>&3 fi "$@" echo $? ) | LC_ALL=C ${AM_TAP_AWK-awk} \ -v me="$me" \ -v test_script_name="$test_name" \ -v log_file="$log_file" \ -v trs_file="$trs_file" \ -v expect_failure="$expect_failure" \ -v merge="$merge" \ -v ignore_exit="$ignore_exit" \ -v comments="$comments" \ -v diag_string="$diag_string" \ ' # FIXME: the usages of "cat >&3" below could be optimized when using # FIXME: GNU awk, and/on on systems that supports /dev/fd/. # Implementation note: in what follows, `result_obj` will be an # associative array that (partly) simulates a TAP result object # from the `TAP::Parser` perl module. ## ----------- ## ## FUNCTIONS ## ## ----------- ## function fatal(msg) { print me ": " msg | "cat >&2" exit 1 } function abort(where) { fatal("internal error " where) } # Convert a boolean to a "yes"/"no" string. function yn(bool) { return bool ? "yes" : "no"; } function add_test_result(result) { if (!test_results_index) test_results_index = 0 test_results_list[test_results_index] = result test_results_index += 1 test_results_seen[result] = 1; } # Whether the test script should be re-run by "make recheck". function must_recheck() { for (k in test_results_seen) if (k != "XFAIL" && k != "PASS" && k != "SKIP") return 1 return 0 } # Whether the content of the log file associated to this test should # be copied into the "global" test-suite.log. function copy_in_global_log() { for (k in test_results_seen) if (k != "PASS") return 1 return 0 } # FIXME: this can certainly be improved ... function get_global_test_result() { if ("ERROR" in test_results_seen) return "ERROR" if ("FAIL" in test_results_seen || "XPASS" in test_results_seen) return "FAIL" all_skipped = 1 for (k in test_results_seen) if (k != "SKIP") all_skipped = 0 if (all_skipped) return "SKIP" return "PASS"; } function stringify_result_obj(result_obj) { if (result_obj["is_unplanned"] || result_obj["number"] != testno) return "ERROR" if (plan_seen == LATE_PLAN) return "ERROR" if (result_obj["directive"] == "TODO") return result_obj["is_ok"] ? "XPASS" : "XFAIL" if (result_obj["directive"] == "SKIP") return result_obj["is_ok"] ? "SKIP" : COOKED_FAIL; if (length(result_obj["directive"])) abort("in function stringify_result_obj()") return result_obj["is_ok"] ? COOKED_PASS : COOKED_FAIL } function decorate_result(result) { color_name = color_for_result[result] if (color_name) return color_map[color_name] "" result "" color_map["std"] # If we are not using colorized output, or if we do not know how # to colorize the given result, we should return it unchanged. return result } function report(result, details) { if (result ~ /^(X?(PASS|FAIL)|SKIP|ERROR)/) { msg = ": " test_script_name add_test_result(result) } else if (result == "#") { msg = " " test_script_name ":" } else { abort("in function report()") } if (length(details)) msg = msg " " details # Output on console might be colorized. print decorate_result(result) msg # Log the result in the log file too, to help debugging (this is # especially true when said result is a TAP error or "Bail out!"). print result msg | "cat >&3"; } function testsuite_error(error_message) { report("ERROR", "- " error_message) } function handle_tap_result() { details = result_obj["number"]; if (length(result_obj["description"])) details = details " " result_obj["description"] if (plan_seen == LATE_PLAN) { details = details " # AFTER LATE PLAN"; } else if (result_obj["is_unplanned"]) { details = details " # UNPLANNED"; } else if (result_obj["number"] != testno) { details = sprintf("%s # OUT-OF-ORDER (expecting %d)", details, testno); } else if (result_obj["directive"]) { details = details " # " result_obj["directive"]; if (length(result_obj["explanation"])) details = details " " result_obj["explanation"] } report(stringify_result_obj(result_obj), details) } # `skip_reason` should be empty whenever planned > 0. function handle_tap_plan(planned, skip_reason) { planned += 0 # Avoid getting confused if, say, `planned` is "00" if (length(skip_reason) && planned > 0) abort("in function handle_tap_plan()") if (plan_seen) { # Error, only one plan per stream is acceptable. testsuite_error("multiple test plans") return; } planned_tests = planned # The TAP plan can come before or after *all* the TAP results; we speak # respectively of an "early" or a "late" plan. If we see the plan line # after at least one TAP result has been seen, assume we have a late # plan; in this case, any further test result seen after the plan will # be flagged as an error. plan_seen = (testno >= 1 ? LATE_PLAN : EARLY_PLAN) # If testno > 0, we have an error ("too many tests run") that will be # automatically dealt with later, so do not worry about it here. If # $plan_seen is true, we have an error due to a repeated plan, and that # has already been dealt with above. Otherwise, we have a valid "plan # with SKIP" specification, and should report it as a particular kind # of SKIP result. if (planned == 0 && testno == 0) { if (length(skip_reason)) skip_reason = "- " skip_reason; report("SKIP", skip_reason); } } function extract_tap_comment(line) { if (index(line, diag_string) == 1) { # Strip leading `diag_string` from `line`. line = substr(line, length(diag_string) + 1) # And strip any leading and trailing whitespace left. sub("^[ \t]*", "", line) sub("[ \t]*$", "", line) # Return what is left (if any). return line; } return ""; } # When this function is called, we know that line is a TAP result line, # so that it matches the (perl) RE "^(not )?ok\b". function setup_result_obj(line) { # Get the result, and remove it from the line. result_obj["is_ok"] = (substr(line, 1, 2) == "ok" ? 1 : 0) sub("^(not )?ok[ \t]*", "", line) # If the result has an explicit number, get it and strip it; otherwise, # automatically assing the next progresive number to it. if (line ~ /^[0-9]+$/ || line ~ /^[0-9]+[^a-zA-Z0-9_]/) { match(line, "^[0-9]+") # The final `+ 0` is to normalize numbers with leading zeros. result_obj["number"] = substr(line, 1, RLENGTH) + 0 line = substr(line, RLENGTH + 1) } else { result_obj["number"] = testno } if (plan_seen == LATE_PLAN) # No further test results are acceptable after a "late" TAP plan # has been seen. result_obj["is_unplanned"] = 1 else if (plan_seen && testno > planned_tests) result_obj["is_unplanned"] = 1 else result_obj["is_unplanned"] = 0 # Strip trailing and leading whitespace. sub("^[ \t]*", "", line) sub("[ \t]*$", "", line) # This will have to be corrected if we have a "TODO"/"SKIP" directive. result_obj["description"] = line result_obj["directive"] = "" result_obj["explanation"] = "" if (index(line, "#") == 0) return # No possible directive, nothing more to do. # Directives are case-insensitive. rx = "[ \t]*#[ \t]*([tT][oO][dD][oO]|[sS][kK][iI][pP])[ \t]*" # See whether we have the directive, and if yes, where. pos = match(line, rx "$") if (!pos) pos = match(line, rx "[^a-zA-Z0-9_]") # If there was no TAP directive, we have nothing more to do. if (!pos) return # Let`s now see if the TAP directive has been escaped. For example: # escaped: ok \# SKIP # not escaped: ok \\# SKIP # escaped: ok \\\\\# SKIP # not escaped: ok \ # SKIP if (substr(line, pos, 1) == "#") { bslash_count = 0 for (i = pos; i > 1 && substr(line, i - 1, 1) == "\\"; i--) bslash_count += 1 if (bslash_count % 2) return # Directive was escaped. } # Strip the directive and its explanation (if any) from the test # description. result_obj["description"] = substr(line, 1, pos - 1) # Now remove the test description from the line, that has been dealt # with already. line = substr(line, pos) # Strip the directive, and save its value (normalized to upper case). sub("^[ \t]*#[ \t]*", "", line) result_obj["directive"] = toupper(substr(line, 1, 4)) line = substr(line, 5) # Now get the explanation for the directive (if any), with leading # and trailing whitespace removed. sub("^[ \t]*", "", line) sub("[ \t]*$", "", line) result_obj["explanation"] = line } function get_test_exit_message(status) { if (status == 0) return "" if (status !~ /^[1-9][0-9]*$/) abort("getting exit status") if (status < 127) exit_details = "" else if (status == 127) exit_details = " (command not found?)" else if (status >= 128 && status <= 255) exit_details = sprintf(" (terminated by signal %d?)", status - 128) else if (status > 256 && status <= 384) # We used to report an "abnormal termination" here, but some Korn # shells, when a child process die due to signal number n, can leave # in $? an exit status of 256+n instead of the more standard 128+n. # Apparently, both behaviours are allowed by POSIX (2008), so be # prepared to handle them both. See also Austing Group report ID # 0000051 exit_details = sprintf(" (terminated by signal %d?)", status - 256) else # Never seen in practice. exit_details = " (abnormal termination)" return sprintf("exited with status %d%s", status, exit_details) } function write_test_results() { print ":global-test-result: " get_global_test_result() > trs_file print ":recheck: " yn(must_recheck()) > trs_file print ":copy-in-global-log: " yn(copy_in_global_log()) > trs_file for (i = 0; i < test_results_index; i += 1) print ":test-result: " test_results_list[i] > trs_file close(trs_file); } BEGIN { ## ------- ## ## SETUP ## ## ------- ## '"$init_colors"' # Properly initialized once the TAP plan is seen. planned_tests = 0 COOKED_PASS = expect_failure ? "XPASS": "PASS"; COOKED_FAIL = expect_failure ? "XFAIL": "FAIL"; # Enumeration-like constants to remember which kind of plan (if any) # has been seen. It is important that NO_PLAN evaluates "false" as # a boolean. NO_PLAN = 0 EARLY_PLAN = 1 LATE_PLAN = 2 testno = 0 # Number of test results seen so far. bailed_out = 0 # Whether a "Bail out!" directive has been seen. # Whether the TAP plan has been seen or not, and if yes, which kind # it is ("early" is seen before any test result, "late" otherwise). plan_seen = NO_PLAN ## --------- ## ## PARSING ## ## --------- ## is_first_read = 1 while (1) { # Involutions required so that we are able to read the exit status # from the last input line. st = getline if (st < 0) # I/O error. fatal("I/O error while reading from input stream") else if (st == 0) # End-of-input { if (is_first_read) abort("in input loop: only one input line") break } if (is_first_read) { is_first_read = 0 nextline = $0 continue } else { curline = nextline nextline = $0 $0 = curline } # Copy any input line verbatim into the log file. print | "cat >&3" # Parsing of TAP input should stop after a "Bail out!" directive. if (bailed_out) continue # TAP test result. if ($0 ~ /^(not )?ok$/ || $0 ~ /^(not )?ok[^a-zA-Z0-9_]/) { testno += 1 setup_result_obj($0) handle_tap_result() } # TAP plan (normal or "SKIP" without explanation). else if ($0 ~ /^1\.\.[0-9]+[ \t]*$/) { # The next two lines will put the number of planned tests in $0. sub("^1\\.\\.", "") sub("[^0-9]*$", "") handle_tap_plan($0, "") continue } # TAP "SKIP" plan, with an explanation. else if ($0 ~ /^1\.\.0+[ \t]*#/) { # The next lines will put the skip explanation in $0, stripping # any leading and trailing whitespace. This is a little more # tricky in truth, since we want to also strip a potential leading # "SKIP" string from the message. sub("^[^#]*#[ \t]*(SKIP[: \t][ \t]*)?", "") sub("[ \t]*$", ""); handle_tap_plan(0, $0) } # "Bail out!" magic. # Older versions of prove and TAP::Harness (e.g., 3.17) did not # recognize a "Bail out!" directive when preceded by leading # whitespace, but more modern versions (e.g., 3.23) do. So we # emulate the latter, "more modern" behaviour. else if ($0 ~ /^[ \t]*Bail out!/) { bailed_out = 1 # Get the bailout message (if any), with leading and trailing # whitespace stripped. The message remains stored in `$0`. sub("^[ \t]*Bail out![ \t]*", ""); sub("[ \t]*$", ""); # Format the error message for the bailout_message = "Bail out!" if (length($0)) bailout_message = bailout_message " " $0 testsuite_error(bailout_message) } # Maybe we have too look for dianogtic comments too. else if (comments != 0) { comment = extract_tap_comment($0); if (length(comment)) report("#", comment); } } ## -------- ## ## FINISH ## ## -------- ## # A "Bail out!" directive should cause us to ignore any following TAP # error, as well as a non-zero exit status from the TAP producer. if (!bailed_out) { if (!plan_seen) { testsuite_error("missing test plan") } else if (planned_tests != testno) { bad_amount = testno > planned_tests ? "many" : "few" testsuite_error(sprintf("too %s tests run (expected %d, got %d)", bad_amount, planned_tests, testno)) } if (!ignore_exit) { # Fetch exit status from the last line. exit_message = get_test_exit_message(nextline) if (exit_message) testsuite_error(exit_message) } } write_test_results() exit 0 } # End of "BEGIN" block. ' # TODO: document that we consume the file descriptor 3 :-( } 3>"$log_file" test $? -eq 0 || fatal "I/O or internal error" # Local Variables: # mode: shell-script # sh-indentation: 2 # eval: (add-hook 'write-file-hooks 'time-stamp) # time-stamp-start: "scriptversion=" # time-stamp-format: "%:y-%02m-%02d.%02H" # time-stamp-time-zone: "UTC" # time-stamp-end: "; # UTC" # End: jo-1.4/configure.ac000066400000000000000000000032301370462052600142570ustar00rootroot00000000000000AC_PREREQ([2.63]) AC_INIT([jo], [1.4], [jp@mens.de]) AC_CONFIG_AUX_DIR([build-aux]) AC_CONFIG_SRCDIR([jo.c]) # Checks for programs. AC_PROG_CC # Checks for header files. AC_USE_SYSTEM_EXTENSIONS AC_CHECK_HEADERS([stddef.h stdint.h stdlib.h string.h unistd.h stdbool.h]) # Checks for library functions. # AC_FUNC_MALLOC # AC_FUNC_REALLOC AC_FUNC_STRTOD AC_CHECK_FUNCS([strchr strrchr strlcpy strlcat snprintf pledge err errx]) # backport PKG_CHECK_VAR from pkgconfig 0.29 m4_ifndef([PKG_CHECK_VAR], [AC_DEFUN([PKG_CHECK_VAR], [AC_REQUIRE([PKG_PROG_PKG_CONFIG])dnl AC_ARG_VAR([$1], [value of $3 for $2, overriding pkg-config])dnl _PKG_CONFIG([$1], [variable="][$3]["], [$2]) AS_VAR_COPY([$1], [pkg_cv_][$1]) AS_VAR_IF([$1], [""], [$5], [$4])dnl ])dnl PKG_CHECK_VAR ]) AM_INIT_AUTOMAKE([foreign -Wall]) AM_SILENT_RULES([yes]) AC_REQUIRE_AUX_FILE([tap-driver.sh]) AC_ARG_VAR(PANDOC, [pandoc path]) AC_PATH_PROG(PANDOC, [pandoc], []) if test -z "$PANDOC" then AC_MSG_WARN([pandoc not found, man pages rebuild will not be possible]) fi AM_CONDITIONAL([USE_PANDOC], [test -n "$PANDOC"]) PKG_CHECK_VAR(bashcompdir, [bash-completion], [completionsdir], , bashcompdir="${sysconfdir}/bash_completion.d") AC_SUBST(bashcompdir) AC_CONFIG_FILES([Makefile tests/jo.07.sh]) AC_OUTPUT echo " Jo.............: version $PACKAGE_VERSION Prefix.........: $prefix C compiler.....: $CC $CFLAGS $CPPFLAGS Pandoc.........: ${PANDOC:-NONE} Bash completion: $bashcompdir/jo.bash Now type 'make @<:@@:>@' where the optional is: all - build all binaries check - run the tests install - install everything " jo-1.4/debian/000077500000000000000000000000001370462052600132155ustar00rootroot00000000000000jo-1.4/debian/changelog000066400000000000000000000001761370462052600150730ustar00rootroot00000000000000jo (1.0) trusty; urgency=low * Initial packaging for jo -- Ross Duggan Sat, 12 Mar 2016 18:30:00 +0100 jo-1.4/debian/compat000066400000000000000000000000021370462052600144130ustar00rootroot000000000000008 jo-1.4/debian/control000066400000000000000000000004241370462052600146200ustar00rootroot00000000000000Source: jo Section: devel Priority: optional Maintainer: Jan-Piet Mens Build-Depends: debhelper (>= 8.0.0), dh-autoreconf, libtool, autoconf (>= 2.69), make Standards-Version: 3.9.3 Package: jo Architecture: any Description: A shell command to create JSON jo-1.4/debian/copyright000066400000000000000000000015351370462052600151540ustar00rootroot00000000000000Format: http://dep.debian.net/deps/dep5 Upstream-Name: jo Source: https://github.com/jpmens/jo Files: * Copyright: Jan-Piet Mens License: GPLv2 This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. . This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. . You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. jo-1.4/debian/dirs000066400000000000000000000000331370462052600140750ustar00rootroot00000000000000usr/bin usr/share/man/man1 jo-1.4/debian/docs000066400000000000000000000000001370462052600140560ustar00rootroot00000000000000jo-1.4/debian/jo.manpages000066400000000000000000000000051370462052600153350ustar00rootroot00000000000000jo.1 jo-1.4/debian/rules000077500000000000000000000007141370462052600142770ustar00rootroot00000000000000#!/usr/bin/make -f # -*- makefile -*- # Sample debian/rules that uses debhelper. # This file was originally written by Joey Hess and Craig Small. # As a special exception, when this file is copied by dh-make into a # dh-make output file, you may use that output file without restriction. # This special exception was added by Craig Small in version 0.37 of dh-make. # Uncomment this to turn on verbose mode. #export DH_VERBOSE=1 %: dh $@ --with autoreconf jo-1.4/debian/source/000077500000000000000000000000001370462052600145155ustar00rootroot00000000000000jo-1.4/debian/source/format000066400000000000000000000000141370462052600157230ustar00rootroot000000000000003.0 (quilt) jo-1.4/jo.1000066400000000000000000000250561370462052600124750ustar00rootroot00000000000000.\"t .\" Automatically generated by Pandoc 2.5 .\" .TH "JO" "1" "" "User Manuals" "" .hy .SH NAME .PP jo \- JSON output from a shell .SH SYNOPSIS .PP jo [\-p] [\-a] [\-B] [\-e] [\-v] [\-V] [\-d keydelim] [\[en]] [ [\-s|\-n|\-b] word \&...] .SH DESCRIPTION .PP \f[I]jo\f[R] creates a JSON string on \f[I]stdout\f[R] from _word_s given it as arguments or read from \f[I]stdin\f[R]. Without option \f[C]\-a\f[R] it generates an object whereby each \f[I]word\f[R] is a \f[C]key=value\f[R] (or \f[C]key\[at]value\f[R]) pair with \f[I]key\f[R] being the JSON object element and \f[I]value\f[R] its value. \f[I]jo\f[R] attempts to guess the type of \f[I]value\f[R] in order to create number (using \f[I]strtod(3)\f[R]), string, or null values in JSON. .PP \f[I]jo\f[R] normally treats \f[I]key\f[R] as a literal string value. If the \f[C]\-d\f[R] option is specified, \f[I]key\f[R] will be interpreted as an \f[I]object path\f[R], whose individual components are separated by the first character of \f[I]keydelim\f[R]. .PP \f[I]jo\f[R] normally treats \f[I]value\f[R] as a literal string value, unless it begins with one of the following characters: .PP .TS tab(@); l l. T{ value T}@T{ action T} _ T{ \[at]file T}@T{ substitute the contents of \f[I]file\f[R] as\-is T} T{ %file T}@T{ substitute the contents of \f[I]file\f[R] in base64\-encoded form T} T{ :file T}@T{ interpret the contents of \f[I]file\f[R] as JSON, and substitute the result T} .TE .PP Escape the special character with a backslash to prevent this interpretation. .PP \f[I]jo\f[R] treats \f[C]key\[at]value\f[R] specifically as boolean JSON elements: if the value begins with \f[C]T\f[R], \f[C]t\f[R], or the numeric value is greater than zero, the result is \f[C]true\f[R], else \f[C]false\f[R]. A missing or empty value behind the colon results in a \f[C]null\f[R] JSON element. .PP \f[I]jo\f[R] creates an array instead of an object when \f[C]\-a\f[R] is specified. .PP When the \f[C]:=\f[R] operator is used in a \f[I]word\f[R], the name to the right of \f[C]:=\f[R] is a file containing JSON which is parsed and assigned to the key left of the operator. The file may be specified as \f[C]\-\f[R] to read from \f[I]jo\f[R]\[cq]s standard input. .SH TYPE COERCION .PP \f[I]jo\f[R]\[cq]s type guesses can be overridden on a per\-word basis by prefixing \f[I]word\f[R] with \f[C]\-s\f[R] for \f[I]string\f[R], \f[C]\-n\f[R] for \f[I]number\f[R], or \f[C]\-b\f[R] for \f[I]boolean\f[R]. The list of _word_s \f[I]must\f[R] be prefixed with \f[C]\-\-\f[R], to indicate to \f[I]jo\f[R] that there are no more global options. .PP Type coercion works as follows: .PP .TS tab(@); l l l l l. T{ word T}@T{ \-s T}@T{ \-n T}@T{ \-b T}@T{ default T} _ T{ a= T}@T{ \[lq]a\[rq]:\[dq]\[dq] T}@T{ \[lq]a\[rq]:0 T}@T{ \[lq]a\[rq]:false T}@T{ \[lq]a\[rq]:null T} T{ a=string T}@T{ \[lq]a\[rq]:\[lq]string\[rq] T}@T{ \[lq]a\[rq]:6 T}@T{ \[lq]a\[rq]:true T}@T{ \[lq]a\[rq]:\[lq]string\[rq] T} T{ a=\[dq]quoted\[dq] T}@T{ \[lq]a\[rq]:\[lq]\[dq]quoted\[dq]\[rq] T}@T{ \[lq]a\[rq]:8 T}@T{ \[lq]a\[rq]:true T}@T{ \[lq]a\[rq]:\[lq]\[dq]quoted\[dq]\[rq] T} T{ a=12345 T}@T{ \[lq]a\[rq]:\[lq]12345\[rq] T}@T{ \[lq]a\[rq]:12345 T}@T{ \[lq]a\[rq]:true T}@T{ \[lq]a\[rq]:12345 T} T{ a=true T}@T{ \[lq]a\[rq]:\[lq]true\[rq] T}@T{ \[lq]a\[rq]:1 T}@T{ \[lq]a\[rq]:true T}@T{ \[lq]a\[rq]:true T} T{ a=false T}@T{ \[lq]a\[rq]:\[lq]false\[rq] T}@T{ \[lq]a\[rq]:0 T}@T{ \[lq]a\[rq]:false T}@T{ \[lq]a\[rq]:false T} T{ a=null T}@T{ \[lq]a\[rq]:\[dq]\[dq] T}@T{ \[lq]a\[rq]:0 T}@T{ \[lq]a\[rq]:false T}@T{ \[lq]a\[rq]:null T} .TE .PP Coercing a non\-number string to number outputs the \f[I]length\f[R] of the string. .PP Coercing a non\-boolean string to boolean outputs \f[C]false\f[R] if the string is empty, \f[C]true\f[R] otherwise. .PP Type coercion only applies to \f[C]key=value\f[R] words, and individual words in a \f[C]\-a\f[R] array. Coercing other words has no effect. .SH EXAMPLES .PP Create an object. Note how the incorrectly\-formatted float value becomes a string: .IP .nf \f[C] $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name=\[dq]JP Mens\[dq] nada= coffee\[at]T {\[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} \f[R] .fi .PP Pretty\-print an array with a list of files in the current directory: .IP .nf \f[C] $ jo \-p \-a * [ \[dq]Makefile\[dq], \[dq]README.md\[dq], \[dq]jo.1\[dq], \[dq]jo.c\[dq], \[dq]jo.pandoc\[dq], \[dq]json.c\[dq], \[dq]json.h\[dq] ] \f[R] .fi .PP 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 \&... .IP .nf \f[C] $ 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 { \[dq]name\[dq]: \[dq]JP\[dq], \[dq]object\[dq]: { \[dq]fruit\[dq]: \[dq]Orange\[dq], \[dq]hungry\[dq]: false, \[dq]point\[dq]: { \[dq]x\[dq]: 10, \[dq]y\[dq]: 20, \[dq]list\[dq]: [ 1, 2, 3, 4, 5 ] }, \[dq]number\[dq]: 17 }, \[dq]sunday\[dq]: false } \f[R] .fi .PP Booleans as strings or as boolean (pay particular attention to \f[I]switch\f[R]; the \f[C]\-B\f[R] option disables the default detection of the \[lq]\f[C]true\f[R]\[rq], \[lq]\f[C]false\f[R]\[rq], and \[lq]\f[C]null\f[R]\[rq] strings): .IP .nf \f[C] $ jo switch=true morning\[at]0 {\[dq]switch\[dq]:true,\[dq]morning\[dq]:false} $ jo \-B switch=true morning\[at]0 {\[dq]switch\[dq]:\[dq]true\[dq],\[dq]morning\[dq]:false} \f[R] .fi .PP Elements (objects and arrays) can be nested. The following example nests an array called \f[I]point\f[R] and an object named \f[I]geo\f[R]: .IP .nf \f[C] $ jo \-p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 { \[dq]name\[dq]: \[dq]Jane\[dq], \[dq]point\[dq]: [ 1, 2 ], \[dq]geo\[dq]: { \[dq]lat\[dq]: 10, \[dq]lon\[dq]: 20 } } \f[R] .fi .PP The same example, using object paths: .IP .nf \f[C] $ jo \-p \-d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { \[dq]name\[dq]: \[dq]Jane\[dq], \[dq]point\[dq]: [ 1, 2 ], \[dq]geo\[dq]: { \[dq]lat\[dq]: 10, \[dq]lon\[dq]: 20 } } \f[R] .fi .PP Without \f[C]\-d\f[R], a different object is generated: .IP .nf \f[C] $ jo \-p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { \[dq]name\[dq]: \[dq]Jane\[dq], \[dq]point\[dq]: [ 1, 2 ], \[dq]geo.lat\[dq]: 10, \[dq]geo.lon\[dq]: 20 } \f[R] .fi .PP Create empty objects or arrays, intentionally or potentially: .IP .nf \f[C] $ jo < /dev/null {} $ MY_ARRAY=(a=1 b=2) $ jo \-a \[dq]${MY_ARRAY[\[at]]}\[dq] < /dev/null [\[dq]a=1\[dq],\[dq]b=2\[dq]] \f[R] .fi .PP Type coercion: .IP .nf \f[C] $ 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] { \[dq]a\[dq]: \[dq]true\[dq], \[dq]b\[dq]: true, \[dq]c\[dq]: \[dq]123\[dq], \[dq]d\[dq]: 123, \[dq]e\[dq]: true, \[dq]f\[dq]: true, \[dq]g\[dq]: 14, \[dq]h\[dq]: true } $ jo \-a \-\- \-s 123 \-n \[dq]This is a test\[dq] \-b C_Rocks 456 [\[dq]123\[dq],14,true,456] \f[R] .fi .PP Read element values from files: a value which starts with \f[C]\[at]\f[R] is read in plain whereas if it begins with a \f[C]%\f[R] it will be base64\-encoded and if it starts with \f[C]:\f[R] the contents are interpreted as JSON: .IP .nf \f[C] $ jo program=jo authors=\[at]AUTHORS {\[dq]program\[dq]:\[dq]jo\[dq],\[dq]authors\[dq]:\[dq]Jan\-Piet Mens \[dq]} $ jo filename=AUTHORS content=%AUTHORS {\[dq]filename\[dq]:\[dq]AUTHORS\[dq],\[dq]content\[dq]:\[dq]SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K\[dq]} $ jo nested=:nested.json {\[dq]nested\[dq]:{\[dq]field1\[dq]:123,\[dq]field2\[dq]:\[dq]abc\[dq]}} \f[R] .fi .PP These characters can be escaped to avoid interpretation: .IP .nf \f[C] $ jo name=\[dq]JP Mens\[dq] twitter=\[aq]\[rs]\[at]jpmens\[aq] {\[dq]name\[dq]:\[dq]JP Mens\[dq],\[dq]twitter\[dq]:\[dq]\[at]jpmens\[dq]} $ jo char=\[dq] \[dq] URIescape=\[rs]\[rs]%20 {\[dq]char\[dq]:\[dq] \[dq],\[dq]URIescape\[dq]:\[dq]%20\[dq]} $ jo action=\[dq]split window\[dq] vimcmd=\[dq]\[rs]:split\[dq] {\[dq]action\[dq]:\[dq]split window\[dq],\[dq]vimcmd\[dq]:\[dq]:split\[dq]} \f[R] .fi .PP Read element values from a file in order to overcome ARG_MAX limits during object assignment: .IP .nf \f[C] $ ls | jo \-a > child.json $ jo files:=child.json {\[dq]files\[dq]:[\[dq]AUTHORS\[dq],\[dq]COPYING\[dq],\[dq]ChangeLog\[dq] .... $ ls *.c | jo \-a > source.json; ls *.h | jo \-a > headers.json $ jo \-a :source.json :headers.json [[\[dq]base64.c\[dq],\[dq]jo.c\[dq],\[dq]json.c\[dq]],[\[dq]base64.h\[dq],\[dq]json.h\[dq]]] \f[R] .fi .SH OPTIONS .PP \f[I]jo\f[R] understands the following global options. .TP .B \-a Interpret the list of \f[I]words\f[R] as array values and produce an array instead of an object. .TP .B \-B By default \f[I]jo\f[R] interprets the strings \[lq]\f[C]true\f[R]\[rq] and \[lq]\f[C]false\f[R]\[rq] as boolean elements \f[C]true\f[R] and \f[C]false\f[R] respectively, and \[lq]\f[C]null\f[R]\[rq] as \f[C]null\f[R]. Disable with this option. .TP .B \-e Ignore empty stdin (i.e.\ don\[cq]t produce a diagnostic error when \f[I]stdin\f[R] is empty) .TP .B \-p Pretty\-print the JSON string on output instead of the terse one\-line output it prints by default. .TP .B \-v Show version and exit. .TP .B \-V Show version as a JSON object and exit. .SH BUGS .PP Probably. .PP If a value given to \f[I]jo\f[R] expands to empty in the shell, then \f[I]jo\f[R] produces a \f[C]null\f[R] in object mode, and might appear to hang in array mode; it is not hanging, rather it\[cq]s reading \f[I]stdin\f[R]. This is not a bug. .PP Numeric values are converted to numbers which can produce undesired results. If you quote a numeric value, \f[I]jo\f[R] will make it a string. Compare the following: .IP .nf \f[C] $ jo a=1.0 {\[dq]a\[dq]:1} $ jo a=\[rs]\[dq]1.0\[rs]\[dq] {\[dq]a\[dq]:\[dq]1.0\[dq]} \f[R] .fi .PP Omitting a closing bracket on a nested element causes a diagnostic message to print, but the output contains garbage anyway. This was designed thusly. .SH RETURN CODES .PP \f[I]jo\f[R] exits with a code 0 on success and non\-zero on failure after indicating what caused the failure. .SH AVAILABILITY .PP .SH CREDITS .IP \[bu] 2 This program uses \f[C]json.[ch]\f[R], by Joseph A. Adams. .SH SEE ALSO .IP \[bu] 2 .IP \[bu] 2 .IP \[bu] 2 .IP \[bu] 2 strtod(3) .SH AUTHOR .PP Jan\-Piet Mens jo-1.4/jo.bash000066400000000000000000000017661370462052600132540ustar00rootroot00000000000000# bash completion for jo(1) _jo() { # Don't split words on =, for =@ and =% handling COMP_WORDBREAKS=${COMP_WORDBREAKS//=} # No completion if an exit causing flag is around local i for i in ${!COMP_WORDS[@]}; do [[ $i -ne $COMP_CWORD && ${COMP_WORDS[i]} == -*[hvV]* ]] && return 0 done # Complete available options following a dash if [[ $2 == -* ]]; then COMPREPLY=( $(compgen -W '-a -B -h -p -v -V' -- "$2") ) return 0 fi # Complete filenames on =@ and =% if [[ $2 == *=[@%]* ]]; then local file prefix file="${2#*=[@%]}" prefix="${2:0:${#2}-${#file}}" compopt -o filenames COMPREPLY=( $(compgen -f -- "$file") ) if [[ ${#COMPREPLY[@]} -eq 1 ]]; then if [[ -d "${COMPREPLY[0]}" ]]; then COMPREPLY[0]+=/ compopt -o nospace fi COMPREPLY[0]="$prefix${COMPREPLY[0]}" fi return 0 fi } && complete -F _jo jo jo-1.4/jo.c000066400000000000000000000427071370462052600125610ustar00rootroot00000000000000#include #include #include #include #include #include #include #include #ifndef WIN32 # include #endif #include "json.h" #include "base64.h" /* * Copyright (C) 2016-2019 Jan-Piet Mens * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #define SPACER " " #define FLAG_ARRAY 0x01 #define FLAG_PRETTY 0x02 #define FLAG_NOBOOL 0x04 #define FLAG_BOOLEAN 0x08 #define FLAG_NOSTDIN 0x10 #define FLAG_MASK (FLAG_ARRAY | FLAG_PRETTY | FLAG_NOBOOL | FLAG_BOOLEAN | FLAG_NOSTDIN) /* Size of buffer blocks for pipe slurping */ #define SLURP_BLOCK_SIZE 4096 static JsonNode *pile; /* pile of nested objects/arrays */ #ifdef _WIN32 # define err(n, s) { fprintf(stderr, s); exit(n); } # define errx(n, f, a) { fprintf(stderr, f, a); exit(n); } # define fseeko fseek # define ftello ftell #endif #define TAG_TO_FLAGS(tag) ((FLAG_MASK + 1) * (tag)) #define TAG_FLAG_BOOL (TAG_TO_FLAGS(JSON_BOOL)) #define TAG_FLAG_STRING (TAG_TO_FLAGS(JSON_STRING)) #define TAG_FLAG_NUMBER (TAG_TO_FLAGS(JSON_NUMBER)) #define COERCE_MASK (TAG_FLAG_BOOL | TAG_FLAG_STRING | TAG_FLAG_NUMBER) JsonTag flags_to_tag(int flags) { return flags / (FLAG_MASK + 1); } void json_copy_to_object(JsonNode * obj, JsonNode * object_or_array, int clobber) { JsonNode *node, *node_child, *obj_child; if (obj->tag != JSON_OBJECT && obj->tag != JSON_ARRAY) return; json_foreach(node, object_or_array) { if (!clobber & (json_find_member(obj, node->key) != NULL)) continue; /* Don't clobber existing keys */ if (obj->tag == JSON_OBJECT) { if (node->tag == JSON_STRING) json_append_member(obj, node->key, json_mkstring(node->string_)); else if (node->tag == JSON_NUMBER) json_append_member(obj, node->key, json_mknumber(node->number_)); else if (node->tag == JSON_BOOL) json_append_member(obj, node->key, json_mkbool(node->bool_)); else if (node->tag == JSON_NULL) json_append_member(obj, node->key, json_mknull()); else if (node->tag == JSON_OBJECT) { /* Deep-copy existing object to new object */ json_append_member(obj, node->key, (obj_child = json_mkobject())); json_foreach(node_child, node) { json_copy_to_object(obj_child, node_child, clobber); } } else fprintf(stderr, "PANIC: unhandled JSON type %d\n", node->tag); } else if (obj->tag == JSON_ARRAY) { if (node->tag == JSON_STRING) json_append_element(obj, json_mkstring(node->string_)); if (node->tag == JSON_NUMBER) json_append_element(obj, json_mknumber(node->number_)); if (node->tag == JSON_BOOL) json_append_element(obj, json_mkbool(node->bool_)); if (node->tag == JSON_NULL) json_append_element(obj, json_mknull()); } } } const char *maybe_stdin(const char* filename) { return strcmp(filename, "-") ? filename : "/dev/fd/0"; } char *slurp_file(const char* filename, size_t *out_len, bool fold_newlines) { char *buf; int i = 0; int ch; off_t buffer_len; FILE *fp; if ((fp = fopen(maybe_stdin(filename), "r")) == NULL) { perror(filename); errx(1, "Cannot open %s for reading", filename); } if (fseeko(fp, 0, SEEK_END) != 0) { /* If we cannot seek, we're operating off a pipe and need to dynamically grow the buffer that we're reading into */ buffer_len = SLURP_BLOCK_SIZE; } else { buffer_len = ftello(fp) + 1; fseeko(fp, 0, SEEK_SET); } if ((buf = malloc(buffer_len)) == NULL) { errx(1, "File %s is too large to be read into memory", filename); } while ((ch = fgetc(fp)) != EOF) { if (i == (buffer_len - 1)) { buffer_len += SLURP_BLOCK_SIZE; if ((buf = realloc(buf, buffer_len)) == NULL) { errx(1, "File %s is too large to be read into memory", filename); } } if (ch != '\n' || !fold_newlines) { buf[i++] = ch; } } fclose(fp); buf[i] = 0; *out_len = i; return (buf); } JsonNode *jo_mknull(JsonTag type) { switch (type) { case JSON_STRING: return json_mkstring(""); break; case JSON_NUMBER: return json_mknumber(0); break; case JSON_BOOL: return json_mkbool(false); break; default: return json_mknull(); break; } } JsonNode *jo_mkbool(bool b, JsonTag type) { switch (type) { case JSON_STRING: return json_mkstring(b ? "true" : "false"); break; case JSON_NUMBER: return json_mknumber(b ? 1 : 0); break; default: return json_mkbool(b); break; } } JsonNode *jo_mkstring(char *str, JsonTag type) { switch (type) { case JSON_NUMBER: /* Length of string */ return json_mknumber(strlen(str)); break; case JSON_BOOL: /* True if not empty */ return json_mkbool(strlen(str) > 0); break; default: return json_mkstring(str); break; } } JsonNode *jo_mknumber(char *str, JsonTag type) { /* ASSUMPTION: str already tested as valid number */ double n = strtod(str, NULL); switch (type) { case JSON_STRING: /* Just return the original representation */ return json_mkstring(str); break; case JSON_BOOL: return json_mkbool(n != 0); break; default: /* ASSUMPTION: str already tested as valid number */ return json_mknumber(n); break; } } /* * Attempt to "sniff" the type of data in `str' and return * a JsonNode of the correct JSON type. */ JsonNode *vnode(char *str, int flags) { JsonTag type = flags_to_tag(flags); if (strlen(str) == 0) { return jo_mknull(type); } /* If str begins with a double quote, keep it a string */ if (*str == '"') { #if 0 char *bp = str + strlen(str) - 1; if (bp > str && *bp == '"') *bp = 0; /* Chop closing double quote */ return json_mkstring(str + 1); #endif return jo_mkstring(str, type); } char *endptr; double num = strtod(str, &endptr); if (!*endptr && isfinite(num)) { return jo_mknumber(str, type); } if (!(flags & FLAG_NOBOOL)) { if (strcmp(str, "true") == 0) { return jo_mkbool(true, type); } else if (strcmp(str, "false") == 0) { return jo_mkbool(false, type); } else if (strcmp(str, "null") == 0) { return jo_mknull(type); } } if (*str == '\\') { ++str; } else { if (*str == '@' || *str == '%' || *str == ':') { char *filename = str + 1, *content; bool binmode = (*str == '%'); bool jsonmode = (*str == ':'); size_t len = 0; JsonNode *j = NULL; if ((content = slurp_file(filename, &len, false)) == NULL) { errx(1, "Error reading file %s", filename); } if (binmode) { char *encoded; if ((encoded = base64_encode(content, len)) == NULL) { errx(1, "Cannot base64-encode file %s", filename); } j = json_mkstring(encoded); free(encoded); } else if (jsonmode) { j = json_decode(content); if (j == NULL) { errx(1, "Cannot decode JSON in file %s", filename); } } // If it got this far without valid JSON, just consider it a string if (j == NULL) { char *bp = content + strlen(content) - 1; if (*bp == '\n') *bp-- = 0; if (*bp == '\r') *bp = 0; j = json_mkstring(content); } free(content); return (j); } } if (*str == '{' || *str == '[') { if (type == JSON_STRING) { return json_mkstring(str); } JsonNode *obj = json_decode(str); if (obj == NULL) { /* JSON cannot be decoded; return the string */ // fprintf(stderr, "Cannot decode JSON from %s\n", str); obj = json_mkstring(str); } return (obj); } return jo_mkstring(str, type); } /* * Attempt to sniff `str' into a boolean; return a * corresponding JsonNode for it. */ JsonNode *boolnode(char *str) { if (strlen(str) == 0) { return json_mknull(); } if (tolower((unsigned char) *str) == 't') { return json_mkbool(1); } return json_mkbool(atoi(str)); } int usage(char *prog) { fprintf(stderr, "Usage: %s [-a] [-B] [-d keydelim] [-p] [-e] [-v] [-V] [--] [-s|-n|-b] [word...]\n", prog); fprintf(stderr, "\tword is key=value or key@value\n"); fprintf(stderr, "\t-a creates an array of words\n"); fprintf(stderr, "\t-B disable boolean true/false\n"); fprintf(stderr, "\t-d key will be object path separated by keydelim\n"); fprintf(stderr, "\t-p pretty-prints JSON on output\n"); fprintf(stderr, "\t-e if stdin is empty do not wait for input quit\n"); fprintf(stderr, "\t-s coerce type guessing to string\n"); fprintf(stderr, "\t-b coerce type guessing to bool\n"); fprintf(stderr, "\t-n coerce type guessing to number\n"); fprintf(stderr, "\t-v show version\n"); fprintf(stderr, "\t-V show version in JSON\n"); return (-1); } /* * Check whether we're being given nested arrays or objects. * `kv' contains the "key" such as "number" or "point[]" or * "geo[lat]". `value' the actual value for that element. * * Returns true if nesting is completely handled, otherwise: * *keyp -> remaining key for caller to insert "value" * *baseop -> object node in which caller should insert "value" */ bool resolve_nested(int flags, char **keyp, char key_delim, char *value, JsonNode **baseop) { char *member = NULL, *bo, *bc, *so; /* bracket open, close, sub-object */ JsonNode *op; int found = false; if (key_delim) { /* First construct nested object */ while ((so = strchr(*keyp, key_delim)) != NULL) { *so = 0; if ((op = json_find_member(*baseop, *keyp)) == NULL) { /* Add a nested object node */ op = json_mkobject(); json_append_member(*baseop, *keyp, op); } *baseop = op; *keyp = so + 1; } } /* Now check for trailing geo[] or geo[lat] */ if ((bo = strchr(*keyp, '[')) != NULL) { if (*(bo+1) == ']') { *bo = 0; } else if ((bc = strchr(bo + 1, ']')) == NULL) { fprintf(stderr, "missing closing bracket on %s\n", *keyp); return (false); } else { *bo = *bc = 0; member = bo + 1; } /* * *keyp is now `geo' for both `geo[]` and `geo[lat]` * member is null for the former and "lat" for the latter. * Find an existing object in *baseop for this member name * or create a new one if we don't have it. */ if ((op = json_find_member(*baseop, *keyp)) != NULL) { found = true; } else { op = (member == NULL) ? json_mkarray() : json_mkobject(); } if (member == NULL) { /* we're doing an array */ if (flags & FLAG_BOOLEAN) { json_append_element(op, boolnode(value)); } else { json_append_element(op, vnode(value, flags)); } } else { /* we're doing an object */ if (flags & FLAG_BOOLEAN) { json_append_member(op, member, boolnode(value)); } else { json_append_member(op, member, vnode(value, flags)); } } if (!found) { json_append_member(*baseop, *keyp, op); } return (true); } return (false); } int member_to_object(JsonNode *object, int flags, char key_delim, char *kv) { /* we expect key=value or key:value (boolean on last) */ char *p = strchr(kv, '='); char *q = strchr(kv, '@'); char *r = strchr(kv, ':'); if ((r && *(r+1) == '=') && !q) { char *filename = p + 1; char *content; size_t len; if ((content = slurp_file(filename, &len, false)) == NULL) { errx(1, "Error reading file %s", filename); } JsonNode *o = json_decode(content); free(content); if (o == NULL) { errx(1, "Cannot decode JSON in file %s", filename); } *r = 0; /* Chop at ":=" */ json_append_member(object, kv, o); return (0); } if (!p && !q && !r) { return (-1); } if (p) { if (p) { *p = 0; if (!resolve_nested(flags, &kv, key_delim, p+1, &object)) json_append_member(object, kv, vnode(p+1, flags)); } } else { if (q) { *q = 0; if (!resolve_nested(flags | FLAG_BOOLEAN, &kv, key_delim, q+1, &object)) json_append_member(object, kv, boolnode(q+1)); } } return (0); } /* * Append kv to the array or object. */ void append_kv(JsonNode *object_or_array, int flags, char key_delim, char *kv) { if (flags & FLAG_ARRAY) { json_append_element(object_or_array, vnode(kv, flags)); } else { if (member_to_object(object_or_array, flags, key_delim, kv) == -1) { fprintf(stderr, "Argument `%s' is neither k=v nor k@v\n", kv); } } } #ifdef _WIN32 #include char* utf8_from_locale(const char *str, size_t len) { wchar_t* wcsp; char* mbsp; size_t mbssize, wcssize; if (len == 0) { return strdup(""); } if (len == -1) { len = strlen(str); } wcssize = MultiByteToWideChar(GetACP(), 0, str, len, NULL, 0); wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t)); if (!wcsp) { return NULL; } wcssize = MultiByteToWideChar(GetACP(), 0, str, len, wcsp, wcssize + 1); wcsp[wcssize] = 0; mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL); mbsp = (char*) malloc((mbssize + 1)); if (!mbsp) { free(wcsp); return NULL; } mbssize = WideCharToMultiByte(CP_UTF8, 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL); mbsp[mbssize] = 0; free(wcsp); return mbsp; } # define utf8_free(p) free(p) char* locale_from_utf8(const char *utf8, size_t len) { wchar_t* wcsp; char* mbsp; size_t mbssize, wcssize; if (len == 0) { return strdup(""); } if (len == -1) { len = strlen(utf8); } wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len, NULL, 0); wcsp = (wchar_t*) malloc((wcssize + 1) * sizeof(wchar_t)); if (!wcsp) { return NULL; } wcssize = MultiByteToWideChar(CP_UTF8, 0, utf8, len, wcsp, wcssize + 1); wcsp[wcssize] = 0; mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, NULL, 0, NULL, NULL); mbsp = (char*) malloc((mbssize + 1)); if (!mbsp) { free(wcsp); return NULL; } mbssize = WideCharToMultiByte(GetACP(), 0, (LPCWSTR) wcsp, -1, mbsp, mbssize, NULL, NULL); mbsp[mbssize] = 0; free(wcsp); return mbsp; } # define locale_free(p) free(p) #else # define utf8_from_locale(p, l) (p) # define utf8_free(p) # define locale_from_utf8(p, l) (p) # define locale_free(p) #endif char *stringify(JsonNode *json, int flags) { int pretty = flags & FLAG_PRETTY; return json_stringify(json, (pretty) ? SPACER : NULL); } int version(int flags) { JsonNode *json = json_mkobject(); char *js; json_append_member(json, "program", json_mkstring("jo")); json_append_member(json, "author", json_mkstring("Jan-Piet Mens")); json_append_member(json, "repo", json_mkstring("https://github.com/jpmens/jo")); json_append_member(json, "version", json_mkstring(PACKAGE_VERSION)); if ((js = stringify(json, flags)) != NULL) { printf("%s\n", js); free(js); } json_delete(json); return (0); } int main(int argc, char **argv) { int c, key_delim = 0; bool showversion = false; char *kv, *js_string, *progname, buf[BUFSIZ], *p; int ttyin = isatty(fileno(stdin)), ttyout = isatty(fileno(stdout)); int flags = 0; JsonNode *json, *op; #if HAVE_PLEDGE if (pledge("stdio rpath", NULL) == -1) { err(1, "pledge"); } #endif progname = (progname = strrchr(*argv, '/')) ? progname + 1 : *argv; while ((c = getopt(argc, argv, "aBd:hpevV")) != EOF) { switch (c) { case 'a': flags |= FLAG_ARRAY; break; case 'B': flags |= FLAG_NOBOOL; break; case 'd': key_delim = optarg[0]; break; case 'h': usage(progname); return (0); case 'p': flags |= FLAG_PRETTY; break; case 'e': flags |= FLAG_NOSTDIN; break; case 'v': printf("jo %s\n", PACKAGE_VERSION); exit(0); case 'V': showversion = true; break; default: exit(usage(progname)); } } if (showversion) { return(version(flags)); } argc -= optind; argv += optind; pile = json_mkobject(); json = (flags & FLAG_ARRAY) ? json_mkarray() : json_mkobject(); if (argc == 0) { if (flags & FLAG_NOSTDIN) { return(0); } while (fgets(buf, sizeof(buf), stdin) != NULL) { if (buf[strlen(buf) - 1] == '\n') buf[strlen(buf) - 1] = 0; p = ttyin ? utf8_from_locale(buf, -1) : buf; append_kv(json, flags, key_delim, p); if (ttyin) utf8_free(p); } } else { while ((kv = *argv++)) { if (kv[0] == '-' && !(flags & COERCE_MASK)) { /* Set one-shot coerce flag */ switch (kv[1]) { case 'b': flags |= TAG_FLAG_BOOL; break; case 's': flags |= TAG_FLAG_STRING; break; case 'n': flags |= TAG_FLAG_NUMBER; break; default: /* Treat as normal input */ p = utf8_from_locale(kv, -1); append_kv(json, flags, key_delim, p); utf8_free(p); /* Reset any one-shot coerce flags */ flags &= ~(COERCE_MASK); } } else { p = utf8_from_locale(kv, -1); append_kv(json, flags, key_delim, p); utf8_free(p); /* Reset any one-shot coerce flags */ flags &= ~(COERCE_MASK); } } } /* * See if we have any nested objects or arrays in the pile, * and copy these into our main object if so. */ json_foreach(op, pile) { JsonNode *o; if (op->tag == JSON_ARRAY) { o = json_mkarray(); } else if (op->tag == JSON_OBJECT) { o = json_mkobject(); } else { continue; } json_copy_to_object(o, op, 0); json_append_member(json, op->key, o); } if ((js_string = stringify(json, flags)) == NULL) { fprintf(stderr, "Invalid JSON\n"); exit(2); } p = ttyout ? locale_from_utf8(js_string, -1) : js_string; printf("%s\n", p); if (ttyout) locale_free(p); free(js_string); json_delete(json); json_delete(pile); return (0); } jo-1.4/jo.md000066400000000000000000000211701370462052600127260ustar00rootroot00000000000000--- title: 'JO(1) User Manuals' --- NAME ==== jo - JSON output from a shell SYNOPSIS ======== jo \[-p\] \[-a\] \[-B\] \[-e\] \[-v\] \[-V\] \[-d keydelim\] \[--\] \[ \[-s\|-n\|-b\] word ...\] DESCRIPTION =========== *jo* creates a JSON string on *stdout* from \_word\_s given it as arguments or read from *stdin*. Without option `-a` it generates an object whereby each *word* is a `key=value` (or `key@value`) pair with *key* being the JSON object element and *value* its value. *jo* attempts to guess the type of *value* in order to create number (using *strtod(3)*), string, or null values in JSON. *jo* normally treats *key* as a literal string value. If the `-d` option is specified, *key* will be interpreted as an *object path*, whose individual components are separated by the first character of *keydelim*. *jo* normally treats *value* as a literal string value, unless it begins with one of the following characters: value action -------- --------------------------------------------------------------------- @file substitute the contents of *file* as-is \%file substitute the contents of *file* in base64-encoded form :file interpret the contents of *file* as JSON, and substitute the result Escape the special character with a backslash to prevent this interpretation. *jo* treats `key@value` specifically as boolean JSON elements: if the value begins with `T`, `t`, or the numeric value is greater than zero, the result is `true`, else `false`. A missing or empty value behind the colon results in a `null` JSON element. *jo* creates an array instead of an object when `-a` is specified. 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. TYPE COERCION ============= *jo*'s type guesses can be overridden on a per-word basis by prefixing *word* with `-s` for *string*, `-n` for *number*, or `-b` for *boolean*. The list of \_word\_s *must* be prefixed with `--`, to indicate to *jo* that there are no more global options. Type coercion works as follows: word -s -n -b default -------------- ------------------ ----------- ----------- ------------------ a= "a":\"\" "a":0 "a":false "a":null a=string "a":"string" "a":6 "a":true "a":"string" a=\"quoted\" "a":"\"quoted\"" "a":8 "a":true "a":"\"quoted\"" a=12345 "a":"12345" "a":12345 "a":true "a":12345 a=true "a":"true" "a":1 "a":true "a":true a=false "a":"false" "a":0 "a":false "a":false a=null "a":\"\" "a":0 "a":false "a":null Coercing a non-number string to number outputs the *length* of the string. Coercing a non-boolean string to boolean outputs `false` if the string is empty, `true` otherwise. Type coercion only applies to `key=value` words, and individual words in a `-a` array. Coercing other words has no effect. EXAMPLES ======== Create an object. Note how the incorrectly-formatted float value becomes a string: $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name="JP Mens" nada= coffee@T {"tst":1457081292,"lat":12.3456,"cc":"FR","badfloat":"3.14159.26","name":"JP Mens","nada":null,"coffee":true} Pretty-print an array with a list of files in the current directory: $ jo -p -a * [ "Makefile", "README.md", "jo.1", "jo.c", "jo.pandoc", "json.c", "json.h" ] 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 ... $ 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 { "name": "JP", "object": { "fruit": "Orange", "hungry": false, "point": { "x": 10, "y": 20, "list": [ 1, 2, 3, 4, 5 ] }, "number": 17 }, "sunday": false } 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): $ jo switch=true morning@0 {"switch":true,"morning":false} $ jo -B switch=true morning@0 {"switch":"true","morning":false} Elements (objects and arrays) can be nested. The following example nests an array called *point* and an object named *geo*: $ jo -p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 { "name": "Jane", "point": [ 1, 2 ], "geo": { "lat": 10, "lon": 20 } } The same example, using object paths: $ jo -p -d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { "name": "Jane", "point": [ 1, 2 ], "geo": { "lat": 10, "lon": 20 } } Without `-d`, a different object is generated: $ jo -p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { "name": "Jane", "point": [ 1, 2 ], "geo.lat": 10, "geo.lon": 20 } Create empty objects or arrays, intentionally or potentially: $ jo < /dev/null {} $ MY_ARRAY=(a=1 b=2) $ jo -a "${MY_ARRAY[@]}" < /dev/null ["a=1","b=2"] Type coercion: $ 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" { "a": "true", "b": true, "c": "123", "d": 123, "e": true, "f": true, "g": 14, "h": true } $ jo -a -- -s 123 -n "This is a test" -b C_Rocks 456 ["123",14,true,456] 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: $ jo program=jo authors=@AUTHORS {"program":"jo","authors":"Jan-Piet Mens "} $ jo filename=AUTHORS content=%AUTHORS {"filename":"AUTHORS","content":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} $ jo nested=:nested.json {"nested":{"field1":123,"field2":"abc"}} These characters can be escaped to avoid interpretation: $ jo name="JP Mens" twitter='\@jpmens' {"name":"JP Mens","twitter":"@jpmens"} $ jo char=" " URIescape=\\%20 {"char":" ","URIescape":"%20"} $ jo action="split window" vimcmd="\:split" {"action":"split window","vimcmd":":split"} Read element values from a file in order to overcome ARG\_MAX limits during object assignment: $ ls | jo -a > child.json $ jo files:=child.json {"files":["AUTHORS","COPYING","ChangeLog" .... $ ls *.c | jo -a > source.json; ls *.h | jo -a > headers.json $ jo -a :source.json :headers.json [["base64.c","jo.c","json.c"],["base64.h","json.h"]] OPTIONS ======= *jo* understands the following global options. -a : Interpret the list of *words* as array values and produce an array instead of an object. -B : By default *jo* interprets the strings "`true`" and "`false`" as boolean elements `true` and `false` respectively, and "`null`" as `null`. Disable with this option. -e : Ignore empty stdin (i.e. don't produce a diagnostic error when *stdin* is empty) -p : Pretty-print the JSON string on output instead of the terse one-line output it prints by default. -v : Show version and exit. -V : Show version as a JSON object and exit. BUGS ==== Probably. 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. 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: $ jo a=1.0 {"a":1} $ jo a=\"1.0\" {"a":"1.0"} Omitting a closing bracket on a nested element causes a diagnostic message to print, but the output contains garbage anyway. This was designed thusly. RETURN CODES ============ *jo* exits with a code 0 on success and non-zero on failure after indicating what caused the failure. AVAILABILITY ============ CREDITS ======= - This program uses `json.[ch]`, by Joseph A. Adams. SEE ALSO ======== - - - - strtod(3) AUTHOR ====== Jan-Piet Mens jo-1.4/jo.pandoc000066400000000000000000000200051370462052600135660ustar00rootroot00000000000000% JO(1) User Manuals # NAME jo - JSON output from a shell # SYNOPSIS jo [-p] [-a] [-B] [-e] [-v] [-V] [-d keydelim] [--] [ [-s|-n|-b] word ...] # DESCRIPTION *jo* creates a JSON string on _stdout_ from _word_s given it as arguments or read from _stdin_. Without option `-a` it generates an object whereby each _word_ is a `key=value` (or `key@value`) pair with _key_ being the JSON object element and _value_ its value. *jo* attempts to guess the type of _value_ in order to create number (using _strtod(3)_), string, or null values in JSON. *jo* normally treats _key_ as a literal string value. If the `-d` option is specified, _key_ will be interpreted as an _object path_, whose individual components are separated by the first character of _keydelim_. *jo* normally treats _value_ as a literal string value, unless it begins with one of the following characters: value action ----- ------ @file substitute the contents of _file_ as-is %file substitute the contents of _file_ in base64-encoded form :file interpret the contents of _file_ as JSON, and substitute the result Escape the special character with a backslash to prevent this interpretation. *jo* treats `key@value` specifically as boolean JSON elements: if the value begins with `T`, `t`, or the numeric value is greater than zero, the result is `true`, else `false`. A missing or empty value behind the colon results in a `null` JSON element. *jo* creates an array instead of an object when `-a` is specified. 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. # TYPE COERCION *jo*'s type guesses can be overridden on a per-word basis by prefixing _word_ with `-s` for _string_, `-n` for _number_, or `-b` for _boolean_. The list of _word_s *must* be prefixed with `--`, to indicate to *jo* that there are no more global options. Type coercion works as follows: word -s -n -b default ------------ ---------------- ------------ --------- ---------------- a= "a":"" "a":0 "a":false "a":null a=string "a":"string" "a":6 "a":true "a":"string" a=\"quoted\" "a":"\"quoted\"" "a":8 "a":true "a":"\"quoted\"" a=12345 "a":"12345" "a":12345 "a":true "a":12345 a=true "a":"true" "a":1 "a":true "a":true a=false "a":"false" "a":0 "a":false "a":false a=null "a":"" "a":0 "a":false "a":null Coercing a non-number string to number outputs the _length_ of the string. Coercing a non-boolean string to boolean outputs `false` if the string is empty, `true` otherwise. Type coercion only applies to `key=value` words, and individual words in a `-a` array. Coercing other words has no effect. # EXAMPLES Create an object. Note how the incorrectly-formatted float value becomes a string: $ jo tst=1457081292 lat=12.3456 cc=FR badfloat=3.14159.26 name="JP Mens" nada= coffee@T {"tst":1457081292,"lat":12.3456,"cc":"FR","badfloat":"3.14159.26","name":"JP Mens","nada":null,"coffee":true} Pretty-print an array with a list of files in the current directory: $ jo -p -a * [ "Makefile", "README.md", "jo.1", "jo.c", "jo.pandoc", "json.c", "json.h" ] 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 ... $ 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 { "name": "JP", "object": { "fruit": "Orange", "hungry": false, "point": { "x": 10, "y": 20, "list": [ 1, 2, 3, 4, 5 ] }, "number": 17 }, "sunday": false } 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): $ jo switch=true morning@0 {"switch":true,"morning":false} $ jo -B switch=true morning@0 {"switch":"true","morning":false} Elements (objects and arrays) can be nested. The following example nests an array called _point_ and an object named _geo_: $ jo -p name=Jane point[]=1 point[]=2 geo[lat]=10 geo[lon]=20 { "name": "Jane", "point": [ 1, 2 ], "geo": { "lat": 10, "lon": 20 } } The same example, using object paths: $ jo -p -d. name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { "name": "Jane", "point": [ 1, 2 ], "geo": { "lat": 10, "lon": 20 } } Without `-d`, a different object is generated: $ jo -p name=Jane point[]=1 point[]=2 geo.lat=10 geo.lon=20 { "name": "Jane", "point": [ 1, 2 ], "geo.lat": 10, "geo.lon": 20 } Create empty objects or arrays, intentionally or potentially: $ jo < /dev/null {} $ MY_ARRAY=(a=1 b=2) $ jo -a "${MY_ARRAY[@]}" < /dev/null ["a=1","b=2"] Type coercion: $ 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" { "a": "true", "b": true, "c": "123", "d": 123, "e": true, "f": true, "g": 14, "h": true } $ jo -a -- -s 123 -n "This is a test" -b C_Rocks 456 ["123",14,true,456] 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: $ jo program=jo authors=@AUTHORS {"program":"jo","authors":"Jan-Piet Mens "} $ jo filename=AUTHORS content=%AUTHORS {"filename":"AUTHORS","content":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} $ jo nested=:nested.json {"nested":{"field1":123,"field2":"abc"}} These characters can be escaped to avoid interpretation: $ jo name="JP Mens" twitter='\@jpmens' {"name":"JP Mens","twitter":"@jpmens"} $ jo char=" " URIescape=\\%20 {"char":" ","URIescape":"%20"} $ jo action="split window" vimcmd="\:split" {"action":"split window","vimcmd":":split"} Read element values from a file in order to overcome ARG_MAX limits during object assignment: $ ls | jo -a > child.json $ jo files:=child.json {"files":["AUTHORS","COPYING","ChangeLog" .... $ ls *.c | jo -a > source.json; ls *.h | jo -a > headers.json $ jo -a :source.json :headers.json [["base64.c","jo.c","json.c"],["base64.h","json.h"]] # OPTIONS *jo* understands the following global options. -a : Interpret the list of _words_ as array values and produce an array instead of an object. -B : By default *jo* interprets the strings "`true`" and "`false`" as boolean elements `true` and `false` respectively, and "`null`" as `null`. Disable with this option. -e : Ignore empty stdin (i.e. don't produce a diagnostic error when *stdin* is empty) -p : Pretty-print the JSON string on output instead of the terse one-line output it prints by default. -v : Show version and exit. -V : Show version as a JSON object and exit. # BUGS Probably. 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. 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: $ jo a=1.0 {"a":1} $ jo a=\"1.0\" {"a":"1.0"} Omitting a closing bracket on a nested element causes a diagnostic message to print, but the output contains garbage anyway. This was designed thusly. # RETURN CODES *jo* exits with a code 0 on success and non-zero on failure after indicating what caused the failure. # AVAILABILITY # CREDITS * This program uses `json.[ch]`, by Joseph A. Adams. # SEE ALSO * * * * strtod(3) # AUTHOR Jan-Piet Mens jo-1.4/json.c000066400000000000000000000715121370462052600131160ustar00rootroot00000000000000/* Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #include "json.h" #include #include #include #include #include #define out_of_memory() do { \ fprintf(stderr, "Out of memory.\n"); \ exit(EXIT_FAILURE); \ } while (0) /* Sadly, strdup is not portable. */ static char *json_strdup(const char *str) { size_t n = strlen(str) + 1; char *ret = (char*) malloc(n); if (ret == NULL) out_of_memory(); #if HAVE_STRLCPY strlcpy(ret, str, n); #else strcpy(ret, str); #endif return ret; } /* String buffer */ typedef struct { char *cur; char *end; char *start; } SB; static void sb_init(SB *sb) { sb->start = (char*) malloc(17); if (sb->start == NULL) out_of_memory(); sb->cur = sb->start; sb->end = sb->start + 16; } /* sb and need may be evaluated multiple times. */ #define sb_need(sb, need) do { \ if ((sb)->end - (sb)->cur < (need)) \ sb_grow(sb, need); \ } while (0) static void sb_grow(SB *sb, int need) { size_t length = sb->cur - sb->start; size_t alloc = sb->end - sb->start; do { alloc *= 2; } while (alloc < length + need); sb->start = (char*) realloc(sb->start, alloc + 1); if (sb->start == NULL) out_of_memory(); sb->cur = sb->start + length; sb->end = sb->start + alloc; } static void sb_put(SB *sb, const char *bytes, int count) { sb_need(sb, count); memcpy(sb->cur, bytes, count); sb->cur += count; } #define sb_putc(sb, c) do { \ if ((sb)->cur >= (sb)->end) \ sb_grow(sb, 1); \ *(sb)->cur++ = (c); \ } while (0) static void sb_puts(SB *sb, const char *str) { sb_put(sb, str, strlen(str)); } static char *sb_finish(SB *sb) { *sb->cur = 0; assert(sb->start <= sb->cur && strlen(sb->start) == (size_t)(sb->cur - sb->start)); return sb->start; } static void sb_free(SB *sb) { free(sb->start); } /* * Unicode helper functions * * These are taken from the ccan/charset module and customized a bit. * Putting them here means the compiler can (choose to) inline them, * and it keeps ccan/json from having a dependency. */ /* * Type for Unicode codepoints. * We need our own because wchar_t might be 16 bits. */ typedef uint32_t js_uchar_t; /* * Validate a single UTF-8 character starting at @s. * The string must be null-terminated. * * If it's valid, return its length (1 thru 4). * If it's invalid or clipped, return 0. * * This function implements the syntax given in RFC3629, which is * the same as that given in The Unicode Standard, Version 6.0. * * It has the following properties: * * * All codepoints U+0000..U+10FFFF may be encoded, * except for U+D800..U+DFFF, which are reserved * for UTF-16 surrogate pair encoding. * * UTF-8 byte sequences longer than 4 bytes are not permitted, * as they exceed the range of Unicode. * * The sixty-six Unicode "non-characters" are permitted * (namely, U+FDD0..U+FDEF, U+xxFFFE, and U+xxFFFF). */ static int utf8_validate_cz(const char *s) { unsigned char c = *s++; if (c <= 0x7F) { /* 00..7F */ return 1; } else if (c <= 0xC1) { /* 80..C1 */ /* Disallow overlong 2-byte sequence. */ return 0; } else if (c <= 0xDF) { /* C2..DF */ /* Make sure subsequent byte is in the range 0x80..0xBF. */ if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; return 2; } else if (c <= 0xEF) { /* E0..EF */ /* Disallow overlong 3-byte sequence. */ if (c == 0xE0 && (unsigned char)*s < 0xA0) return 0; /* Disallow U+D800..U+DFFF. */ if (c == 0xED && (unsigned char)*s > 0x9F) return 0; /* Make sure subsequent bytes are in the range 0x80..0xBF. */ if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; return 3; } else if (c <= 0xF4) { /* F0..F4 */ /* Disallow overlong 4-byte sequence. */ if (c == 0xF0 && (unsigned char)*s < 0x90) return 0; /* Disallow codepoints beyond U+10FFFF. */ if (c == 0xF4 && (unsigned char)*s > 0x8F) return 0; /* Make sure subsequent bytes are in the range 0x80..0xBF. */ if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; if (((unsigned char)*s++ & 0xC0) != 0x80) return 0; return 4; } else { /* F5..FF */ return 0; } } /* Validate a null-terminated UTF-8 string. */ static bool utf8_validate(const char *s) { int len; for (; *s != 0; s += len) { len = utf8_validate_cz(s); if (len == 0) return false; } return true; } /* * Read a single UTF-8 character starting at @s, * returning the length, in bytes, of the character read. * * This function assumes input is valid UTF-8, * and that there are enough characters in front of @s. */ static int utf8_read_char(const char *s, js_uchar_t *out) { const unsigned char *c = (const unsigned char*) s; assert(utf8_validate_cz(s)); if (c[0] <= 0x7F) { /* 00..7F */ *out = c[0]; return 1; } else if (c[0] <= 0xDF) { /* C2..DF (unless input is invalid) */ *out = ((js_uchar_t)c[0] & 0x1F) << 6 | ((js_uchar_t)c[1] & 0x3F); return 2; } else if (c[0] <= 0xEF) { /* E0..EF */ *out = ((js_uchar_t)c[0] & 0xF) << 12 | ((js_uchar_t)c[1] & 0x3F) << 6 | ((js_uchar_t)c[2] & 0x3F); return 3; } else { /* F0..F4 (unless input is invalid) */ *out = ((js_uchar_t)c[0] & 0x7) << 18 | ((js_uchar_t)c[1] & 0x3F) << 12 | ((js_uchar_t)c[2] & 0x3F) << 6 | ((js_uchar_t)c[3] & 0x3F); return 4; } } /* * Write a single UTF-8 character to @s, * returning the length, in bytes, of the character written. * * @unicode must be U+0000..U+10FFFF, but not U+D800..U+DFFF. * * This function will write up to 4 bytes to @out. */ static int utf8_write_char(js_uchar_t unicode, char *out) { unsigned char *o = (unsigned char*) out; assert(unicode <= 0x10FFFF && !(unicode >= 0xD800 && unicode <= 0xDFFF)); if (unicode <= 0x7F) { /* U+0000..U+007F */ *o++ = unicode; return 1; } else if (unicode <= 0x7FF) { /* U+0080..U+07FF */ *o++ = 0xC0 | unicode >> 6; *o++ = 0x80 | (unicode & 0x3F); return 2; } else if (unicode <= 0xFFFF) { /* U+0800..U+FFFF */ *o++ = 0xE0 | unicode >> 12; *o++ = 0x80 | (unicode >> 6 & 0x3F); *o++ = 0x80 | (unicode & 0x3F); return 3; } else { /* U+10000..U+10FFFF */ *o++ = 0xF0 | unicode >> 18; *o++ = 0x80 | (unicode >> 12 & 0x3F); *o++ = 0x80 | (unicode >> 6 & 0x3F); *o++ = 0x80 | (unicode & 0x3F); return 4; } } /* * Compute the Unicode codepoint of a UTF-16 surrogate pair. * * @uc should be 0xD800..0xDBFF, and @lc should be 0xDC00..0xDFFF. * If they aren't, this function returns false. */ static bool from_surrogate_pair(uint16_t uc, uint16_t lc, js_uchar_t *unicode) { if (uc >= 0xD800 && uc <= 0xDBFF && lc >= 0xDC00 && lc <= 0xDFFF) { *unicode = 0x10000 + ((((js_uchar_t)uc & 0x3FF) << 10) | (lc & 0x3FF)); return true; } else { return false; } } /* * Construct a UTF-16 surrogate pair given a Unicode codepoint. * * @unicode must be U+10000..U+10FFFF. */ static void to_surrogate_pair(js_uchar_t unicode, uint16_t *uc, uint16_t *lc) { js_uchar_t n; assert(unicode >= 0x10000 && unicode <= 0x10FFFF); n = unicode - 0x10000; *uc = ((n >> 10) & 0x3FF) | 0xD800; *lc = (n & 0x3FF) | 0xDC00; } #define is_space(c) ((c) == '\t' || (c) == '\n' || (c) == '\r' || (c) == ' ') #define is_digit(c) ((c) >= '0' && (c) <= '9') static bool parse_value (const char **sp, JsonNode **out); static bool parse_string (const char **sp, char **out); static bool parse_number (const char **sp, double *out); static bool parse_array (const char **sp, JsonNode **out); static bool parse_object (const char **sp, JsonNode **out); static bool parse_hex16 (const char **sp, uint16_t *out); static bool expect_literal (const char **sp, const char *str); static void skip_space (const char **sp); static void emit_value (SB *out, const JsonNode *node); static void emit_value_indented (SB *out, const JsonNode *node, const char *space, int indent_level); static void emit_string (SB *out, const char *str); static void emit_number (SB *out, double num); static void emit_array (SB *out, const JsonNode *array); static void emit_array_indented (SB *out, const JsonNode *array, const char *space, int indent_level); static void emit_object (SB *out, const JsonNode *object); static void emit_object_indented (SB *out, const JsonNode *object, const char *space, int indent_level); static int write_hex16(char *out, uint16_t val); static JsonNode *mknode(JsonTag tag); static void append_node(JsonNode *parent, JsonNode *child); static void prepend_node(JsonNode *parent, JsonNode *child); static void append_member(JsonNode *object, char *key, JsonNode *value); /* Assertion-friendly validity checks */ static bool tag_is_valid(unsigned int tag); static bool number_is_valid(const char *num); JsonNode *json_decode(const char *json) { const char *s = json; JsonNode *ret; skip_space(&s); if (!parse_value(&s, &ret)) return NULL; skip_space(&s); if (*s != 0) { json_delete(ret); return NULL; } return ret; } char *json_encode(const JsonNode *node) { return json_stringify(node, NULL); } char *json_encode_string(const char *str) { SB sb; sb_init(&sb); emit_string(&sb, str); return sb_finish(&sb); } char *json_stringify(const JsonNode *node, const char *space) { SB sb; sb_init(&sb); if (space != NULL) emit_value_indented(&sb, node, space, 0); else emit_value(&sb, node); return sb_finish(&sb); } void json_delete(JsonNode *node) { if (node != NULL) { json_remove_from_parent(node); switch (node->tag) { case JSON_STRING: free(node->string_); break; case JSON_ARRAY: case JSON_OBJECT: { JsonNode *child, *next; for (child = node->children.head; child != NULL; child = next) { next = child->next; json_delete(child); } break; } default:; } free(node); } } bool json_validate(const char *json) { const char *s = json; skip_space(&s); if (!parse_value(&s, NULL)) return false; skip_space(&s); if (*s != 0) return false; return true; } JsonNode *json_find_element(JsonNode *array, int index) { JsonNode *element; int i = 0; if (array == NULL || array->tag != JSON_ARRAY) return NULL; json_foreach(element, array) { if (i == index) return element; i++; } return NULL; } JsonNode *json_find_member(JsonNode *object, const char *name) { JsonNode *member; if (object == NULL || object->tag != JSON_OBJECT) return NULL; json_foreach(member, object) if (strcmp(member->key, name) == 0) return member; return NULL; } JsonNode *json_first_child(const JsonNode *node) { if (node != NULL && (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT)) return node->children.head; return NULL; } static JsonNode *mknode(JsonTag tag) { JsonNode *ret = (JsonNode*) calloc(1, sizeof(JsonNode)); if (ret == NULL) out_of_memory(); ret->tag = tag; return ret; } JsonNode *json_mknull(void) { return mknode(JSON_NULL); } JsonNode *json_mkbool(bool b) { JsonNode *ret = mknode(JSON_BOOL); ret->bool_ = b; return ret; } static JsonNode *mkstring(char *s) { JsonNode *ret = mknode(JSON_STRING); ret->string_ = s; return ret; } JsonNode *json_mkstring(const char *s) { return mkstring(json_strdup(s)); } JsonNode *json_mknumber(double n) { JsonNode *node = mknode(JSON_NUMBER); node->number_ = n; return node; } JsonNode *json_mkarray(void) { return mknode(JSON_ARRAY); } JsonNode *json_mkobject(void) { return mknode(JSON_OBJECT); } static void append_node(JsonNode *parent, JsonNode *child) { child->parent = parent; child->prev = parent->children.tail; child->next = NULL; if (parent->children.tail != NULL) parent->children.tail->next = child; else parent->children.head = child; parent->children.tail = child; } static void prepend_node(JsonNode *parent, JsonNode *child) { child->parent = parent; child->prev = NULL; child->next = parent->children.head; if (parent->children.head != NULL) parent->children.head->prev = child; else parent->children.tail = child; parent->children.head = child; } static void append_member(JsonNode *object, char *key, JsonNode *value) { value->key = key; append_node(object, value); } void json_append_element(JsonNode *array, JsonNode *element) { assert(array->tag == JSON_ARRAY); assert(element->parent == NULL); append_node(array, element); } void json_prepend_element(JsonNode *array, JsonNode *element) { assert(array->tag == JSON_ARRAY); assert(element->parent == NULL); prepend_node(array, element); } void json_append_member(JsonNode *object, const char *key, JsonNode *value) { assert(object->tag == JSON_OBJECT); assert(value->parent == NULL); append_member(object, json_strdup(key), value); } void json_prepend_member(JsonNode *object, const char *key, JsonNode *value) { assert(object->tag == JSON_OBJECT); assert(value->parent == NULL); value->key = json_strdup(key); prepend_node(object, value); } void json_remove_from_parent(JsonNode *node) { JsonNode *parent = node->parent; if (parent != NULL) { if (node->prev != NULL) node->prev->next = node->next; else parent->children.head = node->next; if (node->next != NULL) node->next->prev = node->prev; else parent->children.tail = node->prev; free(node->key); node->parent = NULL; node->prev = node->next = NULL; node->key = NULL; } } static bool parse_value(const char **sp, JsonNode **out) { const char *s = *sp; switch (*s) { case 'n': if (expect_literal(&s, "null")) { if (out) *out = json_mknull(); *sp = s; return true; } return false; case 'f': if (expect_literal(&s, "false")) { if (out) *out = json_mkbool(false); *sp = s; return true; } return false; case 't': if (expect_literal(&s, "true")) { if (out) *out = json_mkbool(true); *sp = s; return true; } return false; case '"': { char *str; if (parse_string(&s, out ? &str : NULL)) { if (out) *out = mkstring(str); *sp = s; return true; } return false; } case '[': if (parse_array(&s, out)) { *sp = s; return true; } return false; case '{': if (parse_object(&s, out)) { *sp = s; return true; } return false; default: { double num; if (parse_number(&s, out ? &num : NULL)) { if (out) *out = json_mknumber(num); *sp = s; return true; } return false; } } } static bool parse_array(const char **sp, JsonNode **out) { const char *s = *sp; JsonNode *ret = out ? json_mkarray() : NULL; JsonNode *element; if (*s++ != '[') goto failure; skip_space(&s); if (*s == ']') { s++; goto success; } for (;;) { if (!parse_value(&s, out ? &element : NULL)) goto failure; skip_space(&s); if (out) json_append_element(ret, element); if (*s == ']') { s++; goto success; } if (*s++ != ',') goto failure; skip_space(&s); } success: *sp = s; if (out) *out = ret; return true; failure: json_delete(ret); return false; } static bool parse_object(const char **sp, JsonNode **out) { const char *s = *sp; JsonNode *ret = out ? json_mkobject() : NULL; char *key; JsonNode *value; if (*s++ != '{') goto failure; skip_space(&s); if (*s == '}') { s++; goto success; } for (;;) { if (!parse_string(&s, out ? &key : NULL)) goto failure; skip_space(&s); if (*s++ != ':') goto failure_free_key; skip_space(&s); if (!parse_value(&s, out ? &value : NULL)) goto failure_free_key; skip_space(&s); if (out) append_member(ret, key, value); if (*s == '}') { s++; goto success; } if (*s++ != ',') goto failure; skip_space(&s); } success: *sp = s; if (out) *out = ret; return true; failure_free_key: if (out) free(key); failure: json_delete(ret); return false; } bool parse_string(const char **sp, char **out) { const char *s = *sp; SB sb; char throwaway_buffer[4]; /* enough space for a UTF-8 character */ char *b; if (*s++ != '"') return false; if (out) { sb_init(&sb); sb_need(&sb, 4); b = sb.cur; } else { b = throwaway_buffer; } while (*s != '"') { unsigned char c = *s++; /* Parse next character, and write it to b. */ if (c == '\\') { c = *s++; switch (c) { case '"': case '\\': case '/': *b++ = c; break; case 'b': *b++ = '\b'; break; case 'f': *b++ = '\f'; break; case 'n': *b++ = '\n'; break; case 'r': *b++ = '\r'; break; case 't': *b++ = '\t'; break; case 'u': { uint16_t uc, lc; js_uchar_t unicode; if (!parse_hex16(&s, &uc)) goto failed; if (uc >= 0xD800 && uc <= 0xDFFF) { /* Handle UTF-16 surrogate pair. */ if (*s++ != '\\' || *s++ != 'u' || !parse_hex16(&s, &lc)) goto failed; /* Incomplete surrogate pair. */ if (!from_surrogate_pair(uc, lc, &unicode)) goto failed; /* Invalid surrogate pair. */ } else if (uc == 0) { /* Disallow "\u0000". */ goto failed; } else { unicode = uc; } b += utf8_write_char(unicode, b); break; } default: /* Invalid escape */ goto failed; } } else if (c <= 0x1F) { /* Control characters are not allowed in string literals. */ goto failed; } else { /* Validate and echo a UTF-8 character. */ int len; s--; len = utf8_validate_cz(s); if (len == 0) goto failed; /* Invalid UTF-8 character. */ while (len--) *b++ = *s++; } /* * Update sb to know about the new bytes, * and set up b to write another character. */ if (out) { sb.cur = b; sb_need(&sb, 4); b = sb.cur; } else { b = throwaway_buffer; } } s++; if (out) *out = sb_finish(&sb); *sp = s; return true; failed: if (out) sb_free(&sb); return false; } /* * The JSON spec says that a number shall follow this precise pattern * (spaces and quotes added for readability): * '-'? (0 | [1-9][0-9]*) ('.' [0-9]+)? ([Ee] [+-]? [0-9]+)? * * However, some JSON parsers are more liberal. For instance, PHP accepts * '.5' and '1.'. JSON.parse accepts '+3'. * * This function takes the strict approach. */ bool parse_number(const char **sp, double *out) { const char *s = *sp; /* '-'? */ if (*s == '-') s++; /* (0 | [1-9][0-9]*) */ if (*s == '0') { s++; } else { if (!is_digit(*s)) return false; do { s++; } while (is_digit(*s)); } /* ('.' [0-9]+)? */ if (*s == '.') { s++; if (!is_digit(*s)) return false; do { s++; } while (is_digit(*s)); } /* ([Ee] [+-]? [0-9]+)? */ if (*s == 'E' || *s == 'e') { s++; if (*s == '+' || *s == '-') s++; if (!is_digit(*s)) return false; do { s++; } while (is_digit(*s)); } if (out) *out = strtod(*sp, NULL); *sp = s; return true; } static void skip_space(const char **sp) { const char *s = *sp; while (is_space(*s)) s++; *sp = s; } static void emit_value(SB *out, const JsonNode *node) { assert(tag_is_valid(node->tag)); switch (node->tag) { case JSON_NULL: sb_puts(out, "null"); break; case JSON_BOOL: sb_puts(out, node->bool_ ? "true" : "false"); break; case JSON_STRING: emit_string(out, node->string_); break; case JSON_NUMBER: emit_number(out, node->number_); break; case JSON_ARRAY: emit_array(out, node); break; case JSON_OBJECT: emit_object(out, node); break; default: assert(false); } } void emit_value_indented(SB *out, const JsonNode *node, const char *space, int indent_level) { assert(tag_is_valid(node->tag)); switch (node->tag) { case JSON_NULL: sb_puts(out, "null"); break; case JSON_BOOL: sb_puts(out, node->bool_ ? "true" : "false"); break; case JSON_STRING: emit_string(out, node->string_); break; case JSON_NUMBER: emit_number(out, node->number_); break; case JSON_ARRAY: emit_array_indented(out, node, space, indent_level); break; case JSON_OBJECT: emit_object_indented(out, node, space, indent_level); break; default: assert(false); } } static void emit_array(SB *out, const JsonNode *array) { const JsonNode *element; sb_putc(out, '['); json_foreach(element, array) { emit_value(out, element); if (element->next != NULL) sb_putc(out, ','); } sb_putc(out, ']'); } static void emit_array_indented(SB *out, const JsonNode *array, const char *space, int indent_level) { const JsonNode *element = array->children.head; int i; if (element == NULL) { sb_puts(out, "[]"); return; } sb_puts(out, "[\n"); while (element != NULL) { for (i = 0; i < indent_level + 1; i++) sb_puts(out, space); emit_value_indented(out, element, space, indent_level + 1); element = element->next; sb_puts(out, element != NULL ? ",\n" : "\n"); } for (i = 0; i < indent_level; i++) sb_puts(out, space); sb_putc(out, ']'); } static void emit_object(SB *out, const JsonNode *object) { const JsonNode *member; sb_putc(out, '{'); json_foreach(member, object) { emit_string(out, member->key); sb_putc(out, ':'); emit_value(out, member); if (member->next != NULL) sb_putc(out, ','); } sb_putc(out, '}'); } static void emit_object_indented(SB *out, const JsonNode *object, const char *space, int indent_level) { const JsonNode *member = object->children.head; int i; if (member == NULL) { sb_puts(out, "{}"); return; } sb_puts(out, "{\n"); while (member != NULL) { for (i = 0; i < indent_level + 1; i++) sb_puts(out, space); emit_string(out, member->key); sb_puts(out, ": "); emit_value_indented(out, member, space, indent_level + 1); member = member->next; sb_puts(out, member != NULL ? ",\n" : "\n"); } for (i = 0; i < indent_level; i++) sb_puts(out, space); sb_putc(out, '}'); } void emit_string(SB *out, const char *str) { bool escape_unicode = false; const char *s = str; char *b; assert(utf8_validate(str)); /* * 14 bytes is enough space to write up to two * \uXXXX escapes and two quotation marks. */ sb_need(out, 14); b = out->cur; *b++ = '"'; while (*s != 0) { unsigned char c = *s++; /* Encode the next character, and write it to b. */ switch (c) { case '"': *b++ = '\\'; *b++ = '"'; break; case '\\': *b++ = '\\'; *b++ = '\\'; break; case '\b': *b++ = '\\'; *b++ = 'b'; break; case '\f': *b++ = '\\'; *b++ = 'f'; break; case '\n': *b++ = '\\'; *b++ = 'n'; break; case '\r': *b++ = '\\'; *b++ = 'r'; break; case '\t': *b++ = '\\'; *b++ = 't'; break; default: { int len; s--; len = utf8_validate_cz(s); if (len == 0) { /* * Handle invalid UTF-8 character gracefully in production * by writing a replacement character (U+FFFD) * and skipping a single byte. * * This should never happen when assertions are enabled * due to the assertion at the beginning of this function. */ assert(false); if (escape_unicode) { #if HAVE_STRLCPY strlcpy(b, "\\uFFFD", out->end - out->start ); #else strcpy(b, "\\uFFFD"); #endif b += 6; } else { *b++ = 0xEF; *b++ = 0xBF; *b++ = 0xBD; } s++; } else if (c < 0x1F || (c >= 0x80 && escape_unicode)) { /* Encode using \u.... */ uint32_t unicode; s += utf8_read_char(s, &unicode); if (unicode <= 0xFFFF) { *b++ = '\\'; *b++ = 'u'; b += write_hex16(b, unicode); } else { /* Produce a surrogate pair. */ uint16_t uc, lc; assert(unicode <= 0x10FFFF); to_surrogate_pair(unicode, &uc, &lc); *b++ = '\\'; *b++ = 'u'; b += write_hex16(b, uc); *b++ = '\\'; *b++ = 'u'; b += write_hex16(b, lc); } } else { /* Write the character directly. */ while (len--) *b++ = *s++; } break; } } /* * Update *out to know about the new bytes, * and set up b to write another encoded character. */ out->cur = b; sb_need(out, 14); b = out->cur; } *b++ = '"'; out->cur = b; } static void emit_number(SB *out, double num) { /* * This isn't exactly how JavaScript renders numbers, * but it should produce valid JSON for reasonable numbers * preserve precision well enough, and avoid some oddities * like 0.3 -> 0.299999999999999988898 . */ char buf[64]; snprintf(buf, sizeof(buf), "%.16g", num); if (number_is_valid(buf)) sb_puts(out, buf); else sb_puts(out, "null"); } static bool tag_is_valid(unsigned int tag) { return (/* tag >= JSON_NULL && */ tag <= JSON_OBJECT); } static bool number_is_valid(const char *num) { return (parse_number(&num, NULL) && *num == '\0'); } static bool expect_literal(const char **sp, const char *str) { const char *s = *sp; while (*str != '\0') if (*s++ != *str++) return false; *sp = s; return true; } /* * Parses exactly 4 hex characters (capital or lowercase). * Fails if any input chars are not [0-9A-Fa-f]. */ static bool parse_hex16(const char **sp, uint16_t *out) { const char *s = *sp; uint16_t ret = 0; uint16_t i; uint16_t tmp; char c; for (i = 0; i < 4; i++) { c = *s++; if (c >= '0' && c <= '9') tmp = c - '0'; else if (c >= 'A' && c <= 'F') tmp = c - 'A' + 10; else if (c >= 'a' && c <= 'f') tmp = c - 'a' + 10; else return false; ret <<= 4; ret += tmp; } if (out) *out = ret; *sp = s; return true; } /* * Encodes a 16-bit number into hexadecimal, * writing exactly 4 hex chars. */ static int write_hex16(char *out, uint16_t val) { const char *hex = "0123456789ABCDEF"; *out++ = hex[(val >> 12) & 0xF]; *out++ = hex[(val >> 8) & 0xF]; *out++ = hex[(val >> 4) & 0xF]; *out++ = hex[ val & 0xF]; return 4; } bool json_check(const JsonNode *node, char errmsg[256]) { #define problem(...) do { \ if (errmsg != NULL) \ snprintf(errmsg, 256, __VA_ARGS__); \ return false; \ } while (0) if (node->key != NULL && !utf8_validate(node->key)) problem("key contains invalid UTF-8"); if (!tag_is_valid(node->tag)) problem("tag is invalid (%u)", node->tag); if (node->tag == JSON_BOOL) { if (node->bool_ != false && node->bool_ != true) problem("bool_ is neither false (%d) nor true (%d)", (int)false, (int)true); } else if (node->tag == JSON_STRING) { if (node->string_ == NULL) problem("string_ is NULL"); if (!utf8_validate(node->string_)) problem("string_ contains invalid UTF-8"); } else if (node->tag == JSON_ARRAY || node->tag == JSON_OBJECT) { JsonNode *head = node->children.head; JsonNode *tail = node->children.tail; if (head == NULL || tail == NULL) { if (head != NULL) problem("tail is NULL, but head is not"); if (tail != NULL) problem("head is NULL, but tail is not"); } else { JsonNode *child; JsonNode *last = NULL; if (head->prev != NULL) problem("First child's prev pointer is not NULL"); for (child = head; child != NULL; last = child, child = child->next) { if (child == node) problem("node is its own child"); if (child->next == child) problem("child->next == child (cycle)"); if (child->next == head) problem("child->next == head (cycle)"); if (child->parent != node) problem("child does not point back to parent"); if (child->next != NULL && child->next->prev != child) problem("child->next does not point back to child"); if (node->tag == JSON_ARRAY && child->key != NULL) problem("Array element's key is not NULL"); if (node->tag == JSON_OBJECT && child->key == NULL) problem("Object member's key is NULL"); if (!json_check(child, errmsg)) return false; } if (last != tail) problem("tail does not match pointer found by starting at head and following next links"); } } return true; #undef problem } jo-1.4/json.h000066400000000000000000000066441370462052600131270ustar00rootroot00000000000000/* Copyright (C) 2011 Joseph A. Adams (joeyadams3.14159@gmail.com) All rights reserved. Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef CCAN_JSON_H #define CCAN_JSON_H #include #include typedef enum { JSON_NULL, JSON_BOOL, JSON_STRING, JSON_NUMBER, JSON_ARRAY, JSON_OBJECT, } JsonTag; typedef struct JsonNode JsonNode; struct JsonNode { /* only if parent is an object or array (NULL otherwise) */ JsonNode *parent; JsonNode *prev, *next; /* only if parent is an object (NULL otherwise) */ char *key; /* Must be valid UTF-8. */ JsonTag tag; union { /* JSON_BOOL */ bool bool_; /* JSON_STRING */ char *string_; /* Must be valid UTF-8. */ /* JSON_NUMBER */ double number_; /* JSON_ARRAY */ /* JSON_OBJECT */ struct { JsonNode *head, *tail; } children; }; }; /*** Encoding, decoding, and validation ***/ JsonNode *json_decode (const char *json); char *json_encode (const JsonNode *node); char *json_encode_string (const char *str); char *json_stringify (const JsonNode *node, const char *space); void json_delete (JsonNode *node); bool json_validate (const char *json); /*** Lookup and traversal ***/ JsonNode *json_find_element (JsonNode *array, int index); JsonNode *json_find_member (JsonNode *object, const char *key); JsonNode *json_first_child (const JsonNode *node); #define json_foreach(i, object_or_array) \ for ((i) = json_first_child(object_or_array); \ (i) != NULL; \ (i) = (i)->next) /*** Construction and manipulation ***/ JsonNode *json_mknull(void); JsonNode *json_mkbool(bool b); JsonNode *json_mkstring(const char *s); JsonNode *json_mknumber(double n); JsonNode *json_mkarray(void); JsonNode *json_mkobject(void); void json_append_element(JsonNode *array, JsonNode *element); void json_prepend_element(JsonNode *array, JsonNode *element); void json_append_member(JsonNode *object, const char *key, JsonNode *value); void json_prepend_member(JsonNode *object, const char *key, JsonNode *value); void json_remove_from_parent(JsonNode *node); /*** Debugging ***/ /* * Look for structure and encoding problems in a JsonNode or its descendents. * * If a problem is detected, return false, writing a description of the problem * to errmsg (unless errmsg is NULL). */ bool json_check(const JsonNode *node, char errmsg[256]); #endif jo-1.4/press.md000066400000000000000000000061361370462052600134570ustar00rootroot00000000000000## "Press" reports * [Hacker News](https://news.ycombinator.com/item?id=11230023) * [Lobsters](https://lobste.rs/s/tyehi1/a_shell_command_to_create_json_jo) * [reddit](https://www.reddit.com/r/programming/comments/49sx6x/a_shell_command_to_create_json_jo) * [Hacker News](https://news.ycombinator.com/item?id=11272678) * [Trivium](http://chneukirchen.org/trivium/2016-05-13) jo-1.4/rpm-build/000077500000000000000000000000001370462052600136665ustar00rootroot00000000000000jo-1.4/rpm-build/jo.spec000066400000000000000000000023001370462052600151450ustar00rootroot00000000000000Name: jo Version: 1.4 Release: 2%{?dist} Summary: jo is a small utility to create JSON objects License: GPL2 URL: https://github.com/jpmens/jo Source0: https://github.com/jpmens/jo/releases/download/%{version}/jo-%{version}.tar.gz BuildRequires: autoconf BuildRequires: pandoc %description jo is a small utility to create JSON objects %prep %setup -q %build %configure make %{?_smp_mflags} make check %install rm -rf $RPM_BUILD_ROOT %make_install %files %doc %{_bindir}/* %{_mandir}/man1/* %if 0%{?suse_version} %{_datadir}/bash-completion/completions %else %{_sysconfdir}/bash_completion.d/%{name}.bash %endif %changelog * Sat Jul 18 2020 JP Mens 1.4 - bump version -- see Changelog * Tue Apr 28 2020 Christian Albrecht 1.3-2 - Fix broken download url - Make bash completion work on RHEL based distros * Tue Apr 7 2020 Kilian Cavalotti 1.3-1 - Bumped to 1.3 release version - Include bash-completion file in package * Thu May 18 2017 Fabian Arrotin 1.1-1 - Bumped to 1.1 release version * Wed Mar 15 2017 Fabian Arrotin 1.0-1 - initial spec jo-1.4/snapcraft.yaml000066400000000000000000000005521370462052600146420ustar00rootroot00000000000000name: jo version: "1.4" summary: jo description: | This is jo, a small utility to create JSON objects or arrays. confinement: strict grade: stable base: core apps: jo: command: jo plugs: [home, removable-media] parts: jo: plugin: autotools source-type: git source: https://github.com/jpmens/jo build-packages: - pkg-config jo-1.4/tests/000077500000000000000000000000001370462052600131355ustar00rootroot00000000000000jo-1.4/tests/jo-logo.png000066400000000000000000000176221370462052600152210ustar00rootroot00000000000000‰PNG  IHDR,,y}ŽusRGB®ÎéLIDATxí ˜\E¹†ÿ™ž}É2“Ìd²/$d…@@‹!`€ç²)rTr“”E D¸¬A q ‚ *‹ÑkˆŠÑË&È’ û>I&Édö«zÒ™žîžž:}ºO×™~ëyÎt÷9µüõþÝßTÕ©S%B€ àY1ì¯Î¥Ž!êÈŽqS€RM M°U/ªãõPaá‚¥ÅénuÌQGN(¯€ÒH U•ý¤:f©£>fÈíêýuê?v™·€<' U“Õ1T¿ µ°t÷oµ: ÔA€ `eÐq¡1ªéêbe›‹°Ð=¿™!Áª 倀¥*B‚zµÔNÌ‚ Yß@À7,߸ C!‹ï àŽ&ˆÎ8¾XÎ>©Ô7•ÃP@À~+Þ: O/Ûkd¨#Á:zl¡\q^™QÆD‚ `B@?ƒc*Xt Mˆ°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!€`™P" `Ë 7` `BÁ2¡D@À –nÀ@À„‚eB‰8€€,+Ü€€€ Ë„q + XV¸# – %â@V@°¬pF@&,JĬ €`YጀL X&”ˆXAÁ²Â ˜@°L(°‚‚e…00!c‰8Þس¯YN½r´´F—÷Ý«*äÌJ]øÆ‚­²ìoµ‡>‡ÞLX( ¿90ô1£_¿të&ysu}ƒ™ÓJä;WV:ÿ«å52ÑÎCŸCoòÔ¯cùÂR\Èÿõ“t¿"Xéö@XùZ¨Þû¸AZZÂN|»·¶³ŠmÜÞ,ï}ÔqP¿\š¥²i‹ÊÊ÷'ÖniŠÉhò˜‚NuÛ³¿5f¼üÜ,ië\:UÞgøvû¬†˜{ë¢íòn ñ0)úÞÿ©”!•y&Q“§¶®U¾÷tµ<ó{eóŽf: W.:½·Ì:¯¯äçÑ¢ˆ»¾±U.™¿)Ö¥nϘ+w}m@·ñˆŸ‚ŸÑÕ?ÿã€,ý€QÜÈH·|µä©”®Ùß"gÌ^'¯½ßÑ]Ú¾»%øù7Ú'¿¼gˆ”Rn‡ß Ð-à_.ß—ÙGÞ¹U—P&$þ•fØ— Mõq¾ñàÖNbŽ`ùä¦G¶‡Ÿâ=¬!€`Yã o ©®i‘Å/ÕÄ-ìÇ¿Ù#ºF€€m,Û<’b{>ÜÔ(Íñ 9Pß&onŒ‰«HưRýÜSJeꤢ¸%T–u¸ [Ý­Ó7ìb…ìˆ-:n¬/2Nžºóe òÌâ™ä•®8f!;"ÙÅc–†!/'KîžÝ1"”_øë›«êä©—÷†Ÿâ} tüZ’˜)Yµ8õ˜bu×­ÌG¯’€Tô ÈæÑݱáU¹ò¦îêÅ #Å>Š;vX~° =ÈÞUR™#£{ç²+{=Øà\Yñftê¡:íG Š]ת~),èP¬\%Xs?WaØ™Å/ïA°Âx$ûmÿƒ’] ù™¨ý‡ÏŠª[ ##~TGEÇÓ ÇŒ7ª0?[nº¬_T¡úçùíY’›ãÿ¯Æ„Q±YŒ`ŸÞÄ Aâ5müÿ­LºÔ<1†àT©É }K;O3˜tX?Æa±[ áÖþ÷ùe2ïò~’Ó9KÑÝÀû¯©”‹gôîÛ÷ãGt!ê‚UÞ+ ƒúwnuéJOè"½oôã½Ô*åç*1:úG6vxždG ¼Œš'¹JpšÂzvz,&²õ‹E–j5Ì»¼¿|þÌ>òk5ïjë.5q´"WÎ9¹Týpãw)cågë¹ #u I¤5l¶ºéÑC;3Öl'¨ëúmïFtÕŠµµ¾™`‚e™—õ,2è®IdÈW­!=ÎôÁºŽ»yzü«_3—jÑÒÝÌ9Å“‰,×OŸõ þŠÉ¶]ª>¨"GŠ ¢;ãTkêÅW;?›9.†/üTÿžh«Ù·»'ÖÜÒ:’ìª5wüÆD”G†,u?qÒaùkŒj9èq0[CuM³¬ßÚ$›ÔsûÕ£AúQ—œ@–æg…vHe® VG®:—Œ [NºÅ¹mWÇSã"ºƒ¡r&EŒwé1­ÃzÀ‡PýzÊ+‚e™'õB}ðÃMM‡,?"ö¸”Çúù:‰Õ:;”‰z󑚃uÞuÂOÅ}ÿÛ†¨.bì²ã&1µ»üÜ\_»Eu7¯Ý ï|½F—›|Cißÿ¸Q¦«Å 祿\I?ãÖÏ`u÷Ó„‘üÒY}CÅñj)çmlK+‚Yé#°j]½œüÕµ)«PÍôò.ßzl‡\óÀV5–œ\(o^ýAÁò‡Ÿ¬µrku³œ5wƒzœ(Á¯jöðÏwÇÈHJŸ@°|îÀtšßªfd^zÛ&ÑK{nùþu÷¯ó¼)¯m <ï XÞ3ï1%.~¹F~ÿ÷ÄDCOÓå&8Šª»‡sîÙ" j.!s$øuÉ@Ô46†&}GÐùʤǎ/®x0íÈ")ïÞMü×úFYúûyô™Ýj2©ùØÔ»j ~ɲ½òÅ™}bÉÙG€Vs©7z~Å>Õt6nuÕ…}åO‹FÈ…Ÿê-Õ3‹z³‹^jræ”q…r·Ú aÅ¢áÁ¥oœÔ`Á’jàóy\ËçL—ùK™åH»¦[$÷Ì÷Ñ¡£ÆÊOnÔå"†‘yêÏo¯iUëR3•"VyœK/+½ü}Yº7zåÍŽÇ]º«„~2Po\ªŸì.LÿD±œ~|÷sÐÂóyiåþð¼ïÁ¬ìÜTUM·höì3ìÖÏóM6ÜæJOà¼Äá˜ÔkïulW–ª:“¯,;üà++VoèXÒÆÄði“‹­Üy‚zäÈÉBŸ«×70ŽeâˆÁêNôº [NuºLË€ò€Œ7ÿjê«ÃéóšåyGÀü[áM”d9 æStU"—wî®zzM¯>%æ_Í:eO«yµ»â¹n1óo…Å•À4o ´9lÎ8éÞ…jâ$M«z®G Cäzö+‚Õ³ý›’ÚÆXb8^ANw‘Ö­¥šZó&SšÏk×›x6qÍŸ,ú-­VW®2rÍFgÏîÜÓ,5îBê1¯x›¡†ìàÕÿ,ÿûÐóŒV;ö8 }û€£»x+ß­s4ˆ®×²×›jz>«çû8é5§¶+)4ˆ7WÕËûkÍg£;E?elì=“^q2L;+í.ðŸE9A=¼lôý·n7je½±ªNžýcÇÆ&eèe ™AÁÊ ?'½–Ÿ=ÍÙîÐÏ­Ø/w?¹3®hmÜÞ(ŸŸ·ÑÑ…1CsåˆÑ´°’î`K3D°,uŒíf÷ÉRÇ++Üüè¹X Ò[«õUÇ\®½µ-òƒçvÉÔË>–mp6@¯×k×; 2ƒëae†Ÿ“^Ë’¢€|ýóårýCÎÖÄzFí£¨>¥Ù2 që GÏý%ËÔ+/è+gŸTš¬ìÈÇ',Ÿ8ÊV3'Ž*§ï¬Z:Þ#]0½4¸ s¯lýV¤Î.+ul­Ë9lœ;©¶M?¶D^|`¨T–¥¶{¦%ñ*Õ²zâ–ÁF‹&µ’dfË 7xcDø¹d—x‚ÚBþï?)眔š9QJ ŸºcÜ͵ӎw­¹ds"?w,wü|•º±©c*A* ×KÏ|å§‚,?yË¥­·;›ECÉõÆå½säÄÉú8tš7pL€1,ÇÈü›àõ÷êŸk°-—£ ‰ —,—ý’\¯úùë?;Û¿¯ÔÁF~á€þ&€`ùÛFÖ·¨õ]¾ñàV©QãF¦¡·Ú¢¤ˆ¯‡)/âyC€1,o8§©”6õ¬^“\»`›üÂáSúAVAH“Û(¶KV—hü}¡MMk¿áámòðÏwKC£óùW§ãl»xÓÂz¿ ÍïO9´Sχ6 7!±RÛÊÓ{9,‘èH=ZX)d\×Ð*zqºx¡Xm™HÑݸK>ÝGæ?¶Cö8»Ò¶Î<±DÆ ÏgvF^Ó­Ö}âjŸRGÁJ[¹A-nwãÃñ¸{ýÉ‘2~Dj–øÕ3Ä/;§Ü·x—q-‹Õæw}­’G`bÓ;^WÎXãJÇ)‡{Ìv$亄F˜‹¤¿¼úq”xGb9›§šu~™zXØ,¾~¤ø‘ë«dôZW]‹çK}MoKH+ul­ÈyøÀ\£…îô¸•~„æs§;Û\ŠJbDÆ@°z¸«õc1³?[·–¥j¾Õæ”k..§+—ÓMÁJ’tw*‘#IÅÇÍfê¤"9v|ô8Ya~–|é¬>òöS£äâ}«¸Û/&âc††£É)¬§æ²äŽÁÒØœXíúõNýÒ)Ùª¿wõgÊdÖ[¤¢,'¸qÄ™'”ÈžÜ+ø91Ë3/•øu¿ÓþŸÉaõsSïf‡ù3:‚•¿é•l©O/8µWpµNÖBOÌ[zæU?û}Xíü‘ úþð“k+u++O¬ àcŒaùØy˜L#€`ešÇ©/|LÁò±ó0™FÁÊ4S_ø˜‚åcça:2‚•i§¾ð1ËÇÎÃtd+Óvž6]oŒ°âZYùΟ×$qó5ƒeÛ/o®ªK<Rú‚«5øÂM]Y]Ó"3f¯“üÜ,ÙüâáR¤váÉ´°vK“Ìœ»^ú–fËÖ—g!Âü@°|îÜ>¥¹\팣…ª /3—¨Ö¨ú§{ËÐÊ\ÄÊçßçîÌG°º#dùõµ§á÷®h¹•©5/?/[¿yPj !w+d^ÿÁ ìÞÑÐÔ*MÍη¬Oª¥¡Ñ“š©YfºæV003—X] …Õ[O׫…Ïœ³N#6”?"_ÝØ¹¥¥£¿\#÷þ¬Z>XÛ zÕÑ£Æȼ/÷—Ó+ñ¬ŠùG­ÜÿÔ.yku½4+Ñ,ï‚üì`7nÖ¹ñwô‰edÍþfùôÜ Q—>¡6Ú¸nU§ó­jsÈÇŸß- –î’5ƒKD;¾Pnýj9qrq§¸|°Ÿ‚e¿:Y˜­ÚÄ“•èèM;uÐ-‡%¿«i<ÓþG‹Õ=?­–Ù.ŸœR$ ¿9P›Údá³»äœkÖË“· ’ §§~Â'_Ø#_ùÎfÑ›m|æSí›^üóÃYºl¯œrLb‚¡»ÁG+¡Ð¦(<ñÛé]ܹàܼp»üï“Õ¢7ݸá‹ýd¿Újþ¡§wɳ×˳w –SKCÙðê–œnb^n¶ÜMG+Bÿ(ÿøZmx”àû5eþ÷·ËÉGÉ  U-‹ö³ã/ýH¾v÷–`+«wIê¶sYµ®A®¼k‹LP­¿— “þ}Û¿nk65+Ñ[Å…yèÚÎ ž{e_ƒ7>¨—{•hŸ}R‰<óÝ!Á¦Žt¾ÚŒcÊ>’+Ô.B«ž-VwX; ]TFœ°†ž²ÆÉ5d©ju5©­ÇæªÍQCb¥KèU+Î+“ê½­òâ«Ñ?r+V¯ow?Š>t·S h(Ü·x§Ô7¶Éc7V«Ð5/^ŸR TP¾þ_å‡ÄJ—Û¯OŽ\¦î¬nÚÑSì½°2#@ +1nÖ§Ò‚¢ÃäÑ]§ÑGŽÎ¾ Å 7}=sÎzY¿µ)*zŸ’lÙ¢æAå¨F›;Zö·Z6 G¦Œ+ŒŠëÅ ]?mËø‘íõ /Sw«uÐqΠ[ŽÆê÷–ÕîIܸÐÁ\5¡42ä<Šy½»ÏÏÝ3$8/ ÚëúÐá@}«l­n½ëtVpOìöóúox+¬ãlòßéúé1¿œÛ›…¸$Ê ùÖ’£ Ë„’ã ˜´úC5–Uqpì(T5Û[G#Æ 7}8*ºÕ+­–ʆÆÖ¨K?}AÝ$P¡£ó%)'FTåÊ+oˆlØÞ$ãGt«ûPÝ1Ôa¸ŠCðưüã+G–žóíw¿ô]ºðîª-~i×™q|ê¦6è™÷•å9òöšÙ½W ¦ ¿\®¦Y¨p´-© gÔÎàg/¶ d¨¬æ–6ù™šî¡·ž?ÍÃé¡òyMœ‚•8;«Sžxd‘œûÉRyü¹=òí”Í;šäãÍ2ûÞ-ò‡ÿ? ³?[&ÃRغÐs¾.œÞKêÚäs7o ð_·`«\¬ÞÏûr?ѽ´}µÑ­¯dByb©œzL‘Ü·¸ZXR좮ÙÐ _¾}“¼þ~½Üti?)ïM'#™ÌSÞJ5aòoQ-=¸´`üxþ ¹ZM_¸ý‡;ä–E;‚—õó†×~¡\¾=«"åÏÝÍ»¼¿üS jÿne­ÉZ%Ù²ð†*¹øŒÞ²`É.Y­¦=èÖ_VVô8[x]º{¯oL¶¨yi¹ ô|­§ï¢¦/l–ëÚ&×>¸-˜UqA–ÜvEÅ¡_wYsÝ2–eqjŽ«=ûZdBŒ;aº[öø¼r›'-z@\Ït×·õ½%EyþÞ¡òÚûuR³¿U¦N,”R5­B ÌêgG'Í=!VO-Q/=Ïì©Ûïj¾§¦]äådÉ”±…¢'ø€7ß\ÿqñÅzŒ¨º¦Uº×­—Á¹Á#• ¨–ÞqŠ:­TÅ…ÉxU­V§æ{…¦*t*L}Ð †UåÈk|öË_þ’Ç~±Ku­RQ–“ºã‡;ÕdÐìà3>«JÂæ>´´Z«¥d4‡ÕëåÛ?Ø!•e¹è´Ô?j”°Ñ$L +)½Ëäùû‚ãA-j¼ZwñtWð'· ’¡Ú§1xgIúJZ²¬Fu3ëÕ]F5ÇJõìŽ:¼@¹¾*(â鳊’½ €`yA9‰eè1!½Ê¨ž˜YR”le¸´N¢yždõÊc#¤zOsðd©j]–õ ¸¸÷Äp qMÁrÐÛ êΗîfrÐwÿ*Ë™ð™‰ßä|f"=ê xJÁò7…An Xnè‘ð”‚å)n ƒÜ@°ÜÐ#- à)ËSܸ!€`¹¡GZ@ÀS–§¸) pCÁrC´€€§,OqS à†‚å†i!O Xžâ¦0@À Ë =ÒBž@°<ÅMa€€–z¤…<%€`yŠ›Â 7,7ôH xJÁò7…An Xnè‘ð”‚å)n ƒÜ@°ÜÐ#- à)ËSܸ!€`¹¡GZ@ÀS–§¸) pCÁrC´€€§,OqS à†‚å†i!O Xžâ¦0@À Ë =ÒBž@°<ÅMa€€–z¤…<%€`yŠ›Â 7,7ôH xJÁò7…An Xnè‘ð”‚å)n ƒÜ@°ÜÐ#- à)ËSܸ!€`¹¡GZ@ÀS–§¸) pCÁrC´€€§,OqS à†‚å†i!O Xžâ¦0@À Ë =ÒBž@°<ÅMa€€–z¤…<%€`yŠ›Â 7,7ôH xJÁò7…An Xnè‘ð”‚å)n ƒÜ@°ÜÐ#- à)ËSܸ!€`¹¡GZ@ÀS–§¸) pCÁrC´€€§,OqS à†‚å†i!O Xžâ¦0@À Ë =ÒBž@°<ÅMa€€–z¤…<%€`yŠ›Â 7,7ôH xJÁò7…Anä8I¼üõZ¹ùÑmN’€@\o¬ª{=ü¢#Áúë;u¢ t K˜ê” $DÁJ‰ t@°ÒA2!„„«%¡Ô$‚ àÖ`mò®LJ‚ MY“U¨×5ê(M(AH-&•ý‘ƒeÔªW=_a†:B"vð/€ÒNàeÁâ`ikVªcŸ:¦©#O@é&Р ¸[7©£-Vkª¿ºp†:†ª#4Æ¥Þ xF M•´E/«cc¨Ôäk•à†m IEND®B`‚jo-1.4/tests/jo.01.exp000066400000000000000000000000071370462052600144770ustar00rootroot00000000000000["jo"] jo-1.4/tests/jo.01.sh000066400000000000000000000000351370462052600143160ustar00rootroot00000000000000# basic logo ${JO:-jo} -a jo jo-1.4/tests/jo.02.exp000066400000000000000000000000071370462052600145000ustar00rootroot00000000000000["jo"] jo-1.4/tests/jo.02.sh000066400000000000000000000000551370462052600143210ustar00rootroot00000000000000# basic logo (stdin) echo jo | ${JO:-jo} -a jo-1.4/tests/jo.03.exp000066400000000000000000000000341370462052600145010ustar00rootroot00000000000000{"pass":true,"type":"text"} jo-1.4/tests/jo.03.sh000066400000000000000000000000611370462052600143170ustar00rootroot00000000000000# basic two values ${JO:-jo} pass=true type=text jo-1.4/tests/jo.04.exp000066400000000000000000000000711370462052600145030ustar00rootroot00000000000000{"name":"Jane Jolie","data":{"age":null,"country":"ES"}} jo-1.4/tests/jo.04.sh000066400000000000000000000003131370462052600143200ustar00rootroot00000000000000# nested with executable ${JO:-jo} name="Jane Jolie" data="$(${JO:-jo} age= country=ES)" # the double quotes around data are required for OpenBSD 5.8 # which mucks up the input with its pdksh otherwise jo-1.4/tests/jo.05.exp000066400000000000000000000000501370462052600145010ustar00rootroot00000000000000{"a":[1,2],"geo":{"lat":111,"lon":222}} jo-1.4/tests/jo.05.sh000066400000000000000000000001001370462052600143130ustar00rootroot00000000000000# nested native ${JO:-jo} a[]=1 a[]=2 geo[lat]=111 geo[lon]=222 jo-1.4/tests/jo.06.exp000066400000000000000000000001161370462052600145050ustar00rootroot00000000000000{ "artist": "Vanessa Paradis", "song": "Joe le taxi", "year": 1987 } jo-1.4/tests/jo.06.sh000066400000000000000000000001531370462052600143240ustar00rootroot00000000000000# strings and numbers; pretty (Vanessa) ${JO:-jo} -p artist="Vanessa Paradis" song="Joe le taxi" year=1987 jo-1.4/tests/jo.07.sh.in000066400000000000000000000000771370462052600147370ustar00rootroot00000000000000# version check [ "$(${JO:-jo} -v)" = "jo @PACKAGE_VERSION@" ] jo-1.4/tests/jo.08.exp000066400000000000000000000000761370462052600145140ustar00rootroot00000000000000{"program":"jo","authors":"Jan-Piet Mens "} jo-1.4/tests/jo.08.sh000066400000000000000000000001141370462052600143230ustar00rootroot00000000000000# data from file: text ${JO:-jo} program="jo" authors=@${srcdir:=.}/AUTHORS jo-1.4/tests/jo.09.exp000066400000000000000000000001121370462052600145040ustar00rootroot00000000000000{"program":"jo","authors":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} jo-1.4/tests/jo.09.sh000066400000000000000000000001261370462052600143270ustar00rootroot00000000000000# data from file: base64-encoded ${JO:-jo} program="jo" authors=%${srcdir:=.}/AUTHORS jo-1.4/tests/jo.10.exp000066400000000000000000000000461370462052600145020ustar00rootroot00000000000000["spring","summer","autumn","winter"] jo-1.4/tests/jo.10.sh000066400000000000000000000000711370462052600143160ustar00rootroot00000000000000# array: simple ${JO:-jo} -a spring summer autumn winter jo-1.4/tests/jo.11.exp000066400000000000000000000000641370462052600145030ustar00rootroot00000000000000[true,false,null,"\"true\"","\"false\"","\"null\""] jo-1.4/tests/jo.11.sh000066400000000000000000000001461370462052600143220ustar00rootroot00000000000000# array: true,false,null (native and string) ${JO:-jo} -a true false null '"true"' '"false"' '"null"' jo-1.4/tests/jo.12.exp000066400000000000000000000002201370462052600144760ustar00rootroot00000000000000{ "type": "location", "cog": 120, "t": "u", "lat": 48.85833, "lon": 2.29513, "acc": 5, "tid": "JJ", "tst": 1457767154 } jo-1.4/tests/jo.12.sh000066400000000000000000000001521370462052600143200ustar00rootroot00000000000000# object: geo ${JO:-jo} -p type=location cog=120 t=u lat=48.85833 lon=2.29513 acc=5 tid=JJ tst=1457767154 jo-1.4/tests/jo.13.exp000066400000000000000000000253451370462052600145160ustar00rootroot00000000000000{ "name": "This is jo", "px": [ 300, 300 ], "face": "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", "meta": { "width": 300, "height": 300, "bytes": 8082, "type": "png", "creator": "JP" } } jo-1.4/tests/jo.13.sh000066400000000000000000000003041370462052600143200ustar00rootroot00000000000000# object: face card jo logo ${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 jo-1.4/tests/jo.14.exp000066400000000000000000000000371370462052600145060ustar00rootroot00000000000000{"form":"=ok","this":"==sure"} jo-1.4/tests/jo.14.sh000066400000000000000000000001031370462052600143160ustar00rootroot00000000000000# values: with equals signs in them ${JO:-jo} form==ok this===sure jo-1.4/tests/jo.15.exp000066400000000000000000000000511370462052600145030ustar00rootroot00000000000000{"name":"Jane","obj":["eX","whY","Zed"]} jo-1.4/tests/jo.15.sh000066400000000000000000000002041370462052600143210ustar00rootroot00000000000000# object from file tmp=/tmp/jo.$$ trap "rm -f $tmp; exit" 0 1 2 15 ${JO:-jo} -a eX whY Zed > $tmp ${JO:-jo} name=Jane obj:=$tmp jo-1.4/tests/jo.16.exp000066400000000000000000000000441370462052600145060ustar00rootroot00000000000000{"msg":"\"All's Well\", she said."} jo-1.4/tests/jo.16.sh000066400000000000000000000001611370462052600143240ustar00rootroot00000000000000# quotes in quotes tmp=/tmp/jo.$$ trap "rm -f $tmp; exit" 0 1 2 15 ${JO:-jo} msg='"All'\''s Well", she said.' jo-1.4/tests/jo.17.exp000066400000000000000000000015221370462052600145110ustar00rootroot00000000000000{"s":"","n":0,"b":false,"a":null} {"s":"string","n":6,"b":true,"a":"string"} {"s":"\"quoted\"","n":8,"b":true,"a":"\"quoted\""} {"s":"12345","n":12345,"b":true,"a":12345} {"s":"true","n":1,"b":true,"a":true} {"s":"false","n":0,"b":false,"a":false} {"s":"","n":0,"b":false,"a":null} ["123",14,true,456] ["-s",2,true] ["--test","--toast"] {"--test":"--toast"} [true,"--toast","--test","--toast",6,"--toast"] {"s":false,"n":false,"b":false,"a":false} {"s":true,"n":true,"b":true,"a":true} {"s":"Jan-Piet Mens ","n":"Jan-Piet Mens ","b":"Jan-Piet Mens ","a":"Jan-Piet Mens "} {"s":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","n":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","b":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K","a":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} jo-1.4/tests/jo.17.sh000066400000000000000000000017431370462052600143340ustar00rootroot00000000000000# type coercion # coerce key=val for v in "" string \"quoted\" 12345 true false null; do ${JO:-jo} -- -s s="$v" -n n="$v" -b b="$v" a="$v" done # coerce array items ${JO:-jo} -a -- -s 123 -n "This is a test" -b C_Rocks 456 # coercion flag strings should be usable as inputs, when they aren't flags ${JO:-jo} -a -- -s -s -n -n -b -b # non-flag strings should be read as normal strings, even if they begin with "-" ${JO:-jo} -a -- --test --toast ${JO:-jo} -- --test=--toast # coercion is one-shot, so all "--toast" strings are normal input ${JO:-jo} -a -- -b --test --toast -s --test --toast -n --test --toast ### These should NOT be coerced # @ booleans for v in 0 1; do ${JO:-jo} -- -s s@"$v" -n n@"$v" -b b@"$v" a@"$v" done # @/% file inclusions ${JO:-jo} -- -s s=@${srcdir:=.}/AUTHORS -n n=@${srcdir:=.}/AUTHORS -b b=@${srcdir:=.}/AUTHORS a=@${srcdir:=.}/AUTHORS ${JO:-jo} -- -s s=%${srcdir:=.}/AUTHORS -n n=%${srcdir:=.}/AUTHORS -b b=%${srcdir:=.}/AUTHORS a=%${srcdir:=.}/AUTHORS jo-1.4/tests/jo.18.exp000066400000000000000000000003241370462052600145110ustar00rootroot00000000000000{"a.b":0,"a.c.d":1,"a.d.e":[2,"sam"],"a.c":{"f":true},"b.e":["hi"]} {"a":{"b":0,"c":{"d":1,"f":true},"d":{"e":[2,"sam"]}},"b":{"e":["hi"]}} {"a":{"b":0,"c":{"d":1,"f":true},"d":{"e":[2,"sam"]}},"b":{"e":["hi"]}} jo-1.4/tests/jo.18.sh000066400000000000000000000005271370462052600143340ustar00rootroot00000000000000# nested objects with user-specified delimiter # without delimiter ${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 # with delimiter ${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 # with more complex delimiter ${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 jo-1.4/tests/jo.19.exp000066400000000000000000000000301370462052600145040ustar00rootroot00000000000000{"foo":["hello world"]} jo-1.4/tests/jo.19.sh000066400000000000000000000000741370462052600143320ustar00rootroot00000000000000# read from pipe echo '["hello world"]' | ${JO:-jo} foo:=- jo-1.4/tests/jo.20.exp000066400000000000000000000000521370462052600145000ustar00rootroot00000000000000[{"a":1,"b":"val"},{"a":478,"b":"other"}] jo-1.4/tests/jo.20.sh000066400000000000000000000002211370462052600143140ustar00rootroot00000000000000# read json array elements echo '{"a":1,"b":"val"}' > $$.1 echo '{"a":478,"b":"other"}' > $$.2 ${JO:-jo} -a :$$.1 :$$.2 rm -f $$.1 rm -f $$.2 jo-1.4/tests/jo.21.exp000066400000000000000000000000351370462052600145020ustar00rootroot00000000000000{"nested":{"a":1,"b":"val"}} jo-1.4/tests/jo.21.sh000066400000000000000000000001411370462052600143160ustar00rootroot00000000000000# read nested json elements echo '{"a":1,"b":"val"}' > $$.1 ${JO:-jo} nested=:$$.1 rm -f $$.1 jo-1.4/tests/jo.22.exp000066400000000000000000000000251370462052600145020ustar00rootroot00000000000000{"key":"@timestamp"} jo-1.4/tests/jo.22.sh000066400000000000000000000001061370462052600143200ustar00rootroot00000000000000# avoid reading from file if escaped \@ ${JO:-jo} key="\@timestamp" jo-1.4/tests/jo.test000077500000000000000000000020061370462052600144470ustar00rootroot00000000000000#!/bin/sh # input file (jo.??.in) is a shell script; the first line must # be a comment and is used as the name of the test: # # # basic logo # $JO -a jo # # the expect file (jo.??.exp) is a file which will be diff'd # against the output of the corresponding .in file: # # ["jo"] # JO="$(pwd)/jo" TESTDIR="${0%/*}" NTESTS=$(expr $(ls $TESTDIR/jo.??.sh 2>/dev/null | wc -l)) export JO export TESTDIR echo "1..$NTESTS" # Number of tests to be executed. n=0 for t in $TESTDIR/jo.??.sh; do n=$(expr $n + 1) input=$t expected="$TESTDIR/$(basename $t .sh).exp" output=$(mktemp /tmp/jo.XXXXXX) title=$(head -1 $input | sed -e 's/^#//') sh $input > $output RC=$? if [ $RC -ne 0 ]; then echo "not ok $n - $title" rm -f $output continue fi # if self-contained test (i.e. no 'expected' file exists) # use the previous RC if test -f "$expected"; then diff "$output" "$expected" > /dev/null RC=$? fi status="ok" if [ $RC -ne 0 ]; then status="not ok" fi echo "$status $n - $title" rm -f $output done