Repository: jqlang/jq

Problem statement:
Consider removing `-0`/`--nul-output` before releasing 1.7
See #1271.  @vdukhovni [believes](https://github.com/jqlang/jq/issues/1271#issuecomment-1627791117) we should remove it, and explains [why](https://github.com/jqlang/jq/issues/1271#issuecomment-945051603) in the comments.


Hints:
I think we should also add a couple of sentences of disclaimers about *safety* with the `-r` option. It is the jq program's responsibility to make sure that the result is syntactically (and perhaps semantically) appropriate for its downstream consumer.
One question I have is whether having a `-0` is _bad_.  You make a good argument that it's not necessary.
Let's recap current behavior:

- `jq -r` includes a newline after each raw output:
  ```
  jq -nr '"foo"'|od -c
  0000000   f   o   o  \n
  0000004
  ```
- `jq -0` includes a `NUL` after each raw output:
  ```
  jq -n0 '"foo"'|od -c
  0000000   f   o   o  \0
  0000004
  ```
- but fortunately we have `-j` which causes `jq` to not output a pesky newline, and then I can output a `NUL` -or whatever other separator- at will:
  ```
  jq -nrj '"foo\u0000"'|od -c
  0000000   f   o   o  \0
  0000004
  ```

I.e., we just don't need `-0`/`--nul-output`.

So I'm in favor of removing `-0`/`--nul-output`.
Nice, and `-j` subsumes `-r`, so for example:
```
$ jq -nj '
  def nulterm: select(contains("\u0000")|not) | . + "\u0000";
  ("foo", "o\u0000ps", "bar") | nulterm' | cat -v; echo
foo^@bar^@
```
Can we just halt the program with `-0` when the top level value is a string because that's the only unsafe case?
```
$ cat nulterm.jq 
def nultermparanoid:
    if type != "string"
    then "string values required\n" | halt_error(1)
    elif contains("\u0000")
    then "internal NUL bytes invalid\n" | halt_error(1)
    else . + "\u0000"
    end;
("foo", "bar", .) | nultermparanoid
```
With that:
```
$ printf '"foo\0bar"\n' | jq -jf nulterm.jq > /dev/null
internal NUL bytes invalid

$ printf '"baz"\n' | jq -jf nulterm.jq  | cat -v; echo
foo^@bar^@baz^@
```

Patch:
diff --git a/docs/content/manual/manual.yml b/docs/content/manual/manual.yml
index a2ff8a763b..5479f98bc4 100644
--- a/docs/content/manual/manual.yml
+++ b/docs/content/manual/manual.yml
@@ -136,14 +136,15 @@ sections:
         formatted as a JSON string with quotes. This can be useful for
         making jq filters talk to non-JSON-based systems.
 
-      * `--join-output` / `-j`:
-
-        Like `-r` but jq won't print a newline after each output.
-
-      * `--nul-output` / `-0`:
+      * `--raw-output0`:
 
         Like `-r` but jq will print NUL instead of newline after each output.
         This can be useful when the values being output can contain newlines.
+        When the output value contains NUL, jq exits with non-zero code.
+
+      * `--join-output` / `-j`:
+
+        Like `-r` but jq won't print a newline after each output.
 
       * `--ascii-output` / `-a`:
 
diff --git a/jq.1.prebuilt b/jq.1.prebuilt
index 02518f54d9..4273110c6e 100644
--- a/jq.1.prebuilt
+++ b/jq.1.prebuilt
@@ -82,16 +82,16 @@ By default, jq pretty\-prints JSON output\. Using this option will result in mor
 With this option, if the filter\'s result is a string then it will be written directly to standard output rather than being formatted as a JSON string with quotes\. This can be useful for making jq filters talk to non\-JSON\-based systems\.
 .
 .TP
-\fB\-\-join\-output\fR / \fB\-j\fR:
+\fB\-\-raw\-output0\fR:
 .
 .IP
-Like \fB\-r\fR but jq won\'t print a newline after each output\.
+Like \fB\-r\fR but jq will print NUL instead of newline after each output\. This can be useful when the values being output can contain newlines\. When the output value contains NUL, jq exits with non\-zero code\.
 .
 .TP
-\fB\-\-nul\-output\fR / \fB\-0\fR:
+\fB\-\-join\-output\fR / \fB\-j\fR:
 .
 .IP
-Like \fB\-r\fR but jq will print NUL instead of newline after each output\. This can be useful when the values being output can contain newlines\.
+Like \fB\-r\fR but jq won\'t print a newline after each output\.
 .
 .TP
 \fB\-\-ascii\-output\fR / \fB\-a\fR:
