@ 261102.1

141226.9 · 5bfb9781f7e5ba2ad99b71150bccc559d53b928f

Add Streaming parser (--stream)

Streaming means that outputs are produced as soon as possible. With the `foreach` syntax one can write programs which reduce portions of the streaming parse of a large input (reduce into proper JSON values, for example), and discard the rest, processing incrementally.

This:

$ jq -c --stream .

should produce the same output as this:

$ jq -c '. as $dot | path(..) as $p | $dot | getpath($p) | [$p,.]'

The output of `jq --stream .` should be a sequence of`[[<path>],<leaf>]` and `[[<path>]]` values. The latter indicate that the array/object at that path ended.

Scalars and empty arrays and objects are leaf values for this purpose.

For example, a truncated input produces a path as soon as possible, then later the error:

$ printf '[0,\n'|./jq -c --stream .
[[0],0]
parse error: Unfinished JSON term at EOF at line 3, column 0
$

reading declarations

builtin.c

@@ -862,6 +862,18 @@ static jv f_modulemeta(jq_state *jq, jv a) {   return load_module_meta(jq, a); } +static jv f_input(jq_state *jq, jv input) {+  jv_free(input);+  jq_input_cb cb;+  void *data;+  jq_get_input_cb(jq, &cb, &data);+  jv v = cb(jq, data);+  if (jv_is_valid(v) || jv_invalid_has_msg(jv_copy(v)))+    return v;+  return jv_invalid_with_msg(jv_string("break"));+}++  #define LIBM_DD(name) \   {(cfunction_ptr)f_ ## name, "_" #name, 1},@@ -912,6 +924,7 @@ static const struct cfunction function_list[] = {   {(cfunction_ptr)f_env, "env", 1},   {(cfunction_ptr)f_match, "_match_impl", 4},   {(cfunction_ptr)f_modulemeta, "modulemeta", 1},+  {(cfunction_ptr)f_input, "_input", 1}, }; #undef LIBM_DD @@ -1014,6 +1027,7 @@ static const char* const jq_builtins[] = {   "def nulls: select(type == \"null\");",   "def values: select(. != null);",   "def scalars: select(. == null or . == true or . == false or type == \"number\" or type == \"string\");",+  "def scalars_or_empty: select(. == null or . == true or . == false or type == \"number\" or type == \"string\" or ((type==\"array\" or type==\"object\") and length==0));",   "def leaf_paths: paths(scalars);",   "def join($x): reduce .[] as $i (\"\"; . + (if . == \"\" then $i else $x + $i end));",   "def flatten: reduce .[] as $i ([]; if $i | type == \"array\" then . + ($i | flatten) else . + [$i] end);",

docs/content/3.manual/manual.yml

@@ -103,6 +103,17 @@ sections:         RS.  This more also parses the output of jq without the `--seq`         option. +      * `--stream`:++        Parse the input in streaming fashion, outputing arrays of path+        and leaf values (scalars and empty arrays or empty objects).+        For example, `"a"` becomes `[[],"a"]`, and `[[],"a",["b"]]`+        becomes `[[0],[]]`, `[[1],"a"]`, and `[[1,0],"b"]`.++        This is useful for processing very large inputs.  Use this in+        conjunction with filtering and the `reduce` and `foreach` syntax+        to reduce large inputs incrementally.+       * `--slurp`/`-s`:          Instead of running the filter for each JSON object in the@@ -2205,6 +2216,29 @@ sections:             input: '1'             output: ['[1,2,4,8,16,32,64]'] +  - title: 'I/O'+    body: |++      At this time jq has minimal support for I/O, mostly in the+      form of control over when inputs are read.  Two builtins functions+      are provided for this, `input` and `inputs`, that read from the+      same sources (e.g., `stdin`, files named on the command-line) as+      jq itself.  These two builtins, and jq's own reading actions, can+      be interleaved with each other.++      - title: "`input`"+        body: |++          Outputs one new input.++      - title: "`inputs`"+        body: |++          Outputs all remaining inputs, one by one.++          This is primarily useful for reductions over a program's+          inputs.+   - title: Assignment     body: | 

execute.c

