From cbc4b087c6863003630dfeee6b248287312cad1f Mon Sep 17 00:00:00 2001 From: Assaf Gordon Date: Fri, 17 Apr 2015 15:28:00 -0400 Subject: [PATCH] report filename:line on runtime errors On runtime errors (ending in invalid state at 'main.c:process()'), print the input filename and current line number which triggered the error. * util.c: struct jq_util_input_state: add variables jq_util_input_init(): initialize new variables jq_util_input_read_more(): update current file/line number on `fgets()` calls. jq_util_input_get_position(): helper functions returning a JV_STRING containing the current file/line. strncpyz(): helper function for safe string copy * main.c: process(): upon invalid result ('uncaught jq exception'), get the current input position and print it to stderr. * jq.h: declare 'jq_input_get_position()'. With this patch, runtime errors printed to stderr will contain the filename and line of the offending input. 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 minor ugly hack: The call the get the current filename/line in 'main.c' is hard-coded to 'jq_util_input_get_position()' which somewhat bypasses the idea of using an input callback (e.g. 'jq_set_input_cb()'). But since similar calls to 'jq_utl_input_XXXX' are also hard-coded in 'main.c', the input callback mechanism isn't really generatic at the moment. --- jq.h | 1 + main.c | 8 ++++++-- util.c | 48 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/jq.h b/jq.h index 9182cfae70..a012325632 100644 --- a/jq.h +++ b/jq.h @@ -47,5 +47,6 @@ int jq_util_input_open_errors(jq_util_input_state); int jq_util_input_read_more(jq_util_input_state); jv jq_util_input_next_input(jq_util_input_state); jv jq_util_input_next_input_cb(jq_state *, void *); +jv jq_util_input_get_position(jq_state*); #endif /* !_JQ_H_ */ diff --git a/main.c b/main.c index d00e1883e7..f0e68f6452 100644 --- a/main.c +++ b/main.c @@ -126,13 +126,17 @@ static int process(jq_state *jq, jv value, int flags, int dumpopts) { 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: %s\n", jv_string_value(msg)); + 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 (not a string): %s\n", jv_string_value(msg)); + 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); diff --git a/util.c b/util.c index 31a1768316..5806082b71 100644 --- a/util.c +++ b/util.c @@ -154,6 +154,9 @@ struct jq_util_input_state { int open_failures; jv slurped; char buf[4096]; + char current_filename[PATH_MAX]; //TODO: is PATH_MAX portable enough? + size_t current_line; + int found_newline; //flag to help count current-line correctly }; static void fprinter(void *data, jv fname) { @@ -176,6 +179,9 @@ jq_util_input_state jq_util_input_init(jq_msg_cb err_cb, void *err_cb_data) { new_state->files = jv_array(); new_state->slurped = jv_invalid(); new_state->buf[0] = 0; + new_state->current_filename[0] = 0; + new_state->current_line = 0; + new_state->found_newline = 0; return new_state; } @@ -220,6 +226,15 @@ static jv next_file(jq_util_input_state state) { return next; } +//An ad-hoc safer version of strncpy, +//which always adds a NULL (possibly truncating 'src') +static inline void strncpyz(char *dest, const char*src, size_t n) +{ + //TODO: is there a safe/portable str{n,l}cpy? + strncpy(dest, src, n); + dest[n-1] = 0; +} + int jq_util_input_read_more(jq_util_input_state state) { if (!state->current_input || feof(state->current_input) || ferror(state->current_input)) { if (state->current_input && ferror(state->current_input)) { @@ -235,26 +250,46 @@ int jq_util_input_read_more(jq_util_input_state state) { fclose(state->current_input); } state->current_input = NULL; + state->current_filename[0] = 0; + state->current_line = 0 ; + state->found_newline = 0; } jv f = next_file(state); if (jv_is_valid(f)) { if (!strcmp(jv_string_value(f), "-")) { state->current_input = stdin; + strncpyz(state->current_filename,"stdin", + sizeof(state->current_filename)); } else { state->current_input = fopen(jv_string_value(f), "r"); + strncpyz(state->current_filename,jv_string_value(f), + sizeof(state->current_filename)); if (!state->current_input) { state->err_cb(state->err_cb_data, jv_copy(f)); state->open_failures++; } } + state->current_line = 1; + state->found_newline = 0; jv_free(f); } } state->buf[0] = 0; if (state->current_input) { - if (!fgets(state->buf, sizeof(state->buf), state->current_input)) + //The previous 'fgets' found a newline character, + //so this fgets will start a new text line from the input file. + if (state->found_newline) + state->current_line++; + state->found_newline = 0; + if (!fgets(state->buf, sizeof(state->buf), state->current_input)) { state->buf[0] = 0; + } else { + //Check if we've read a newline - flag it for later + const size_t len = strlen(state->buf); + //TODO: do we care about '\r' for Win/Mac? + state->found_newline = (len>=1 && state->buf[len-1]=='\n'); + } } return jv_array_length(jv_copy(state->files)) == 0 && (!state->current_input || feof(state->current_input)); } @@ -263,6 +298,17 @@ jv jq_util_input_next_input_cb(jq_state *jq, void *data) { return jq_util_input_next_input((jq_util_input_state)data); } +// Return the current_filename:current_line as a JV_STRING +jv jq_util_input_get_position(jq_state *jq) { + jq_input_cb cb=NULL; + void *cb_data=NULL; + jq_get_input_cb(jq, &cb, &cb_data); + jq_util_input_state s = (jq_util_input_state)cb_data; + jv v = jv_string_fmt("%s:%zu",s->current_filename,s->current_line); + return v; +} + + // Blocks to read one more input from stdin and/or given files // When slurping, it returns just one value jv jq_util_input_next_input(jq_util_input_state state) {