diff --git a/src/main.c b/src/main.c
index d02c623afd..d9b1713c0f 100644
--- a/src/main.c
+++ b/src/main.c
@@ -74,8 +74,8 @@ static void usage(int code, int keep_it_short) {
       "  -s, --slurp               read all inputs into an array and use it as the single input value;\n"
       "  -c, --compact-output      compact instead of pretty-printed output;\n"
       "  -r, --raw-output          output strings directly without escapes and quotes;\n"
+      "      --raw-output0         implies -r and output NUL after each output;\n"
       "  -j, --join-output         implies -r and output without newline after each output;\n"
-      "  -0, --nul-output          implies -r and output NUL after each output;\n"
       "  -a, --ascii-output        output strings by only ASCII characters using escape sequences;\n"
       "  -S, --sort-keys           sort keys of each object on output;\n"
       "  -C, --color-output        colorize JSON output;\n"
@@ -111,11 +111,8 @@ static void die() {
   exit(2);
 }
 
-
-
-
 static int isoptish(const char* text) {
-  return text[0] == '-' && (text[1] == '-' || isalpha(text[1]) || text[1] == '0');
+  return text[0] == '-' && (text[1] == '-' || isalpha(text[1]));
 }
 
 static int isoption(const char* text, char shortopt, const char* longopt, size_t *short_opts) {
@@ -141,7 +138,7 @@ enum {
   RAW_INPUT             = 2,
   PROVIDE_NULL          = 4,
   RAW_OUTPUT            = 8,
-  RAW_NUL               = 16,
+  RAW_OUTPUT0           = 16,
   ASCII_OUTPUT          = 32,
   COLOR_OUTPUT          = 64,
   NO_COLOR_OUTPUT       = 128,
@@ -191,6 +188,11 @@ static int process(jq_state *jq, jv value, int flags, int dumpopts, int options)
     if ((options & RAW_OUTPUT) && jv_get_kind(result) == JV_KIND_STRING) {
       if (options & ASCII_OUTPUT) {
         jv_dumpf(jv_copy(result), stdout, JV_PRINT_ASCII);
+      } else if ((options & RAW_OUTPUT0) && strlen(jv_string_value(result)) != (unsigned long)jv_string_length_bytes(jv_copy(result))) {
+        jv_free(result);
+        result = jv_invalid_with_msg(jv_string(
+              "Cannot dump a string containing NUL with --raw-output0 option"));
+        break;
       } else {
         priv_fwrite(jv_string_value(result), jv_string_length_bytes(jv_copy(result)),
             stdout, dumpopts & JV_PRINT_ISATTY);
@@ -208,7 +210,7 @@ static int process(jq_state *jq, jv value, int flags, int dumpopts, int options)
     }
     if (!(options & RAW_NO_LF))
       priv_fwrite("\n", 1, stdout, dumpopts & JV_PRINT_ISATTY);
-    if (options & RAW_NUL)
+    if (options & RAW_OUTPUT0)
       priv_fwrite("\0", 1, stdout, dumpopts & JV_PRINT_ISATTY);
     if (options & UNBUFFERED_OUTPUT)
       fflush(stdout);
@@ -394,6 +396,14 @@ int main(int argc, char* argv[]) {
         options |= RAW_OUTPUT;
         if (!short_opts) continue;
       }
+      if (isoption(argv[i], 0, "raw-output0", &short_opts)) {
+        options |= RAW_OUTPUT | RAW_NO_LF | RAW_OUTPUT0;
+        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], 'c', "compact-output", &short_opts)) {
         dumpopts &= ~(JV_PRINT_TAB | JV_PRINT_INDENT_FLAGS(7));
         if (!short_opts) continue;
@@ -430,14 +440,6 @@ int main(int argc, char* argv[]) {
         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', "nul-output", &short_opts)) {
-        options |= RAW_OUTPUT | RAW_NO_LF | RAW_NUL;
-        if (!short_opts) continue;
-      }
       if (isoption(argv[i], 'b', "binary", &short_opts)) {
 #ifdef WIN32
         fflush(stdout);


Test patch:
diff --git a/tests/shtest b/tests/shtest
index 6cc428ca2a..a644b5742d 100755
--- a/tests/shtest
+++ b/tests/shtest
@@ -165,12 +165,20 @@ cmp $d/out $d/expected
 printf "[1,2][3,4]\n" | $JQ -cs add > $d/out 2>&1
 cmp $d/out $d/expected
 
-# Regression test for -0 / --nul-output
+# Regression test for --raw-output0
 printf "a\0b\0" > $d/expected
-printf '["a", "b"]' | $JQ -0 .[] > $d/out 2>&1
-cmp $d/out $d/expected
-printf '["a", "b"]' | $JQ --nul-output .[] > $d/out 2>&1
+printf '["a", "b"]' | $VALGRIND $Q $JQ --raw-output0 .[] > $d/out
 cmp $d/out $d/expected
+printf "a\0" > $d/expected
+if printf '["a", "c\\u0000d", "b"]' | $VALGRIND $Q $JQ --raw-output0 .[] > $d/out; then
+  echo "Should exit error on string containing NUL with --raw-output0" 1>&2
+  exit 1
+elif [ $? -ne 5 ]; then
+  echo "Invalid error code" 1>&2
+  exit 1
+else
+  cmp $d/out $d/expected
+fi
 
 ## Test streaming parser