Implement `_experimental_snapshot/2`
Enables writing to files from within jq scripts.

Resolves #3153

The scary name and lack of documentation should serve to inform users that its behavior is subject to breaking changes, and should therefore not be used in production scripts for the time being.

At the moment, this builtin performs a dry run by default. To actually write any data to disk, set the environment variable `JQ_ENABLE_SNAPSHOT=1`. A proper sandboxing interface is outside the scope of this PR, please refer to the link below instead:
* #3092

I've renamed this builtin to `snapshot/2` to highlight the fact that script execution is unaffected by the results of the file writing process (dry run, success, failure), allowing scripts that use it to remain constant and repeatable.

This allows the language to maintain referential transparency even when interacting with the outside world.
I would like to propose a new builtin, say `output_file("filename"; "contents")`, which copies its input to its output while saving its arguments as a file. [It's similar in principle to the `debug` builtin.](https://jqlang.github.io/jq/manual/#debug)

If [`--sandbox`](https://github.com/jqlang/jq/pull/3092) is specified, it will simply output the filename to stderr, essentially acting as a dry run.

Having this builtin would make it less awkward to split json files. See below for some workarounds that are currently required:
* #2438
* #3121
* https://stackoverflow.com/questions/70569726/jq-split-json-in-several-files

Proposed semantics:

```jq
# sample script
to_entries[] | output_file(.key; .value) | .key

# stdin
{
	"a.txt": "string\nstring",
	"b/c.txt": "invalid",
	"d.json": {
		"e": 10
	}
}

# stdout
"a.txt"
"b/c.txt"
"d.json"

# stderr
b/c.txt: No such file or directory


# a.txt
string
string

# d.json
{"e":10}
```

Add builtin to output to file
diff --git a/src/builtin.c b/src/builtin.c
index da4a770fac..29a6e4e21e 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -1346,7 +1346,38 @@ static jv f_input(jq_state *jq, jv input) {
     return v;
   return jv_invalid_with_msg(jv_string("break"));
 }
+static jv f_snapshot(jq_state *jq, jv input, jv filename, jv data) {
+  if (jv_get_kind(filename) != JV_KIND_STRING)
+    return ret_error(filename, jv_string("filename must be a string"));
 
+  const char *name_str = jv_string_value(filename);
+  int sandbox = 1;
+  {
+    // TODO: replace this when --sandbox is implemented
+    const char *disable_sandbox = getenv("JQ_ENABLE_SNAPSHOT");
+    if (disable_sandbox && *disable_sandbox)
+      sandbox = 0;
+  }
+  if (sandbox) {
+    fprintf(stderr, "jq: dry run: %s (set JQ_ENABLE_SNAPSHOT=1)\n", name_str);
+  } else {
+    FILE *fp = fopen(name_str, "wb");
+    if (!fp) {
+      //perror("fopen");
+      fprintf(stderr, "jq: error: could not open %s for writing\n", name_str);
+    } else {
+      if (jv_get_kind(data) == JV_KIND_STRING)
+        priv_fwrite(jv_string_value(data), jv_string_length_bytes(jv_copy(data)), fp, 0);
+      else
+        jv_dumpf(jv_copy(data), fp, JV_PRINT_ASCII);
+      fclose(fp);
+    }
+  }
+
+  jv_free(filename);
+  jv_free(data);
+  return input;
+}
 static jv f_debug(jq_state *jq, jv input) {
   jq_msg_cb cb;
   void *data;
@@ -1856,6 +1887,7 @@ BINOPS
   {f_match, "_match_impl", 4},
   {f_modulemeta, "modulemeta", 1},
   {f_input, "input", 1},
+  {f_snapshot, "_experimental_snapshot", 3},
   {f_debug, "debug", 1},
   {f_stderr, "stderr", 1},
   {f_strptime, "strptime", 2},

diff --git a/tests/shtest b/tests/shtest
index 4aa27823ce..a325a5dde6 100755
--- a/tests/shtest
+++ b/tests/shtest
@@ -275,6 +275,28 @@ grep "Expected string key after '{', not '\\['" $d/err > /dev/null
 echo '{"x":"y",["a","b"]}' | $JQ --stream > /dev/null 2> $d/err || true
 grep "Expected string key after ',' in object, not '\\['" $d/err > /dev/null
 
+## Test IO
+
+# snapshot
+cat <<EOF | (cd $d; JQ_ENABLE_SNAPSHOT=1 $VALGRIND $Q $JQ -r 'to_entries[] | _experimental_snapshot(.key;.value) | .key' >keys)
+{
+  "a.txt": "aaa",
+  "b/b.json": "invalid",
+  "c/c.json": ["not","valid"],
+  "d.json": {"e":10},
+  "f.json": 8
+}
+EOF
+(cd $d; ! cat $(cat keys) keys >out)
+cat > $d/expected <<'EOF'
+aaa{"e":10}8a.txt
+b/b.json
+c/c.json
+d.json
+f.json
+EOF
+cmp $d/out $d/expected
+
 # debug, stderr
 $VALGRIND $Q $JQ -n '"test", {} | debug, stderr' >/dev/null
 $JQ -n -c -j '"hello\nworld", null, [false, 0], {"foo":["bar"]}, "\n" | stderr' >$d/out 2>$d/err

{"tests/shtest": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"test_options": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "regset": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "callback_each_match": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/utf8test": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "names": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test_utf8": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "simple": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/base64test": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/uritest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "echo": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/mantest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "testc": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test_syntax": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test_back": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "bug_fix": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/jqtest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "scan": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/onigtest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "encode": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "callout": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "listcap": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/optionaltest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "user_property": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "tests/manonigtest": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "test_regset": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "testcu": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "count": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "syntax": {"run": "PASS", "test": "PASS", "fix": "PASS"}, "sql": {"run": "PASS", "test": "PASS", "fix": "PASS"}}
{"tests/shtest": {"run": "PASS", "test": "FAIL", "fix": "PASS"}}
{"passed_count": 30, "failed_count": 0, "skipped_count": 0, "passed_tests": ["regset", "callback_each_match", "names", "test_utf8", "testc", "test_syntax", "sql", "bug_fix", "scan", "tests/onigtest", "encode", "listcap", "tests/optionaltest", "tests/manonigtest", "test_regset", "testcu", "count", "syntax", "test_options", "tests/utf8test", "simple", "tests/base64test", "tests/uritest", "tests/mantest", "test_back", "tests/jqtest", "tests/shtest", "callout", "user_property", "echo"], "failed_tests": [], "skipped_tests": []}
{"passed_count": 29, "failed_count": 1, "skipped_count": 0, "passed_tests": ["regset", "callback_each_match", "names", "test_utf8", "testc", "test_syntax", "sql", "bug_fix", "scan", "tests/onigtest", "encode", "listcap", "tests/optionaltest", "tests/manonigtest", "test_regset", "testcu", "count", "syntax", "test_options", "tests/utf8test", "simple", "tests/base64test", "tests/uritest", "tests/mantest", "test_back", "tests/jqtest", "callout", "user_property", "echo"], "failed_tests": ["tests/shtest"], "skipped_tests": []}
{"passed_count": 30, "failed_count": 0, "skipped_count": 0, "passed_tests": ["regset", "callback_each_match", "names", "test_utf8", "testc", "test_syntax", "sql", "bug_fix", "scan", "tests/onigtest", "encode", "listcap", "tests/optionaltest", "tests/manonigtest", "test_regset", "testcu", "count", "syntax", "test_options", "tests/utf8test", "simple", "tests/base64test", "tests/uritest", "tests/mantest", "test_back", "tests/jqtest", "tests/shtest", "callout", "user_property", "echo"], "failed_tests": [], "skipped_tests": []}