diff --git a/.gitmodules b/.gitmodules index 3fff3b881c..0cb65fbd85 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,8 @@ [submodule "modules/oniguruma"] path = modules/oniguruma url = https://github.com/kkos/oniguruma.git + +[submodule "modules/TinyMT"] + path = modules/TinyMT + url = https://github.com/MersenneTwister-Lab/TinyMT.git + ignore = untracked diff --git a/Makefile.am b/Makefile.am index 6344b4e22d..23bc46eee6 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,13 +5,13 @@ LIBJQ_INCS = src/builtin.h src/bytecode.h src/compile.h \ src/exec_stack.h src/jq_parser.h src/jv_alloc.h src/jv_dtoa.h \ src/jv_unicode.h src/jv_utf8_tables.h src/lexer.l src/libm.h \ src/linker.h src/locfile.h src/opcode_list.h src/parser.y \ - src/util.h + src/util.h src/rand.h LIBJQ_SRC = src/builtin.c src/bytecode.c src/compile.c src/execute.c \ src/jq_test.c src/jv.c src/jv_alloc.c src/jv_aux.c \ src/jv_dtoa.c src/jv_file.c src/jv_parse.c src/jv_print.c \ src/jv_unicode.c src/linker.c src/locfile.c src/util.c \ - ${LIBJQ_INCS} + src/rand.c modules/TinyMT/tinymt/tinymt64.c ${LIBJQ_INCS} ### C build options @@ -117,9 +117,18 @@ if ENABLE_ALL_STATIC jq_LDFLAGS += -all-static endif +### TinyMT + +AM_CFLAGS += -I$(srcdir)/modules/TinyMT/tinymt + +check_PROGRAMS = modules/TinyMT/tinymt/check64 +modules_TinyMT_tinymt_check64_SOURCES = modules/TinyMT/tinymt/check64.c +modules_TinyMT_tinymt_check64_LDADD = modules/TinyMT/tinymt/tinymt64.o + + ### Tests (make check) -TESTS = tests/optionaltest tests/mantest tests/jqtest tests/onigtest tests/shtest tests/utf8test tests/base64test +TESTS = tests/optionaltest tests/mantest tests/jqtest tests/onigtest tests/shtest tests/utf8test tests/base64test tests/mttest TESTS_ENVIRONMENT = NO_VALGRIND=$(NO_VALGRIND) diff --git a/configure.ac b/configure.ac index 9186d00dc5..f021e030db 100644 --- a/configure.ac +++ b/configure.ac @@ -211,6 +211,32 @@ AC_CHECK_MATH_FUNC(y0, [.5]) AC_CHECK_MATH_FUNC(y1, [.5]) AC_CHECK_MATH_FUNC(yn, [1,.5]) +dnl Check random sources +AC_MSG_CHECKING(for the Linux getrandom() syscall) +AC_LINK_IFELSE( +[ +AC_LANG_SOURCE([[ + #include + #include + #include + + int main() { + char buffer; + const size_t buflen = sizeof(buffer); + (void)syscall(SYS_getrandom, &buffer, buflen); + return 0; + } + ]]) +],[have_getrandom_syscall=yes],[have_getrandom_syscall=no]) +AC_MSG_RESULT($have_getrandom_syscall) + +if test "$have_getrandom_syscall" = yes; then + AC_DEFINE(HAVE_GETRANDOM_SYSCALL, 1, + [Define to 1 if the Linux getrandom() syscall is available]) +else + AC_CHECK_FILES("/dev/urandom", [AC_MSG_NOTICE(could not link getrandom falling back to /dev/urandom)]) +fi + dnl Thread local storage have___thread=no AC_MSG_CHECKING(for thread-local storage) diff --git a/docs/content/2.download/default.yml b/docs/content/2.download/default.yml index e9fbdc15c8..1617923211 100644 --- a/docs/content/2.download/default.yml +++ b/docs/content/2.download/default.yml @@ -12,7 +12,8 @@ body: jq is licensed under the MIT license. For all of the gory details, read the file `COPYING` in the source distribution. - + jq uses Mersenne Twister as a fast PRNG, its license is located in the + [copyright](#copyright) section. ### Linux @@ -168,3 +169,40 @@ body: the YAML docs, and you'll still need the Ruby dependencies to build the manpage. + ### Copyright + + jq uses Mersenne Twister for fast pseudorandom number generation, + specifically + [TinyMT](http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/TINYMT/). The + following is the verbatim license: + + Copyright (c) 2011, 2013 Mutsuo Saito, Makoto Matsumoto, + Hiroshima University and The University of Tokyo. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the name of the Hiroshima University nor the names of + its contributors may be used to endorse or promote products + derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/docs/content/3.manual/manual.yml b/docs/content/3.manual/manual.yml index f03efcda97..623b34b505 100644 --- a/docs/content/3.manual/manual.yml +++ b/docs/content/3.manual/manual.yml @@ -258,6 +258,13 @@ sections: Remaining arguments are positional JSON text arguments. These are available to the jq program as `$ARGS.positional[]`. + * `--rand-seed seed-int`: + + Set the seed for randomness generation to anything interpretable as an + unsigned long. If unspecified, they key will be taken from the best + source of randomness (currently /dev/urandom if it's available, and + otherwise the current time `time(NULL)`). + * `--run-tests [filename]`: Runs the tests in the given file or standard input. This must @@ -1329,6 +1336,81 @@ sections: input: '[{"foo":1, "bar":14}, {"foo":2, "bar":3}]' output: ['{"foo":2, "bar":3}'] + - title: "`rand`" + body: | + + Generate a random number greater than or equal to zero and less than + one. + + *NOTE: This function should not be used for security purposes.* + + Also see the command line argument `--rand-seed`. + + examples: + - program: 'rand | . >= 0 and . < 1' + input: 'null' + output: ['true'] + + - title: "`randint`, `randint(b)`, `randint(a; b)`" + body: | + + Generate a random integer greater than or equal to `a` and less + than `b`. `a` is zero when unspecified. The first form reads `b` from + its input. The range must have at least one number in it, e.g. `b` + must be greater than zero if a is unspecified. Inputs will be cast to + an integer, so `randint(5)` and `randint(5.6)` are equivalent. + + *NOTE: This function should not be used for security purposes.* + + Also see the command line argument `--rand-seed`. + + examples: + - program: 'randint | . >= 0 and . < 5' + input: '5' + output: ['true'] + - program: 'randint(-5; 5 + 1) | . >= -5 and . <= 5' + input: 'null' + output: ['true'] + - program: '.[randint(length)] | . == 1 or . == 5 or . == 17' + input: '[1, 5, 17]' + output: ['true'] + + - title: "`shuffle`" + body: | + + Randomly permute an array. + + *NOTE: This function should not be used for security purposes.* + + Also see the command line argument `--rand-seed`. + + examples: + - program: 'shuffle | sort == [1, 5, 17]' + input: '[1, 5, 17]' + output: ['true'] + + - title: "`rand_select(n)`, `rand_select(stream; n)`, `rand_select_rep(n)`" + body: | + + Randomly select `n` elements from an array or stream. The stream + version is significantly slower for large inputs. `rand_select_rep` + does so with repetition. + + *NOTE: This function should not be used for security purposes.* + + Also see the command line argument `--rand-seed`. + + examples: + - program: 'rand_select(2) | map(. == 1 or . == 5 or . == 17) | all' + input: '[1, 5, 17]' + output: ['true'] + - program: 'rand_select(1, 5, 17; 2) | map(. == 1 or . == 5 or . == 17) | all' + input: 'null' + output: ['true'] + - program: 'rand_select_rep(2) | map(. == 1 or . == 5 or . == 17) | all' + input: '[1, 5, 17]' + output: ['true'] + - title: "`unique`, `unique_by(path_exp)`" body: | diff --git a/modules/TinyMT b/modules/TinyMT new file mode 160000 index 0000000000..53206ad2f9 --- /dev/null +++ b/modules/TinyMT @@ -0,0 +1 @@ +Subproject commit 53206ad2f9d09b7696e5b2fcc4ace6650731910c diff --git a/src/builtin.c b/src/builtin.c index c6c8c2ea76..9be49e49af 100644 --- a/src/builtin.c +++ b/src/builtin.c @@ -36,6 +36,7 @@ void *alloca (size_t); #include #include "builtin.h" #include "compile.h" +#include "rand.h" #include "jq_parser.h" #include "bytecode.h" #include "linker.h" @@ -1058,6 +1059,66 @@ static jv f_error(jq_state *jq, jv input, jv msg) { return jv_invalid_with_msg(msg); } +// Random functions +static jv f_rand(jq_state *jq, jv input) { + // Random number in [0, 1) + jv_free(input); + return jv_number(jq_rand_double()); +} + +static jv f_randint(jq_state *jq, jv input) { + // Random int in [0, input) + double max_val = jv_number_value(jv_copy(input)); + if (max_val < 1 || (double)((uint64_t)-1) < max_val) { + return type_error(input, "number invalid, less than 1 or too large"); + } else { + jv_free(input); + return jv_number(jq_rand_int((uint64_t)max_val)); + } +} + +static jv f_shuffle(jq_state *jq, jv input) { + // Shuffle array + if (jv_get_kind(input) != JV_KIND_ARRAY) { + return type_error(input, "cannot be shuffled, as it is not an array"); + } else { + int length = jv_array_length(jv_copy(input)); + for (int i = 0; i < length; ++i) { + int swap = i + (int)jq_rand_int((unsigned long)(length - i)); + jv to_swap = jv_array_get(jv_copy(input), i); + input = jv_array_set(input, i, jv_array_get(jv_copy(input), swap)); + input = jv_array_set(input, swap, to_swap); + } + return input; + } +} + +static jv f_rand_select(jq_state *jq, jv input, jv j_num) { + // Select j_num without replacement from array + // More efficient than `shuffle | slice` + int num = jv_number_value(jv_copy(j_num)); + if (jv_get_kind(input) != JV_KIND_ARRAY) { + return type_error2(input, j_num, "can't select from a non array"); + } else if (num < 0 || num > jv_array_length(jv_copy(input))) { + return type_error2(input, j_num, "can't select less than 0 or more than than the input"); + } else if (num == 0) { + jv_free(input); + jv_free(j_num); + return jv_array(); + } else { + jv result = f_shuffle(jq, jv_array_slice(jv_copy(input), 0, num)); + for (int i = num; i < jv_array_length(jv_copy(input)); ++i) { + if (jq_rand_double() < (double)num / (double)(i + 1)) { + jv selected = jv_array_get(jv_copy(input), i); + result = jv_array_set(result, (int)jq_rand_int((unsigned long)num), selected); + } + } + jv_free(j_num); + jv_free(input); + return result; + } +} + // FIXME Should autoconf check for this! #ifndef WIN32 extern char **environ; @@ -1631,6 +1692,10 @@ static const struct cfunction function_list[] = { {(cfunction_ptr)f_min_by_impl, "_min_by_impl", 2}, {(cfunction_ptr)f_max_by_impl, "_max_by_impl", 2}, {(cfunction_ptr)f_error, "error", 2}, + {(cfunction_ptr)f_rand, "rand", 1}, + {(cfunction_ptr)f_randint, "randint", 1}, + {(cfunction_ptr)f_shuffle, "shuffle", 1}, + {(cfunction_ptr)f_rand_select, "rand_select", 2}, {(cfunction_ptr)f_format, "format", 2}, {(cfunction_ptr)f_env, "env", 1}, {(cfunction_ptr)f_halt, "halt", 1}, diff --git a/src/builtin.jq b/src/builtin.jq index b0e8775ac5..e2d5891055 100644 --- a/src/builtin.jq +++ b/src/builtin.jq @@ -306,3 +306,26 @@ def JOIN($idx; stream; idx_expr; join_expr): stream | [., $idx[idx_expr]] | join_expr; def IN(s): reduce (first(select(. == s)) | true) as $v (false; if . or $v then true else false end); def IN(src; s): reduce (src|IN(s)) as $v (false; if . or $v then true else false end); + +# Random aliases +def randint(upper): upper | randint; +def randint(lower; upper): (lower | floor) as $a + | (upper | floor) as $b + | if $b <= $a + then error("randint upper limit must be greater than the lower \($b) <= \($a)") + else $b - $a | randint + $a + end; +def rand_select_rep(n): if n < 0 + then error("\(type) (\(.)) and \(n | type) (\(n)) can't select less than 0 from the input") + else . as $array | [range(n) | $array[$array | length | randint]] + end; +def rand_select(stream; $n): + if $n < 0 then error("rand_select can't select less than 0 elements") + elif $n == 0 then [] + else reduce stream as $x ([0, []]; .[0] += 1 + | if .[0] <= $n then .[1] += [$x] + elif rand < $n / .[0] then .[1][randint($n)] = $x + else . + end) + | .[1] | shuffle + end; diff --git a/src/main.c b/src/main.c index a7f50b5135..bdf679653f 100644 --- a/src/main.c +++ b/src/main.c @@ -2,8 +2,10 @@ #include #include #include -#include +#include +#include #include +#include #include #include @@ -31,6 +33,7 @@ #include "jq.h" #include "jv_alloc.h" #include "util.h" +#include "rand.h" #include "src/version.h" int jq_testsuite(jv lib_dirs, int verbose, int argc, char* argv[]); @@ -80,6 +83,7 @@ static void usage(int code, int keep_it_short) { " -M monochrome (don't colorize JSON);\n" " -S sort keys of objects on output;\n" " --tab use tabs for indentation;\n" + " --rand-seed r set the random seed for various random functions to ;\n" " --arg a v set variable $a to value ;\n" " --argjson a v set variable $a to JSON value ;\n" " --slurpfile a f set variable $a to an array of JSON texts read from ;\n" @@ -140,8 +144,9 @@ enum { EXIT_STATUS_EXACT = 8192, SEQ = 16384, RUN_TESTS = 32768, + USER_DEFINED_SEED = 65536, /* debugging only */ - DUMP_DISASM = 65536, + DUMP_DISASM = 131072, }; static int options = 0; @@ -238,6 +243,7 @@ int main(int argc, char* argv[]) { int parser_flags = 0; int nfiles = 0; int badwrite; + uint64_t random_seed = 0; jv ARGS = jv_array(); /* positional arguments */ jv program_arguments = jv_object(); /* named arguments */ @@ -405,6 +411,21 @@ int main(int argc, char* argv[]) { options |= EXIT_STATUS; if (!short_opts) continue; } + if (isoption(argv[i], 0, "rand-seed", &short_opts)) { + if (i+1 >= argc) { + fprintf(stderr, "%s: --rand-seed takes an argument\n", progname); + die(); + } + char *parsed; + random_seed = strtoul(argv[i+1], &parsed, 0); + if (parsed == argv[i+1] || *parsed != 0) { + fprintf(stderr, "%s: --rand-seed takes an unsigned long, got '%s'\n", progname, argv[i+1]); + die(); + } + options |= USER_DEFINED_SEED; + i++; + if (!short_opts) continue; + } // FIXME: For --arg* we should check that the varname is acceptable if (isoption(argv[i], 0, "args", &short_opts)) { further_args_are_strings = 1; @@ -528,6 +549,13 @@ int main(int argc, char* argv[]) { if (getenv("JQ_COLORS") != NULL && !jq_set_colors(getenv("JQ_COLORS"))) fprintf(stderr, "Failed to set $JQ_COLORS\n"); + // Randomness + if (options & USER_DEFINED_SEED) { + jq_rand_init_seed(random_seed); + } else { + jq_rand_init(); + } + if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) { // Default search path list lib_search_paths = JV_ARRAY(jv_string("~/.jq"), diff --git a/src/rand.c b/src/rand.c new file mode 100644 index 0000000000..6f2a9c2698 --- /dev/null +++ b/src/rand.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include "rand.h" +#include "tinymt64.h" + +// TODO Add support for cryptographically secure PRNG +// TODO Add Windows CNG + +static tinymt64_t fast_rand; + +#if HAVE_GETRANDOM_SYSCALL +// ------------------- +// Modern linux kernel +// ------------------- +// Use getrandom syscall which has some advantages over /dev/urandom + +#include +#include +#include + +void jq_rand_init() { + uint64_t seed; + if (syscall(SYS_getrandom, &seed, sizeof(seed), 0) != sizeof(seed)) { + fprintf(stderr, "error: could not get data from random pool.\n"); + exit(2); + } + tinymt64_init(&fast_rand, seed); +} + +#elif HAVE__DEV_URANDOM +// ------------------- +// Unix machine +// ------------------- +// Use /dev/urandom as fallback + +void jq_rand_init() { + uint64_t seed; + FILE *devur = fopen("/dev/urandom", "r"); + if (fread(&seed, sizeof(seed), 1, devur) != 1) { + fprintf(stderr, "error: could not get data from /dev/urandom.\n"); + exit(2); + } + if (fclose(devur)) { + fprintf(stderr, "error: could not close /dev/urandom.\n"); + exit(2); + } + tinymt64_init(&fast_rand, seed); +} + +#else +// -------- +// Fallback +// -------- +// Always use MT, seeded with time if no better randomness source exists + +#include + +void jq_rand_init() { + tinymt64_init(&fast_rand, time(NULL)); +} + +#endif + +// Common functions independent of randomness source +void jq_rand_init_seed(uint64_t seed) { + tinymt64_init(&fast_rand, seed); +} + +uint64_t jq_rand_int(uint64_t max_val) { + uint64_t thresh = -1 - ((-1 % max_val) + 1) % max_val; + uint64_t result; + while (thresh < (result = tinymt64_generate_uint64(&fast_rand))); // Efficient rejection sample + return result % max_val; +} + +double jq_rand_double() { + return tinymt64_generate_double01(&fast_rand); +} + +// vim: set shiftwidth=2: diff --git a/src/rand.h b/src/rand.h new file mode 100644 index 0000000000..e93148771f --- /dev/null +++ b/src/rand.h @@ -0,0 +1,20 @@ +#ifndef RAND_H +#define RAND_H + +#include + +// Configures initial random generation +void jq_rand_init(); + +// Sets seed and instructs rand to use pseudorandom generation +void jq_rand_init_seed(uint64_t); + +// Return a random int in [0, max_val) +uint64_t jq_rand_int(uint64_t); + +// Return a random double in [0, 1) +double jq_rand_double(); + +#endif + +// vim: set shiftwidth=2: diff --git a/tests/jq.test b/tests/jq.test index 12d2b4dba0..e2f0a959e5 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -1247,6 +1247,84 @@ bsearch(4) [1,2,3] -4 +# Random function tests +[range(1000) | rand | . >= 0 and . < 1] | all +null +true + +try randint catch . +0 +"number (0) number invalid, less than 1 or too large" + +try randint(-3) catch . +null +"number (-3) number invalid, less than 1 or too large" + +# If this didn't floor each argument first, it would error +randint(0.8; 1.2) +null +0 + +try randint(4; 4) catch . +null +"randint upper limit must be greater than the lower 4 <= 4" + +[range(1000) | randint(10) | . >= 0 and . < 10] | all +null +true + +try shuffle catch . +null +"null (null) cannot be shuffled, as it is not an array" + +[range(1000) | [range(5)] | shuffle | sort == [range(length)]] | all +null +true + +try rand_select(-1) catch . +[] +"array ([]) and number (-1) can't select less than 0 or more than than the input" + +try rand_select(null; -1) catch . +null +"rand_select can't select less than 0 elements" + +try rand_select(4) catch . +[1, 2, 3] +"array ([1,2,3]) and number (4) can't select less than 0 or more than than the input" + +try rand_select(0) catch . +null +"null (null) and number (0) can't select from a non array" + +rand_select(0) +[1, 2, 3, 4] +[] + +rand_select(1, 2, 3, 4; 0) +null +[] + +rand_select(1, 2, 3, 4; 4) | sort +null +[1, 2, 3, 4] + +[range(1000) | [range(5)] | rand_select(2) | length == 2 and .[0] != .[1]] | all +null +true + +rand_select_rep(0) +[1, 2, 3, 4] +[] + +try rand_select_rep(-1) catch . +[1, 3, 6] +"array ([1,3,6]) and number (-1) can't select less than 0 from the input" + +[range(1000) | [range(5)] | rand_select_rep(2) | length == 2 and (map(. >= 0 and . < 5) | all)] | all +null +true + # strptime tests are in optional.test strftime("%Y-%m-%dT%H:%M:%SZ") @@ -1369,6 +1447,7 @@ jq: error: syntax error, unexpected INVALID_CHARACTER, expecting $end (Unix shel (.[{}] = 0)? null + INDEX(range(5)|[., "foo\(.)"]; .[0]) null {"0":[0,"foo0"],"1":[1,"foo1"],"2":[2,"foo2"],"3":[3,"foo3"],"4":[4,"foo4"]} diff --git a/tests/mttest b/tests/mttest new file mode 100755 index 0000000000..53236f7079 --- /dev/null +++ b/tests/mttest @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -e -o pipefail + +. "${0%/*}/setup" "$@" +cd "$JQBASEDIR/modules/TinyMT/tinymt" +head -n1 check64.out.txt | cut -d' ' -f 2-4,7 | xargs ./check64 | cmp - check64.out.txt diff --git a/tests/shtest b/tests/shtest index af1b159eb8..348ac7c619 100755 --- a/tests/shtest +++ b/tests/shtest @@ -298,4 +298,28 @@ JQ_COLORS="0123456789123:0123456789123:0123456789123:0123456789123:0123456789123 cmp $d/color $d/expect cmp $d/warning $d/expect_warning +# Test that random seed produces identical output +# To verify it's not identical clock time we need to put a sleep in, which sucks. +# It's also why the tests are done in "parallel" +$VALGRIND $Q $JQ --rand-seed 1234 -n 'range(5) | rand' >$d/rand1 +$VALGRIND $Q $JQ --rand-seed 1234 -n 'range(1;6) | randint' >$d/randint1 +$VALGRIND $Q $JQ --rand-seed 1234 -n '[range(5)] | shuffle' >$d/shuffle1 +$VALGRIND $Q $JQ --rand-seed 1234 -n '[range(5)] | rand_select(3)' >$d/select1 + +sleep 1.5 + +$VALGRIND $Q $JQ --rand-seed 1234 -n 'range(5) | rand' >$d/rand2 +$VALGRIND $Q $JQ --rand-seed 1234 -n 'range(1;6) | randint' >$d/randint2 +$VALGRIND $Q $JQ --rand-seed 1234 -n '[range(5)] | shuffle' >$d/shuffle2 +$VALGRIND $Q $JQ --rand-seed 1234 -n '[range(5)] | rand_select(3)' >$d/select2 + +cmp $d/rand1 $d/rand2 +cmp $d/randint1 $d/randint2 +cmp $d/shuffle1 $d/shuffle2 +cmp $d/select1 $d/select2 + +# Test that rand seed fails for bad inputs +! $VALGRIND $Q $JQ --rand-seed '' -n '.' 2>/dev/null +! $VALGRIND $Q $JQ --rand-seed a -n '.' 2>/dev/null + exit 0