@@ -39,6 +39,8 @@ struct jq_state {   int initial_execution;    jv attrs;+  jq_input_cb input_cb;+  void *input_cb_data; };  struct closure {@@ -1037,3 +1039,13 @@ jv jq_get_attr(jq_state *jq, jv attr) { void jq_dump_disassembly(jq_state *jq, int indent) {   dump_disassembly(indent, jq->bc); }++void jq_set_input_cb(jq_state *jq, jq_input_cb cb, void *data) {+  jq->input_cb = cb;+  jq->input_cb_data = data;+}++void jq_get_input_cb(jq_state *jq, jq_input_cb *cb, void **data) {+  *cb = jq->input_cb;+  *data = jq->input_cb_data;+}

jq.h

@@ -22,6 +22,10 @@ void jq_start(jq_state *, jv value, int); jv jq_next(jq_state *); void jq_teardown(jq_state **); +typedef jv (*jq_input_cb)(jq_state *, void *);+void jq_set_input_cb(jq_state *, jq_input_cb, void *);+void jq_get_input_cb(jq_state *, jq_input_cb *, void **);+ void jq_set_attrs(jq_state *, jv); jv jq_get_attrs(jq_state *); jv jq_get_jq_origin(jq_state *);

jv.h

@@ -170,7 +170,11 @@ void jv_dump(jv, int flags); void jv_show(jv, int flags); jv jv_dump_string(jv, int flags); -#define JV_PARSE_SEQ 1+enum {+  JV_PARSE_SEQ              = 1,+  JV_PARSE_STREAMING        = 2,+  JV_PARSE_STREAM_ERRORS    = 4,+};  jv jv_parse(const char* string); jv jv_parse_sized(const char* string, int length);@@ -183,6 +187,7 @@ jv jv_load_file(const char *, int); struct jv_parser; struct jv_parser* jv_parser_new(int); void jv_parser_set_buf(struct jv_parser*, const char*, int, int);+int jv_parser_remaining(struct jv_parser*); jv jv_parser_next(struct jv_parser*); void jv_parser_free(struct jv_parser*); 

jv_parse.c

@@ -17,6 +17,15 @@ typedef const char* presult; #define pfunc presult #endif +enum last_seen {+  JV_LAST_NONE = 0,+  JV_LAST_OPEN_ARRAY = '[',+  JV_LAST_OPEN_OBJECT = '{',+  JV_LAST_COLON = ':',+  JV_LAST_COMMA = ',',+  JV_LAST_VALUE = 'V',+};+ struct jv_parser {   const char* curr_buf;   int curr_buf_length;@@ -26,10 +35,13 @@ struct jv_parser {    int flags; -  jv* stack;-  int stackpos;-  int stacklen;-  jv next;+  jv* stack;                   // parser+  int stackpos;                // parser+  int stacklen;                // both (optimization; it's really pathlen for streaming)+  jv path;                     // streamer+  enum last_seen last_seen;    // streamer+  jv output;                   // streamer+  jv next;                     // both      char* tokenbuf;   int tokenpos;@@ -49,10 +61,18 @@ struct jv_parser { };  -static void parser_init(struct jv_parser* p) {-  p->flags = 0;+static void parser_init(struct jv_parser* p, int flags) {+  p->flags = flags;+  if ((p->flags & JV_PARSE_STREAMING)) {+    p->path = jv_array();+  } else {+    p->path = jv_invalid();+    p->flags &= ~(JV_PARSE_STREAM_ERRORS);+  }   p->stack = 0;   p->stacklen = p->stackpos = 0;+  p->last_seen = JV_LAST_NONE;+  p->output = jv_invalid();   p->next = jv_invalid();   p->tokenbuf = 0;   p->tokenlen = p->tokenpos = 0;@@ -66,6 +86,13 @@ static void parser_init(struct jv_parser* p) { }  static void parser_reset(struct jv_parser* p) {+  if ((p->flags & JV_PARSE_STREAMING)) {+    jv_free(p->path);+    p->path = jv_array();+  }+  p->last_seen = JV_LAST_NONE;+  jv_free(p->output);+  p->output = jv_invalid();   jv_free(p->next);   p->next = jv_invalid();   for (int i=0; i<p->stackpos; i++) @@ -77,13 +104,23 @@ static void parser_reset(struct jv_parser* p) {  static void parser_free(struct jv_parser* p) {   parser_reset(p);+  jv_free(p->path);   jv_mem_free(p->stack);   jv_mem_free(p->tokenbuf);   jvp_dtoa_context_free(&p->dtoa); }  static pfunc value(struct jv_parser* p, jv val) {-  if (jv_is_valid(p->next)) return "Expected separator between values";+  if ((p->flags & JV_PARSE_STREAMING)) {+    if (jv_is_valid(p->next) || p->last_seen == JV_LAST_VALUE)+      return "Expected separator between values";+    if (p->stacklen > 0)+      p->last_seen = JV_LAST_VALUE;+    else+      p->last_seen = JV_LAST_NONE;+  } else {+    if (jv_is_valid(p->next)) return "Expected separator between values";+  }   jv_free(p->next);   p->next = val;   return 0;@@ -99,7 +136,7 @@ static void push(struct jv_parser* p, jv v) {   p->stack[p->stackpos++] = v; } -static pfunc token(struct jv_parser* p, char ch) {+static pfunc parse_token(struct jv_parser* p, char ch) {   switch (ch) {   case '[':     if (jv_is_valid(p->next)) return "Expected separator between values";@@ -182,6 +219,159 @@ static pfunc token(struct jv_parser* p, char ch) {   return 0; } +static pfunc stream_token(struct jv_parser* p, char ch) {+  jv_kind k;+  jv last;++  switch (ch) {+  case '[':+    if (jv_is_valid(p->next))+      return "Expected a separator between values";+    p->path = jv_array_append(p->path, jv_number(0)); // push+    p->last_seen = JV_LAST_OPEN_ARRAY;+    p->stacklen++;+    break;++  case '{':+    if (p->last_seen == JV_LAST_VALUE)+      return "Expected a separator between values";+    // Push object key: null, since we don't know it yet+    p->path = jv_array_append(p->path, jv_null()); // push+    p->last_seen = JV_LAST_OPEN_OBJECT;+    p->stacklen++;+    break;++  case ':':+    if (p->stacklen == 0 || jv_get_kind(jv_array_get(jv_copy(p->path), p->stacklen - 1)) == JV_KIND_NUMBER)+      return "':' not as part of an object";+    if (!jv_is_valid(p->next) || p->last_seen == JV_LAST_NONE)+      return "Expected string key before ':'";+    if (jv_get_kind(p->next) != JV_KIND_STRING)+      return "Object keys must be strings";+    if (p->last_seen != JV_LAST_VALUE)+      return "':' should follow a key";+    p->last_seen = JV_LAST_COLON;+    p->path = jv_array_set(p->path, p->stacklen - 1, p->next);+    p->next = jv_invalid();+    break;++  case ',':+    if (p->last_seen != JV_LAST_VALUE)+      return "Expected value before ','";+    if (p->stacklen == 0)+      return "',' not as part of an object or array";+    last = jv_array_get(jv_copy(p->path), p->stacklen - 1);+    k = jv_get_kind(last);+    if (k == JV_KIND_NUMBER) {+      int idx = jv_number_value(last);++      if (jv_is_valid(p->next)) {+        p->output = JV_ARRAY(jv_copy(p->path), p->next);+        p->next = jv_invalid();+      }+      p->path = jv_array_set(p->path, p->stacklen - 1, jv_number(idx + 1));+      p->last_seen = JV_LAST_COMMA;+    } else if (k == JV_KIND_STRING) {+      if (jv_is_valid(p->next)) {+        p->output = JV_ARRAY(jv_copy(p->path), p->next);+        p->next = jv_invalid();+      }+      p->path = jv_array_set(p->path, p->stacklen - 1, jv_true()); // ready for another name:value pair+      p->last_seen = JV_LAST_COMMA;+    } else {+      assert(k == JV_KIND_NULL);+      // this case hits on input like {,}+      // make sure to handle input like {"a", "b"} and {"a":, ...}+      jv_free(last);+      return "Objects must consist of key:value pairs";+    }+    jv_free(last);+    break;++  case ']':+    if (p->stacklen == 0)+      return "Unmatched ']' at the top-level";+    if (p->last_seen == JV_LAST_COMMA)+      return "Expected another array element";+    if (p->last_seen == JV_LAST_OPEN_ARRAY)+      assert(!jv_is_valid(p->next));++    last = jv_array_get(jv_copy(p->path), p->stacklen - 1);+    k = jv_get_kind(last);+    jv_free(last);++    if (k != JV_KIND_NUMBER)+      return "Unmatched ']' in the middle of an object";+    if (jv_is_valid(p->next)) {+      p->output = JV_ARRAY(jv_copy(p->path), p->next, jv_true());+      p->next = jv_invalid();+    } else if (p->last_seen != JV_LAST_OPEN_ARRAY) {+      p->output = JV_ARRAY(jv_copy(p->path));+    }++    p->path = jv_array_slice(p->path, 0, --(p->stacklen)); // pop+    //assert(!jv_is_valid(p->next));+    jv_free(p->next);+    p->next = jv_invalid();++    if (p->last_seen == JV_LAST_OPEN_ARRAY)+      p->output = JV_ARRAY(jv_copy(p->path), jv_array()); // Empty arrays are leaves++    if (p->stacklen == 0)+      p->last_seen = JV_LAST_NONE;+    else+      p->last_seen = JV_LAST_VALUE;+    break;++  case '}':+    if (p->stacklen == 0)+      return "Unmatched '}' at the top-level";+    if (p->last_seen == JV_LAST_COMMA)+      return "Expected another key:value pair";+    if (p->last_seen == JV_LAST_OPEN_OBJECT)+      assert(!jv_is_valid(p->next));++    last = jv_array_get(jv_copy(p->path), p->stacklen - 1);+    k = jv_get_kind(last);+    jv_free(last);+    if (k == JV_KIND_NUMBER)+      return "Unmatched '}' in the middle of an array";++    if (jv_is_valid(p->next)) {+      if (k != JV_KIND_STRING)+        return "Objects must consist of key:value pairs";+      p->output = JV_ARRAY(jv_copy(p->path), p->next, jv_true());+      p->next = jv_invalid();+    } else {+      // Perhaps {"a":[]}+      if (p->last_seen == JV_LAST_COLON)+        // Looks like {"a":}+        return "Missing value in key:value pair";+      if (p->last_seen == JV_LAST_COMMA)+        // Looks like {"a":0,}+        return "Expected another key-value pair";+      if (p->last_seen == JV_LAST_OPEN_ARRAY)+        return "Unmatched '}' in the middle of an array";+      if (p->last_seen != JV_LAST_VALUE && p->last_seen != JV_LAST_OPEN_OBJECT)+        return "Unmatched '}'";+      if (p->last_seen != JV_LAST_OPEN_OBJECT)+        p->output = JV_ARRAY(jv_copy(p->path));+    }+    p->path = jv_array_slice(p->path, 0, --(p->stacklen)); // pop+    jv_free(p->next);+    p->next = jv_invalid();++    if (p->last_seen == JV_LAST_OPEN_OBJECT)+      p->output = JV_ARRAY(jv_copy(p->path), jv_object()); // Empty arrays are leaves++    if (p->stacklen == 0)+      p->last_seen = JV_LAST_NONE;+    else+      p->last_seen = JV_LAST_VALUE;+    break;+  }+  return 0;+}  static void tokenadd(struct jv_parser* p, char c) {   assert(p->tokenpos <= p->tokenlen);@@ -191,6 +381,7 @@ static void tokenadd(struct jv_parser* p, char c) {   }   assert(p->tokenpos < p->tokenlen);   p->tokenbuf[p->tokenpos++] = c;+  p->tokenbuf[p->tokenpos] = '\0'; // for debugging }  static int unhex4(char* hex) {@@ -329,7 +520,7 @@ static chclass classify(char c) {  static const presult OK = "output produced"; -static int check_done(struct jv_parser* p, jv* out) {+static int parse_check_done(struct jv_parser* p, jv* out) {   if (p->stackpos == 0 && jv_is_valid(p->next)) {     *out = p->next;     p->next = jv_invalid();@@ -339,6 +530,50 @@ static int check_done(struct jv_parser* p, jv* out) {   } } +static int stream_check_done(struct jv_parser* p, jv* out) {+  if (p->stacklen == 0 && jv_is_valid(p->next)) {+    *out = JV_ARRAY(jv_copy(p->path),p->next);+    p->next = jv_invalid();+    return 1;+  } else if (jv_is_valid(p->output)) {+    if (jv_array_length(jv_copy(p->output)) > 2) {+      // At end of an array or object, necessitating one more output by+      // which to indicate this+      *out = jv_array_slice(jv_copy(p->output), 0, 2);+      p->output = jv_array_slice(p->output, 0, 1);      // arrange one more output+    } else {+      // No further processing needed+      *out = p->output;+      p->output = jv_invalid();+    }+    return 1;+  } else {+    return 0;+  }+}++static int parse_check_truncation(struct jv_parser* p, jv out) {+  return ((p->flags & JV_PARSE_SEQ) && !p->last_ch_was_ws && jv_get_kind(out) == JV_KIND_NUMBER);+}++static int stream_check_truncation(struct jv_parser* p, jv out) {+  if (!jv_is_valid(out))+    return 0;+  jv v = jv_array_get(jv_copy(out), 1);+  jv_kind k = jv_get_kind(v);+  jv_free(v);+  return (k == JV_KIND_NUMBER || k == JV_KIND_TRUE || k == JV_KIND_FALSE || k == JV_KIND_NULL);+}++#define check_done(p, out) \+   (((p)->flags & JV_PARSE_STREAMING) ? stream_check_done((p), (out)) : parse_check_done((p), (out)))++#define token(p, ch) \+   (((p)->flags & JV_PARSE_STREAMING) ? stream_token((p), (ch)) : parse_token((p), (ch)))++#define check_truncation(p, o) \+    (((p)->flags & JV_PARSE_STREAMING) ? stream_check_truncation((p), (o)) : parse_check_truncation((p), (o)))+ static pfunc scan(struct jv_parser* p, char ch, jv* out) {   p->column++;   if (ch == '\n') {@@ -348,7 +583,7 @@ static pfunc scan(struct jv_parser* p, char ch, jv* out) {   if (ch == '\036' /* ASCII RS; see draft-ietf-json-sequence-07 */) {     TRY(check_literal(p));     if (p->st == JV_PARSER_NORMAL && check_done(p, out)) {-      if ((p->flags & JV_PARSE_SEQ) && !p->last_ch_was_ws && jv_get_kind(*out) == JV_KIND_NUMBER) {+      if (check_truncation(p, *out)) {         jv_free(*out);         *out = jv_invalid();         return "Potentially truncated top-level numeric value";@@ -404,7 +639,7 @@ static pfunc scan(struct jv_parser* p, char ch, jv* out) {  struct jv_parser* jv_parser_new(int flags) {   struct jv_parser* p = jv_mem_alloc(sizeof(struct jv_parser));-  parser_init(p);+  parser_init(p, flags);   p->flags = flags;   return p; }@@ -416,6 +651,12 @@ void jv_parser_free(struct jv_parser* p) {  static const unsigned char UTF8_BOM[] = {0xEF,0xBB,0xBF}; +int jv_parser_remaining(struct jv_parser* p) {+  if (p->curr_buf == 0)+    return 0;+  return (p->curr_buf_length - p->curr_buf_pos);+}+ void jv_parser_set_buf(struct jv_parser* p, const char* buf, int length, int is_partial) {   assert((p->curr_buf == 0 || p->curr_buf_pos == p->curr_buf_length)          && "previous buffer not exhausted");@@ -441,10 +682,25 @@ void jv_parser_set_buf(struct jv_parser* p, const char* buf, int length, int is_   p->curr_buf_is_partial = is_partial; } +static jv make_error(struct jv_parser*, const char *, ...) JV_PRINTF_LIKE(2, 3);++static jv make_error(struct jv_parser* p, const char *fmt, ...) {+  va_list ap;+  va_start(ap, fmt);+  jv e = jv_string_vfmt(fmt, ap);+  va_end(ap);+  if ((p->flags & JV_PARSE_STREAM_ERRORS))+    return JV_ARRAY(e, jv_copy(p->path));+  return jv_invalid_with_msg(e);+}+ jv jv_parser_next(struct jv_parser* p) {-  assert(p->curr_buf && "a buffer must be provided");+  if (!p->curr_buf)+    return jv_invalid(); // Need a buffer   if (p->bom_strip_position == 0xff) return jv_invalid_with_msg(jv_string("Malformed BOM"));-  jv value;+  jv value = jv_invalid();+  if ((p->flags & JV_PARSE_STREAMING) && stream_check_done(p, &value))+    return value;   char ch;   presult msg = 0;   while (!msg && p->curr_buf_pos < p->curr_buf_length) {@@ -456,18 +712,21 @@ jv jv_parser_next(struct jv_parser* p) {   if (msg == OK) {     return value;   } else if (msg) {-    parser_reset(p);     if (ch != '\036' && (p->flags & JV_PARSE_SEQ)) {       // Skip to the next RS       p->st = JV_PARSER_WAITING_FOR_RS;-      return jv_invalid_with_msg(jv_string_fmt("%s at line %d, column %d (need RS to resync)", msg, p->line, p->column));+      value = make_error(p, "%s at line %d, column %d (need RS to resync)", msg, p->line, p->column);+      parser_reset(p);+      return value;     }+    value = make_error(p, "%s at line %d, column %d", msg, p->line, p->column);+    parser_reset(p);     if (!(p->flags & JV_PARSE_SEQ)) {       // We're not parsing a JSON text sequence; throw this buffer away.       p->curr_buf = 0;       p->curr_buf_pos = 0;     } // Else ch must be RS; don't clear buf so we can start parsing again after this ch-    return jv_invalid_with_msg(jv_string_fmt("%s at line %d, column %d", msg, p->line, p->column));+    return value;   } else if (p->curr_buf_is_partial) {     assert(p->curr_buf_pos == p->curr_buf_length);     // need another buffer@@ -477,28 +736,36 @@ jv jv_parser_next(struct jv_parser* p) {     // at EOF     if (p->st != JV_PARSER_WAITING_FOR_RS) {       if (p->st != JV_PARSER_NORMAL) {+        value = make_error(p, "Unfinished string at EOF at line %d, column %d", p->line, p->column);         parser_reset(p);         p->st = JV_PARSER_WAITING_FOR_RS;-        return jv_invalid_with_msg(jv_string("Unfinished string"));+        return value;       }       if ((msg = check_literal(p))) {+        value = make_error(p, "%s at EOF at line %d, column %d", msg, p->line, p->column);         parser_reset(p);         p->st = JV_PARSER_WAITING_FOR_RS;-        return jv_invalid_with_msg(jv_string(msg));+        return value;       }-      if (p->stackpos != 0) {+      if (((p->flags & JV_PARSE_STREAMING) && p->stacklen != 0) ||+          (!(p->flags & JV_PARSE_STREAMING) && p->stackpos != 0)) {+        value = make_error(p, "Unfinished JSON term at EOF at line %d, column %d", p->line, p->column);         parser_reset(p);         p->st = JV_PARSER_WAITING_FOR_RS;-        return jv_invalid_with_msg(jv_string("Unfinished JSON term"));+        return value;       }     }     // p->next is either invalid (nothing here but no syntax error)     // or valid (this is the value). either way it's the thing to return-    value = p->next;+    if ((p->flags & JV_PARSE_STREAMING) && jv_is_valid(p->next)) {+      value = JV_ARRAY(jv_copy(p->path), p->next); // except in streaming mode we've got to make it [path,value]+    } else {+      value = p->next;+    }     p->next = jv_invalid();     if ((p->flags & JV_PARSE_SEQ) && !p->last_ch_was_ws && jv_get_kind(value) == JV_KIND_NUMBER) {       jv_free(value);-      return jv_invalid_with_msg(jv_string("Potentially truncated top-level numeric value"));+      return make_error(p, "Potentially truncated top-level numeric value at EOF at line %d, column %d", p->line, p->column);     }     return value;   }@@ -506,7 +773,7 @@ jv jv_parser_next(struct jv_parser* p) {  jv jv_parse_sized(const char* string, int length) {   struct jv_parser parser;-  parser_init(&parser);+  parser_init(&parser, 0);   jv_parser_set_buf(&parser, string, length, 0);   jv value = jv_parser_next(&parser);   if (jv_is_valid(value)) {

lexer.l

@@ -100,7 +100,7 @@ struct lexer_param;   }   (\\[^u(]|\\u[a-zA-Z0-9]{0,4})+ {     /* pass escapes to the json parser */-    jv escapes = jv_string_fmt("\"%.*s\"", yyleng, yytext);+    jv escapes = jv_string_fmt("\"%.*s\"", (int)yyleng, yytext);     yylval->literal = jv_parse_sized(jv_string_value(escapes), jv_string_length_bytes(jv_copy(escapes)));     jv_free(escapes);     return QQSTRING_TEXT;

main.c

@@ -149,6 +149,7 @@ static int process(jq_state *jq, jv value, int flags) {   return ret; } +// XXX Move all this into the next_input_state struct FILE* current_input; const char** input_filenames = NULL; int ninput_files;@@ -175,6 +176,7 @@ static int read_more(char* buf, size_t size) {     }   } +  buf[0] = 0;   if (current_input) {     if (!fgets(buf, size, current_input))       buf[0] = 0;@@ -182,10 +184,61 @@ static int read_more(char* buf, size_t size) {   return next_input_idx == ninput_files && (!current_input || feof(current_input)); } +struct next_input_state {+  struct jv_parser *parser;+  jv slurped;+  char buf[4096];+};++// Blocks to read one more input from stdin and/or given files+// When slurping, it returns just one value+static jv next_input(jq_state *jq, void *data) {+  struct next_input_state *state = data;+  int is_last = 0;+  jv value = jv_invalid(); // need more input+  do {+    if (options & RAW_INPUT) {+      is_last = read_more(state->buf, sizeof(state->buf));+      if (state->buf[0] == '\0')+        continue;+      int len = strlen(state->buf); // Raw input doesn't support NULs+      if (len > 0) {+        if (options & SLURP) {+          state->slurped = jv_string_concat(state->slurped, jv_string(state->buf));+        } else if (jv_is_valid(value)) {+          if (state->buf[len-1] == '\n') {+            // whole line+            state->buf[len-1] = 0;+            return jv_string_concat(value, jv_string(state->buf));+          }+          value = jv_string_concat(value, jv_string(state->buf));+        }+      }+    } else {+      if (jv_parser_remaining(state->parser) == 0) {+        is_last = read_more(state->buf, sizeof(state->buf));+        jv_parser_set_buf(state->parser, state->buf, strlen(state->buf), !is_last); // NULs also not supported here+      }+      value = jv_parser_next(state->parser);+      if (options & SLURP) {+        if (jv_is_valid(value)) {+          state->slurped = jv_array_append(state->slurped, value);+          value = jv_invalid();+        } else if (jv_invalid_has_msg(jv_copy(value)))+          return value;+      } else if (jv_is_valid(value) || jv_invalid_has_msg(jv_copy(value))) {+        return value;+      }+    }+  } while (!is_last);+  return value;+}+ int main(int argc, char* argv[]) {   jq_state *jq = NULL;   int ret = 0;   int compiled = 0;+  int parser_flags = 0;   char *t = NULL;    if (argc) progname = argv[0];@@ -293,6 +346,14 @@ int main(int argc, char* argv[]) {         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;@@ -476,66 +537,57 @@ int main(int argc, char* argv[]) {     printf("\n");   } +  // XXX Refactor this and input_filenames[] and related setup into a+  // function to setup struct next_input_state.+  if ((options & SEQ))+    parser_flags |= JV_PARSE_SEQ;++  struct next_input_state input_state;+  input_state.parser = jv_parser_new(parser_flags);+  if ((options & RAW_INPUT) && (options & SLURP))+    input_state.slurped = jv_string("");+  else if ((options & SLURP))+    input_state.slurped = jv_array();+  else+    input_state.slurped = jv_invalid();++  // Let jq program read from inputs+  jq_set_input_cb(jq, next_input, &input_state);+   if (options & PROVIDE_NULL) {     ret = process(jq, jv_null(), jq_flags);   } else {-    jv slurped;-    if (options & SLURP) {-      if (options & RAW_INPUT) {-        slurped = jv_string("");-      } else {-        slurped = jv_array();+    jv value;+    while (jv_is_valid((value = next_input(jq, &input_state))) || jv_invalid_has_msg(jv_copy(value))) {+      if (jv_is_valid(value)) {+        ret = process(jq, value, jq_flags);+        continue;       }-    }-    struct jv_parser* parser = jv_parser_new((options & SEQ) ? JV_PARSE_SEQ : 0);-    char buf[4096];-    int is_last = 0;-    do {-      is_last = read_more(buf, sizeof(buf));-      if (options & RAW_INPUT) {-        int len = strlen(buf);-        if (len > 0) {-          if (options & SLURP) {-            slurped = jv_string_concat(slurped, jv_string(buf));-          } else {-            if (buf[len-1] == '\n') buf[len-1] = 0;-            ret = process(jq, jv_string(buf), jq_flags);-          }-        }-      } else {-        jv_parser_set_buf(parser, buf, strlen(buf), !is_last);-        jv value;-        while (jv_is_valid(value = jv_parser_next(parser)) || jv_invalid_has_msg(jv_copy(value))) {-          if (!jv_is_valid(value)) {-            jv msg = jv_invalid_get_msg(value);-            if (!(options & SEQ)) {-              // We used to treat parse errors as 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);-            // ...but with --seq we attempt to recover.-            continue;-          }-          if (options & SLURP) {-            slurped = jv_array_append(slurped, value);-          } else {-            ret = process(jq, value, jq_flags);-            value = jv_invalid();-          }-        }++      // 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;       }-    } while (!is_last);-    jv_parser_free(parser);-    if (ret != 0)-      goto out;+      fprintf(stderr, "ignoring parse error: %s\n", jv_string_value(msg));+      jv_free(msg);+    }     if (options & SLURP) {-      ret = process(jq, slurped, jq_flags);+      ret = process(jq, input_state.slurped, jq_flags);+      input_state.slurped = jv_invalid();     }   }++  jv_free(input_state.slurped);+  jv_parser_free(input_state.parser);++  if (ret != 0)+    goto out;+   if ((options & IN_PLACE)) {     FILE *devnull; #ifdef WIN32

tests/run

@@ -20,7 +20,6 @@ if [ -z "$d" ]; then     exit 0 fi - ## Test constant folding  # String constant folding (addition only)@@ -101,14 +100,14 @@ cmp $d/out $d/expected # Note that here jq sees no inputs at all but it still succeeds because # --seq ignores parse errors cat > $d/expected <<EOF-ignoring parse error: Unfinished string+ignoring parse error: Unfinished string at EOF at line 1, column 4 EOF printf '"foo'|./jq -ce --seq . > $d/out 2>&1 cmp $d/out $d/expected  # Numeric values truncated by EOF are ignored cat > $d/expected <<EOF-ignoring parse error: Potentially truncated top-level numeric value+ignoring parse error: Potentially truncated top-level numeric value at EOF at line 1, column 1 EOF printf '1'|./jq -ce --seq . > $d/out 2>&1 cmp $d/out $d/expected@@ -118,6 +117,12 @@ EOF printf '1\n'|./jq -ces --seq '. == [1]' >/dev/null 2> $d/out cmp $d/out $d/expected +## Test streaming parser++$VALGRIND $Q ./jq -c '. as $d|path(..) as $p|$d|getpath($p)|scalars_or_empty|[$p,.]' < "$PWD/tests/torture/input0.json" > $d/out0+$VALGRIND $Q ./jq --stream -c '.|select(length==2)' < "$PWD/tests/torture/input0.json" > $d/out1+diff $d/out0 $d/out1+ ## Test library/module system  mods=$PWD/tests/modules

tests/torture/input0.json

new file mode 100644@@ -0,0 +1,7 @@+0 12 true false null [] {} [{}] [[]]+[0,01,[12,22,[34,[45,56],7]],[]]+{"a":[1]}+{"a":[{}]}+{"a":[{},[],[[]]]}+{"a":[{"b":[]}]}+{"a":[{"b":[]},{},[2]]}