diff --git a/Makefile.am b/Makefile.am index eaffe24..c814070 100644 --- a/Makefile.am +++ b/Makefile.am @@ -12,12 +12,12 @@ if USE_PANDOC jo.1: jo.pandoc @test -n "$(PANDOC)" || \ { echo 'pandoc' not found during configure.; exit 1; } - $(PANDOC) -s -w man -o $@ $< + $(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 -o $@ $< + $(PANDOC) -s -w markdown+simple_tables -o $@ $< endif diff --git a/jo.1 b/jo.1 index f44c199..8e68885 100644 --- a/jo.1 +++ b/jo.1 @@ -1,4 +1,5 @@ -.\" Automatically generated by Pandoc 1.16.0.2 +.\"t +.\" Automatically generated by Pandoc 1.19.2.1 .\" .TH "JO" "1" "" "User Manuals" "" .hy @@ -7,7 +8,7 @@ jo \- JSON output from a shell .SH SYNOPSIS .PP -jo [\-p] [\-a] [\-B] [\-v] [\-V] [word ...] +jo [\-p] [\-a] [\-B] [\-v] [\-V] [\-\-] [ [\-s|\-n|\-b] word ...] .SH DESCRIPTION .PP \f[I]jo\f[] creates a JSON string on \f[I]stdout\f[] from _word_s given @@ -32,6 +33,119 @@ specified. When the \f[C]:=\f[] operator is used in a \f[I]word\f[], the name to the right of \f[C]:=\f[] is a file containing JSON which is parsed and assigned to the key left of the operator. +.SH TYPE COERCION +.PP +\f[I]jo\f[]\[aq]s type guesses can be overridden on a per\-word basis by +prefixing \f[I]word\f[] with \f[C]\-s\f[] for \f[I]string\f[], +\f[C]\-n\f[] for \f[I]number\f[], or \f[C]\-b\f[] for \f[I]boolean\f[]. +The list of _word_s \f[I]must\f[] be prefixed with \f[C]\-\-\f[], to +indicate to \f[I]jo\f[] 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{ +"a":"" +T}@T{ +"a":0 +T}@T{ +"a":false +T}@T{ +"a":null +T} +T{ +a=string +T}@T{ +"a":"string" +T}@T{ +"a":6 +T}@T{ +"a":true +T}@T{ +"a":"string" +T} +T{ +a="quoted" +T}@T{ +"a":""quoted"" +T}@T{ +"a":8 +T}@T{ +"a":true +T}@T{ +"a":""quoted"" +T} +T{ +a=12345 +T}@T{ +"a":"12345" +T}@T{ +"a":12345 +T}@T{ +"a":true +T}@T{ +"a":12345 +T} +T{ +a=true +T}@T{ +"a":"true" +T}@T{ +"a":1 +T}@T{ +"a":true +T}@T{ +"a":true +T} +T{ +a=false +T}@T{ +"a":"false" +T}@T{ +"a":0 +T}@T{ +"a":false +T}@T{ +"a":false +T} +T{ +a=null +T}@T{ +"a":"" +T}@T{ +"a":0 +T}@T{ +"a":false +T}@T{ +"a":null +T} +.TE +.PP +Coercing a non\-number string to number outputs the \f[I]length\f[] of +the string. +.PP +Coercing a non\-boolean string to boolean outputs \f[C]false\f[] if the +string is empty, \f[C]true\f[] otherwise. +.PP +Type coercion only applies to \f[C]key=value\f[] words, and individual +words in a \f[C]\-a\f[] array. +Coercing other words has no effect. .SH EXAMPLES .PP Create an object. @@ -127,6 +241,27 @@ $\ jo\ \-p\ name=Jane\ point[]=1\ point[]=2\ geo[lat]=10\ geo[lon]=20 \f[] .fi .PP +Type coercion: +.IP +.nf +\f[C] +$\ 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] +\f[] +.fi +.PP Read element values from files: a value which starts with \f[C]\@\f[] is read in plain whereas if it begins with a \f[C]%\f[] it will be base64\-encoded: @@ -153,7 +288,7 @@ $\ jo\ files:=child.json .fi .SH OPTIONS .PP -\f[I]jo\f[] understands the following options. +\f[I]jo\f[] understands the following global options. .TP .B \-a Interpret the list of \f[I]words\f[] as array values and produce an diff --git a/jo.c b/jo.c index 4b06654..87a496b 100644 --- a/jo.c +++ b/jo.c @@ -35,6 +35,7 @@ #define FLAG_PRETTY 0x02 #define FLAG_NOBOOL 0x04 #define FLAG_BOOLEAN 0x08 +#define FLAG_MASK (FLAG_ARRAY | FLAG_PRETTY | FLAG_NOBOOL | FLAG_BOOLEAN) static JsonNode *pile; /* pile of nested objects/arrays */ @@ -45,6 +46,14 @@ static JsonNode *pile; /* pile of nested objects/arrays */ # define ftello ftell #endif +JsonTag flags_to_tag(int flags) { + return flags / (FLAG_MASK + 1); +} + +int tag_to_flags(JsonTag tag) { + return (FLAG_MASK + 1) * tag; +} + void json_copy_to_object(JsonNode * obj, JsonNode * object_or_array, int clobber) { JsonNode *node; @@ -105,6 +114,72 @@ char *slurp_file(FILE *fp, size_t *out_len, bool fold_newlines) 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. @@ -112,8 +187,10 @@ char *slurp_file(FILE *fp, size_t *out_len, bool fold_newlines) JsonNode *vnode(char *str, int flags) { + JsonTag type = flags_to_tag(flags); + if (strlen(str) == 0) { - return json_mknull(); + return jo_mknull(type); } /* If str begins with a double quote, keep it a string */ @@ -126,23 +203,23 @@ JsonNode *vnode(char *str, int flags) *bp = 0; /* Chop closing double quote */ return json_mkstring(str + 1); #endif - return json_mkstring(str); + return jo_mkstring(str, type); } char *endptr; double num = strtod(str, &endptr); if (!*endptr && isfinite(num)) { - return json_mknumber(num); + return jo_mknumber(str, type); } if (!(flags & FLAG_NOBOOL)) { if (strcmp(str, "true") == 0) { - return json_mkbool(true); + return jo_mkbool(true, type); } else if (strcmp(str, "false") == 0) { - return json_mkbool(false); + return jo_mkbool(false, type); } else if (strcmp(str, "null") == 0) { - return json_mknull(); + return jo_mknull(type); } } @@ -196,7 +273,7 @@ JsonNode *vnode(char *str, int flags) return (obj); } - return json_mkstring(str); + return jo_mkstring(str, type); } /* @@ -523,9 +600,28 @@ int main(int argc, char **argv) } } else { while ((kv = *argv++)) { - p = utf8_from_locale(kv, -1); - append_kv(json, flags, p); - utf8_free(p); + if (kv[0] == '-') { + /* Set one-shot coerce flag */ + switch (kv[1]) { + case 'b': + flags |= tag_to_flags(JSON_BOOL); + break; + case 's': + flags |= tag_to_flags(JSON_STRING); + break; + case 'n': + flags |= tag_to_flags(JSON_NUMBER); + break; + default: + exit(usage(progname)); + } + } else { + p = utf8_from_locale(kv, -1); + append_kv(json, flags, p); + utf8_free(p); + /* Reset any one-shot coerce flags */ + flags &= FLAG_MASK; + } } } diff --git a/jo.md b/jo.md index 2643573..c70cdf5 100644 --- a/jo.md +++ b/jo.md @@ -1,3 +1,7 @@ +--- +title: 'JO(1) User Manuals' +--- + NAME ==== @@ -6,12 +10,12 @@ jo - JSON output from a shell SYNOPSIS ======== -jo [-p] [-a] [-B] [-v] [-V] [word ...] +jo \[-p\] \[-a\] \[-B\] \[-v\] \[-V\] \[--\] \[ \[-s|-n|-b\] word ...\] DESCRIPTION =========== -*jo* creates a JSON string on *stdout* from *word*s given it as +*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 @@ -25,6 +29,39 @@ 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. + +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 ======== @@ -99,6 +136,23 @@ an array called *point* and an object named *geo*: } } +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: @@ -108,10 +162,17 @@ plain whereas if it begins with a `%` it will be base64-encoded: $ jo filename=AUTHORS content=%AUTHORS {"filename":"AUTHORS","content":"SmFuLVBpZXQgTWVucyA8anBtZW5zQGdtYWlsLmNvbT4K"} +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" .... + OPTIONS ======= -*jo* understands the following options. +*jo* understands the following global options. -a : Interpret the list of *words* as array values and produce an array @@ -182,4 +243,3 @@ AUTHOR ====== Jan-Piet Mens - diff --git a/jo.pandoc b/jo.pandoc index d34296c..a62a9ee 100644 --- a/jo.pandoc +++ b/jo.pandoc @@ -6,7 +6,7 @@ jo - JSON output from a shell # SYNOPSIS -jo [-p] [-a] [-B] [-v] [-V] [word ...] +jo [-p] [-a] [-B] [-v] [-V] [--] [ [-s|-n|-b] word ...] # DESCRIPTION @@ -23,6 +23,31 @@ empty value behind the colon results in a `null` JSON element. 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. +# 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: @@ -90,6 +115,23 @@ Elements (objects and arrays) can be nested. The following example nests an arra } } +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: $ jo program=jo authors=@AUTHORS @@ -106,7 +148,7 @@ Read element values from a file in order to overcome ARG_MAX limits during objec # OPTIONS -*jo* understands the following options. +*jo* understands the following global options. -a : Interpret the list of _words_ as array values and produce an array instead of diff --git a/tests/jo.17.exp b/tests/jo.17.exp new file mode 100644 index 0000000..b61d6ae --- /dev/null +++ b/tests/jo.17.exp @@ -0,0 +1,12 @@ +{"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":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"} diff --git a/tests/jo.17.sh b/tests/jo.17.sh new file mode 100644 index 0000000..28b9855 --- /dev/null +++ b/tests/jo.17.sh @@ -0,0 +1,20 @@ +# 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 + +### 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