mirror of
https://github.com/stedolan/jq.git
synced 2024-05-11 05:55:39 +00:00
With this patch, jq run-time errors printed to stderr will contain the
filename and line of the offending input.
But note that the JSON text parser does (yet) print the input filename
on parse error. A jq run-time error is a jq program error, not
including syntax errors nor JSON text input format errors.
Examples:
With stdin and multiple lines:
$ printf '{"a":43}\n{"a":{"b":66}}\n' | ./jq '.a+1'
44
jq: error (at stdin:2): object and number cannot be added
With multiple files:
$ printf '{"a":43}' > 1.json
$ printf '{"a":"hello"}\n' > 2.json
$ printf '{"a":{"b":66}}\n' > 3.json
$ ./jq '[.a]|@tsv' 1.json 2.json 3.json
"43"
"hello"
jq: error (at 3.json:1): object is not valid in a csv row
With very long lines (spanning multiple `fgets` calls):
$ ( printf '{"a":43}\n' ;
printf '{"a":{"b":[' ; seq 10000 | paste -d, -s | tr -d '\n' ;
printf ']}}\n' ;
printf '{"a":"hello"}\n' ) | ./jq '[.a] | @tsv'
"43"
jq: error (at stdin:2): object is not valid in a csv row
"hello"
With raw input:
$ seq 1000 | ./jq --raw-input 'select(.=="700") | . + 10'
jq: error (at stdin:700): string and number cannot be added
Caveat:
The reported line will be the last line of the (valid) parsed JSON data.
Example:
$ printf '{\n"a":\n"hello"\n\n\n}\n' | ./jq '.a+4'
jq: error (at stdin:6): string and number cannot be added
502 lines
16 KiB
C
502 lines
16 KiB
C
#include <assert.h>
|
|
#include <ctype.h>
|
|
#include <errno.h>
|
|
#include <libgen.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include "compile.h"
|
|
#include "jv.h"
|
|
#include "jq.h"
|
|
#include "jv_alloc.h"
|
|
#include "util.h"
|
|
#include "version.h"
|
|
|
|
int jq_testsuite(jv lib_dirs, int argc, char* argv[]);
|
|
|
|
static const char* progname;
|
|
|
|
/*
|
|
* For a longer help message we could use a better option parsing
|
|
* strategy, one that lets stack options.
|
|
*/
|
|
static void usage(int code) {
|
|
FILE *f = stderr;
|
|
|
|
if (code == 0)
|
|
f = stdout;
|
|
|
|
#ifdef BAIL
|
|
#undef BAIL
|
|
#endif
|
|
#define BAIL(expr) do { if ((expr) < 0 && code == 0) code = 2; } while (0)
|
|
BAIL(fprintf(f, "\njq - commandline JSON processor [version %s]\n", JQ_VERSION));
|
|
BAIL(fprintf(f, "Usage: %s [options] <jq filter> [file...]\n\n", progname));
|
|
BAIL(fprintf(f, "\tjq is a tool for processing JSON inputs, applying the\n"));
|
|
BAIL(fprintf(f, "\tgiven filter to its JSON text inputs and producing the\n"));
|
|
BAIL(fprintf(f, "\tfilter's results as JSON on standard output.\n"));
|
|
BAIL(fprintf(f, "\tThe simplest filter is ., which is the identity filter,\n"));
|
|
BAIL(fprintf(f, "\tcopying jq's input to its output.\n"));
|
|
BAIL(fprintf(f, "\tFor more advanced filters see the jq(1) manpage (\"man jq\")\n"));
|
|
BAIL(fprintf(f, "\tand/or http://stedolan.github.com/jq\n\n"));
|
|
BAIL(fprintf(f, "\tSome of the options include:\n"));
|
|
BAIL(fprintf(f, "\t -h\t\tthis message;\n"));
|
|
BAIL(fprintf(f, "\t -c\t\tcompact instead of pretty-printed output;\n"));
|
|
BAIL(fprintf(f, "\t -n\t\tuse `null` as the single input value;\n"));
|
|
BAIL(fprintf(f, "\t -s\t\tread (slurp) all inputs into an array; apply filter to it;\n"));
|
|
BAIL(fprintf(f, "\t -r\t\toutput raw strings, not JSON texts;\n"));
|
|
BAIL(fprintf(f, "\t -R\t\tread raw strings, not JSON texts;\n"));
|
|
BAIL(fprintf(f, "\t --arg a v\tset variable $a to value <v>;\n"));
|
|
BAIL(fprintf(f, "\t --argjson a v\tset variable $a to JSON value <v>;\n"));
|
|
BAIL(fprintf(f, "\t --argfile a f\tset variable $a to JSON texts read from <f>;\n"));
|
|
BAIL(fprintf(f, "\tSee the manpage for more options.\n"));
|
|
exit(code);
|
|
}
|
|
|
|
static void die() {
|
|
fprintf(stderr, "Use %s --help for help with command-line options,\n", progname);
|
|
fprintf(stderr, "or see the jq manpage, or online docs at http://stedolan.github.com/jq\n");
|
|
exit(2);
|
|
}
|
|
|
|
|
|
|
|
|
|
static int isoptish(const char* text) {
|
|
return text[0] == '-' && (text[1] == '-' || isalpha(text[1]));
|
|
}
|
|
|
|
static int isoption(const char* text, char shortopt, const char* longopt, size_t *short_opts) {
|
|
if (text[0] != '-' || text[1] == '-')
|
|
*short_opts = 0;
|
|
if (text[0] != '-') return 0;
|
|
|
|
// check long option
|
|
if (text[1] == '-' && !strcmp(text+2, longopt)) return 1;
|
|
else if (text[1] == '-') return 0;
|
|
|
|
// must be short option; check it and...
|
|
if (!shortopt) return 0;
|
|
if (strchr(text, shortopt) != NULL) {
|
|
(*short_opts)++; // ...count it (for option stacking)
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
enum {
|
|
SLURP = 1,
|
|
RAW_INPUT = 2,
|
|
PROVIDE_NULL = 4,
|
|
RAW_OUTPUT = 8,
|
|
COMPACT_OUTPUT = 16,
|
|
ASCII_OUTPUT = 32,
|
|
COLOUR_OUTPUT = 64,
|
|
NO_COLOUR_OUTPUT = 128,
|
|
SORTED_OUTPUT = 256,
|
|
FROM_FILE = 512,
|
|
RAW_NO_LF = 1024,
|
|
UNBUFFERED_OUTPUT = 2048,
|
|
EXIT_STATUS = 4096,
|
|
SEQ = 8192,
|
|
RUN_TESTS = 16384,
|
|
/* debugging only */
|
|
DUMP_DISASM = 32768,
|
|
};
|
|
static int options = 0;
|
|
|
|
static int process(jq_state *jq, jv value, int flags, int dumpopts) {
|
|
int ret = 14; // No valid results && -e -> exit(4)
|
|
jq_start(jq, value, flags);
|
|
jv result;
|
|
while (jv_is_valid(result = jq_next(jq))) {
|
|
if ((options & RAW_OUTPUT) && jv_get_kind(result) == JV_KIND_STRING) {
|
|
fwrite(jv_string_value(result), 1, jv_string_length_bytes(jv_copy(result)), stdout);
|
|
ret = 0;
|
|
jv_free(result);
|
|
} else {
|
|
if (jv_get_kind(result) == JV_KIND_FALSE || jv_get_kind(result) == JV_KIND_NULL)
|
|
ret = 11;
|
|
else
|
|
ret = 0;
|
|
if (options & SEQ)
|
|
fwrite("\036", 1, 1, stdout);
|
|
jv_dump(result, dumpopts);
|
|
}
|
|
if (!(options & RAW_NO_LF))
|
|
printf("\n");
|
|
if (options & UNBUFFERED_OUTPUT)
|
|
fflush(stdout);
|
|
}
|
|
if (jv_invalid_has_msg(jv_copy(result))) {
|
|
// Uncaught jq exception
|
|
jv msg = jv_invalid_get_msg(jv_copy(result));
|
|
jv input_pos = jq_util_input_get_position(jq);
|
|
if (jv_get_kind(msg) == JV_KIND_STRING) {
|
|
fprintf(stderr, "jq: error (at %s): %s\n",
|
|
jv_string_value(input_pos), jv_string_value(msg));
|
|
} else {
|
|
msg = jv_dump_string(msg, 0);
|
|
fprintf(stderr, "jq: error (at %s) (not a string): %s\n",
|
|
jv_string_value(input_pos), jv_string_value(msg));
|
|
}
|
|
ret = 5;
|
|
jv_free(input_pos);
|
|
jv_free(msg);
|
|
}
|
|
jv_free(result);
|
|
return ret;
|
|
}
|
|
|
|
static void debug_cb(void *data, jv input) {
|
|
int dumpopts = *(int *)data;
|
|
jv_dumpf(JV_ARRAY(jv_string("DEBUG:"), input), stderr, dumpopts & ~(JV_PRINT_PRETTY));
|
|
fprintf(stderr, "\n");
|
|
}
|
|
|
|
int main(int argc, char* argv[]) {
|
|
jq_state *jq = NULL;
|
|
int ret = 0;
|
|
int compiled = 0;
|
|
int parser_flags = 0;
|
|
int nfiles = 0;
|
|
|
|
if (argc) progname = argv[0];
|
|
|
|
jq = jq_init();
|
|
if (jq == NULL) {
|
|
perror("malloc");
|
|
ret = 2;
|
|
goto out;
|
|
}
|
|
|
|
const char* program = 0;
|
|
|
|
jq_util_input_state input_state = jq_util_input_init(NULL, NULL); // XXX add err_cb
|
|
|
|
int further_args_are_files = 0;
|
|
int jq_flags = 0;
|
|
size_t short_opts = 0;
|
|
jv program_arguments = jv_array();
|
|
jv lib_search_paths = jv_null();
|
|
for (int i=1; i<argc; i++, short_opts = 0) {
|
|
if (further_args_are_files) {
|
|
jq_util_input_add_input(input_state, jv_string(argv[i]));
|
|
nfiles++;
|
|
} else if (!strcmp(argv[i], "--")) {
|
|
if (!program) usage(2);
|
|
further_args_are_files = 1;
|
|
} else if (!isoptish(argv[i])) {
|
|
if (program) {
|
|
jq_util_input_add_input(input_state, jv_string(argv[i]));
|
|
nfiles++;
|
|
} else {
|
|
program = argv[i];
|
|
}
|
|
} else {
|
|
if (argv[i][1] == 'L') {
|
|
if (jv_get_kind(lib_search_paths) == JV_KIND_NULL)
|
|
lib_search_paths = jv_array();
|
|
if (argv[i][2] != 0) { // -Lname (faster check than strlen)
|
|
lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i]+2));
|
|
} else if (i >= argc - 1) {
|
|
fprintf(stderr, "-L takes a parameter: (e.g. -L /search/path or -L/search/path)\n");
|
|
die();
|
|
} else {
|
|
lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i+1]));
|
|
i++;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (isoption(argv[i], 's', "slurp", &short_opts)) {
|
|
options |= SLURP;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'r', "raw-output", &short_opts)) {
|
|
options |= RAW_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'c', "compact-output", &short_opts)) {
|
|
options |= COMPACT_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'C', "color-output", &short_opts)) {
|
|
options |= COLOUR_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'M', "monochrome-output", &short_opts)) {
|
|
options |= NO_COLOUR_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'a', "ascii-output", &short_opts)) {
|
|
options |= ASCII_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "unbuffered", &short_opts)) {
|
|
options |= UNBUFFERED_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'S', "sort-keys", &short_opts)) {
|
|
options |= SORTED_OUTPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'R', "raw-input", &short_opts)) {
|
|
options |= RAW_INPUT;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'n', "null-input", &short_opts)) {
|
|
options |= PROVIDE_NULL;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'f', "from-file", &short_opts)) {
|
|
options |= FROM_FILE;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'j', "join-output", &short_opts)) {
|
|
options |= RAW_OUTPUT | RAW_NO_LF;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "seq", &short_opts)) {
|
|
options |= SEQ;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "stream", &short_opts)) {
|
|
parser_flags |= JV_PARSE_STREAMING;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "stream-errors", &short_opts)) {
|
|
parser_flags |= JV_PARSE_STREAM_ERRORS;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'e', "exit-status", &short_opts)) {
|
|
options |= EXIT_STATUS;
|
|
if (!short_opts) continue;
|
|
}
|
|
// FIXME: For --arg* we should check that the varname is acceptable
|
|
if (isoption(argv[i], 0, "arg", &short_opts)) {
|
|
if (i >= argc - 2) {
|
|
fprintf(stderr, "%s: --arg takes two parameters (e.g. -a varname value)\n", progname);
|
|
die();
|
|
}
|
|
jv arg = jv_object();
|
|
arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1]));
|
|
arg = jv_object_set(arg, jv_string("value"), jv_string(argv[i+2]));
|
|
program_arguments = jv_array_append(program_arguments, arg);
|
|
i += 2; // skip the next two arguments
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "argjson", &short_opts)) {
|
|
if (i >= argc - 2) {
|
|
fprintf(stderr, "%s: --argjson takes two parameters (e.g. -a varname text)\n", progname);
|
|
die();
|
|
}
|
|
jv v = jv_parse(argv[i+2]);
|
|
if (!jv_is_valid(v)) {
|
|
fprintf(stderr, "%s: invalid JSON text passed to --argjson\n", progname);
|
|
die();
|
|
}
|
|
jv arg = jv_object();
|
|
arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1]));
|
|
arg = jv_object_set(arg, jv_string("value"), v);
|
|
program_arguments = jv_array_append(program_arguments, arg);
|
|
i += 2; // skip the next two arguments
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "argfile", &short_opts)) {
|
|
if (i >= argc - 2) {
|
|
fprintf(stderr, "%s: --argfile takes two parameters (e.g. -a varname filename)\n", progname);
|
|
die();
|
|
}
|
|
jv arg = jv_object();
|
|
arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1]));
|
|
jv data = jv_load_file(argv[i+2], 0);
|
|
if (!jv_is_valid(data)) {
|
|
data = jv_invalid_get_msg(data);
|
|
fprintf(stderr, "%s: Bad JSON in --argfile %s %s: %s\n", progname,
|
|
argv[i+1], argv[i+2], jv_string_value(data));
|
|
jv_free(data);
|
|
ret = 2;
|
|
goto out;
|
|
}
|
|
arg = jv_object_set(arg, jv_string("value"), jv_array_get(data, 0));
|
|
program_arguments = jv_array_append(program_arguments, arg);
|
|
i += 2; // skip the next two arguments
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "debug-dump-disasm", &short_opts)) {
|
|
options |= DUMP_DISASM;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 0, "debug-trace", &short_opts)) {
|
|
jq_flags |= JQ_DEBUG_TRACE;
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'h', "help", &short_opts)) {
|
|
usage(0);
|
|
if (!short_opts) continue;
|
|
}
|
|
if (isoption(argv[i], 'V', "version", &short_opts)) {
|
|
printf("jq-%s\n", JQ_VERSION);
|
|
ret = 0;
|
|
goto out;
|
|
}
|
|
if (isoption(argv[i], 0, "run-tests", &short_opts)) {
|
|
i++;
|
|
// XXX Pass program_arguments, even a whole jq_state *, through;
|
|
// could be useful for testing
|
|
jv_free(program_arguments);
|
|
ret = jq_testsuite(lib_search_paths, argc - i, argv + i);
|
|
goto out;
|
|
}
|
|
|
|
// check for unknown options... if this argument was a short option
|
|
if (strlen(argv[i]) != short_opts + 1) {
|
|
fprintf(stderr, "%s: Unknown option %s\n", progname, argv[i]);
|
|
die();
|
|
}
|
|
}
|
|
}
|
|
|
|
int dumpopts = 0;
|
|
#ifndef WIN32
|
|
/* Disable colour by default on Windows builds as Windows
|
|
terminals tend not to display it correctly */
|
|
if (isatty(fileno(stdout)))
|
|
dumpopts |= JV_PRINT_COLOUR;
|
|
#endif
|
|
if (options & SORTED_OUTPUT) dumpopts |= JV_PRINT_SORTED;
|
|
if (!(options & COMPACT_OUTPUT)) dumpopts |= JV_PRINT_PRETTY;
|
|
if (options & ASCII_OUTPUT) dumpopts |= JV_PRINT_ASCII;
|
|
if (options & COLOUR_OUTPUT) dumpopts |= JV_PRINT_COLOUR;
|
|
if (options & NO_COLOUR_OUTPUT) dumpopts &= ~JV_PRINT_COLOUR;
|
|
|
|
if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) {
|
|
// Default search path list
|
|
lib_search_paths = JV_ARRAY(jv_string("~/.jq"),
|
|
jv_string("$ORIGIN/../lib/jq"),
|
|
jv_string("$ORIGIN/lib"));
|
|
}
|
|
jq_set_attr(jq, jv_string("JQ_LIBRARY_PATH"), lib_search_paths);
|
|
|
|
char *origin = strdup(argv[0]);
|
|
if (origin == NULL) {
|
|
fprintf(stderr, "Error: out of memory\n");
|
|
exit(1);
|
|
}
|
|
jq_set_attr(jq, jv_string("JQ_ORIGIN"), jv_string(dirname(origin)));
|
|
free(origin);
|
|
|
|
if (strchr(JQ_VERSION, '-') == NULL)
|
|
jq_set_attr(jq, jv_string("VERSION_DIR"), jv_string(JQ_VERSION));
|
|
else
|
|
jq_set_attr(jq, jv_string("VERSION_DIR"), jv_string_fmt("%.*s-master", (int)(strchr(JQ_VERSION, '-') - JQ_VERSION), JQ_VERSION));
|
|
|
|
#if (!defined(WIN32) && defined(HAVE_ISATTY)) || defined(HAVE__ISATTY)
|
|
|
|
#if defined(HAVE__ISATTY) && defined(isatty)
|
|
#undef isatty
|
|
#define isatty _isatty
|
|
#endif
|
|
|
|
if (!program && isatty(STDOUT_FILENO) && !isatty(STDIN_FILENO))
|
|
program = ".";
|
|
#endif
|
|
|
|
if (!program) usage(2);
|
|
|
|
if (options & FROM_FILE) {
|
|
char *program_origin = strdup(program);
|
|
if (program_origin == NULL) {
|
|
perror("malloc");
|
|
exit(2);
|
|
}
|
|
|
|
jv data = jv_load_file(program, 1);
|
|
if (!jv_is_valid(data)) {
|
|
data = jv_invalid_get_msg(data);
|
|
fprintf(stderr, "%s: %s\n", progname, jv_string_value(data));
|
|
jv_free(data);
|
|
ret = 2;
|
|
goto out;
|
|
}
|
|
jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string(dirname(program_origin))));
|
|
compiled = jq_compile_args(jq, jv_string_value(data), program_arguments);
|
|
free(program_origin);
|
|
jv_free(data);
|
|
} else {
|
|
jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string("."))); // XXX is this good?
|
|
compiled = jq_compile_args(jq, program, program_arguments);
|
|
}
|
|
if (!compiled){
|
|
ret = 3;
|
|
goto out;
|
|
}
|
|
|
|
if (options & DUMP_DISASM) {
|
|
jq_dump_disassembly(jq, 0);
|
|
printf("\n");
|
|
}
|
|
|
|
if ((options & SEQ))
|
|
parser_flags |= JV_PARSE_SEQ;
|
|
|
|
if ((options & RAW_INPUT))
|
|
jq_util_input_set_parser(input_state, NULL, (options & SLURP) ? 1 : 0);
|
|
else
|
|
jq_util_input_set_parser(input_state, jv_parser_new(parser_flags), (options & SLURP) ? 1 : 0);
|
|
|
|
// Let jq program read from inputs
|
|
jq_set_input_cb(jq, jq_util_input_next_input_cb, input_state);
|
|
|
|
// Let jq program call `debug` builtin and have that go somewhere
|
|
jq_set_debug_cb(jq, debug_cb, &dumpopts);
|
|
|
|
if (nfiles == 0)
|
|
jq_util_input_add_input(input_state, jv_string("-"));
|
|
|
|
if (options & PROVIDE_NULL) {
|
|
ret = process(jq, jv_null(), jq_flags, dumpopts);
|
|
} else {
|
|
jv value;
|
|
while (jq_util_input_errors(input_state) == 0 &&
|
|
(jv_is_valid((value = jq_util_input_next_input(input_state))) || jv_invalid_has_msg(jv_copy(value)))) {
|
|
if (jv_is_valid(value)) {
|
|
ret = process(jq, value, jq_flags, dumpopts);
|
|
continue;
|
|
}
|
|
|
|
// Parse error
|
|
jv msg = jv_invalid_get_msg(value);
|
|
if (!(options & SEQ)) {
|
|
// --seq -> errors are not fatal
|
|
ret = 4;
|
|
fprintf(stderr, "parse error: %s\n", jv_string_value(msg));
|
|
jv_free(msg);
|
|
break;
|
|
}
|
|
fprintf(stderr, "ignoring parse error: %s\n", jv_string_value(msg));
|
|
jv_free(msg);
|
|
}
|
|
}
|
|
|
|
if (jq_util_input_errors(input_state) != 0)
|
|
ret = 2;
|
|
|
|
out:
|
|
/* XXX We really should catch ENOSPC and such errors in jv_dumpf()! */
|
|
if (fclose(stdout)!=0) {
|
|
fprintf(stderr,"Error: writing output failed: %s\n", strerror(errno));
|
|
ret = 2;
|
|
}
|
|
|
|
jq_util_input_free(&input_state);
|
|
jq_teardown(&jq);
|
|
if (ret >= 10 && (options & EXIT_STATUS))
|
|
return ret - 10;
|
|
if (ret >= 10)
|
|
return 0;
|
|
return ret;
|
|
}
|