-
Notifications
You must be signed in to change notification settings - Fork 1.8k
Add --output-file flag for writing values to file
#3410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 6 commits
54c65e2
9dab206
39cdf29
59bcf68
16b8402
c102d05
5be80fe
78314f5
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -227,6 +227,14 @@ sections: | |
| like awk's -f option. This changes the filter argument to be | ||
| interpreted as a filename, instead of the source of a program. | ||
|
|
||
| * `-o` / `--output-file filename`: | ||
|
|
||
| Write output values to the named file instead of standard out. | ||
|
|
||
| The outputs from `--debug-dump-disasm` and `--debug-trace` are | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe these should always have gone to |
||
| still written to standard out, so this option can be used to | ||
| separate them. | ||
|
|
||
| * `-L directory` / `--library-path directory`: | ||
|
|
||
| Prepend `directory` to the search list for modules. If this | ||
|
|
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,13 +1,15 @@ | ||
| #include <assert.h> | ||
| #include <ctype.h> | ||
| #include <errno.h> | ||
| #include <fcntl.h> | ||
| #include <libgen.h> | ||
| #ifdef HAVE_SETLOCALE | ||
| #include <locale.h> | ||
| #endif | ||
| #include <stdlib.h> | ||
| #include <stdio.h> | ||
| #include <string.h> | ||
| #include <sys/stat.h> | ||
| #include <unistd.h> | ||
|
|
||
| #ifdef WIN32 | ||
|
|
@@ -92,6 +94,7 @@ static void usage(int code, int keep_it_short) { | |
| " an array;\n" | ||
| " --seq parse input/output as application/json-seq;\n" | ||
| " -f, --from-file load the filter from a file;\n" | ||
| " -o, --output-file file output to file instead of stdout;\n" | ||
| " -L, --library-path dir search modules from the directory;\n" | ||
| " --arg name value set $name to the string value;\n" | ||
| " --argjson name value set $name to the JSON value;\n" | ||
|
|
@@ -143,22 +146,24 @@ static int isoption(const char** text, char shortopt, const char* longopt, int i | |
| } | ||
|
|
||
| enum { | ||
| SLURP = 1, | ||
| RAW_INPUT = 2, | ||
| PROVIDE_NULL = 4, | ||
| RAW_OUTPUT = 8, | ||
| RAW_OUTPUT0 = 16, | ||
| ASCII_OUTPUT = 32, | ||
| COLOR_OUTPUT = 64, | ||
| NO_COLOR_OUTPUT = 128, | ||
| SORTED_OUTPUT = 256, | ||
| FROM_FILE = 512, | ||
| RAW_NO_LF = 1024, | ||
| UNBUFFERED_OUTPUT = 2048, | ||
| EXIT_STATUS = 4096, | ||
| SEQ = 16384, | ||
| /* debugging only */ | ||
| DUMP_DISASM = 32768, | ||
| SLURP = 1 << 0, | ||
| RAW_INPUT = 1 << 1, | ||
| PROVIDE_NULL = 1 << 2, | ||
| RAW_OUTPUT = 1 << 3, | ||
| RAW_OUTPUT0 = 1 << 4, | ||
| ASCII_OUTPUT = 1 << 5, | ||
| COLOR_OUTPUT = 1 << 6, | ||
| NO_COLOR_OUTPUT = 1 << 7, | ||
| SORTED_OUTPUT = 1 << 8, | ||
| FROM_FILE = 1 << 9, | ||
| RAW_NO_LF = 1 << 10, | ||
| UNBUFFERED_OUTPUT = 1 << 11, | ||
| EXIT_STATUS = 1 << 12, | ||
| SEQ = 1 << 13, | ||
| DUMP_DISASM = 1 << 14, /* debugging only */ | ||
| #ifdef WIN32 | ||
| BINARY_OUTPUT = 1 << 15, | ||
|
thaliaarchi marked this conversation as resolved.
|
||
| #endif | ||
| }; | ||
|
|
||
| enum { | ||
|
|
@@ -172,22 +177,22 @@ enum { | |
| #define jq_exit_with_status(r) exit(abs(r)) | ||
| #define jq_exit(r) exit( r > 0 ? r : 0 ) | ||
|
|
||
| static int process(jq_state *jq, jv value, int flags, int dumpopts, int options) { | ||
| static int process(jq_state *jq, jv value, FILE *ofile, int flags, int dumpopts, int options) { | ||
| int ret = JQ_OK_NO_OUTPUT; // 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) { | ||
| if (options & ASCII_OUTPUT) { | ||
| jv_dumpf(jv_copy(result), stdout, JV_PRINT_ASCII); | ||
| jv_dumpf(jv_copy(result), ofile, 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); | ||
| ofile, dumpopts & JV_PRINT_ISATTY); | ||
| } | ||
| ret = JQ_OK; | ||
| jv_free(result); | ||
|
|
@@ -197,15 +202,15 @@ static int process(jq_state *jq, jv value, int flags, int dumpopts, int options) | |
| else | ||
| ret = JQ_OK; | ||
| if (options & SEQ) | ||
| priv_fwrite("\036", 1, stdout, dumpopts & JV_PRINT_ISATTY); | ||
| jv_dump(result, dumpopts); | ||
| priv_fwrite("\036", 1, ofile, dumpopts & JV_PRINT_ISATTY); | ||
| jv_dumpf(result, ofile, dumpopts); | ||
| } | ||
| if (!(options & RAW_NO_LF)) | ||
| priv_fwrite("\n", 1, stdout, dumpopts & JV_PRINT_ISATTY); | ||
| priv_fwrite("\n", 1, ofile, dumpopts & JV_PRINT_ISATTY); | ||
| if (options & RAW_OUTPUT0) | ||
| priv_fwrite("\0", 1, stdout, dumpopts & JV_PRINT_ISATTY); | ||
| priv_fwrite("\0", 1, ofile, dumpopts & JV_PRINT_ISATTY); | ||
| if (options & UNBUFFERED_OUTPUT) | ||
| fflush(stdout); | ||
| fflush(ofile); | ||
| } | ||
| if (jq_halted(jq)) { | ||
| // jq program invoked `halt` or `halt_error` | ||
|
|
@@ -294,6 +299,7 @@ int main(int argc, char* argv[]) { | |
| int compiled = 0; | ||
| int parser_flags = 0; | ||
| int nfiles = 0; | ||
| FILE *ofile = stdout; | ||
| int last_result = -1; /* -1 = no result, 0=null or false, 1=true */ | ||
| int badwrite; | ||
| int options = 0; | ||
|
|
@@ -404,6 +410,23 @@ int main(int argc, char* argv[]) { | |
| options |= PROVIDE_NULL; | ||
| } else if (isoption(&text, 'f', "from-file", is_short)) { | ||
| options |= FROM_FILE; | ||
| } else if (isoption(&text, 'o', "output-file", is_short)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. what should happen if output file arg is used more than once?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The file from the first flag is opened, then closed when the second flag is parsed. I think this is fine.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No. Each file will be truncated, and only the last one will be written to (after being truncated). This seems like a footgun. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What should the user interface be in this case? Should In any case, it seems to me that the fopen/fclose bit needs to move to after all options have been parsed into the proximity of line 565 - because even if it should be allowed to specify -o only once, the code could catch a violation of that rule only after the first output file has already been truncated, which would potentially truncate inputs. |
||
| options |= NO_COLOR_OUTPUT; | ||
| if (i >= argc - 1) { | ||
| fprintf(stderr, "jq: --output-file takes one parameter (e.g. --output-file filename)\n"); | ||
| die(); | ||
| } | ||
| if (ofile != stdout) | ||
| fclose(ofile); | ||
| ofile = fopen(argv[i+1], "w"); | ||
| if (!ofile) { | ||
| fprintf(stderr, "jq: Could not open --output-file %s: %s\n", argv[i+1], strerror(errno)); | ||
| die(); | ||
| } | ||
| #ifdef WIN32 | ||
| _setmode(fileno(ofile), options & BINARY_OUTPUT ? _O_BINARY : _O_TEXT | _O_U8TEXT); | ||
|
thaliaarchi marked this conversation as resolved.
|
||
| #endif | ||
| i += 1; // skip the next argument | ||
| } else if (isoption(&text, 'L', "library-path", is_short)) { | ||
| if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) | ||
| lib_search_paths = jv_array(); | ||
|
|
@@ -419,9 +442,14 @@ int main(int argc, char* argv[]) { | |
| } | ||
| } else if (isoption(&text, 'b', "binary", is_short)) { | ||
| #ifdef WIN32 | ||
| options |= BINARY_OUTPUT; | ||
| if (ofile != stdout) { | ||
| fflush(ofile); | ||
| _setmode(fileno(ofile), _O_BINARY); | ||
|
thaliaarchi marked this conversation as resolved.
|
||
| } | ||
| fflush(stdout); | ||
| fflush(stderr); | ||
| _setmode(fileno(stdin), _O_BINARY); | ||
| _setmode(fileno(stdin), _O_BINARY); | ||
| _setmode(fileno(stdout), _O_BINARY); | ||
| _setmode(fileno(stderr), _O_BINARY); | ||
| #endif | ||
|
|
@@ -537,7 +565,7 @@ int main(int argc, char* argv[]) { | |
| } | ||
|
|
||
| #ifdef USE_ISATTY | ||
| if (isatty(STDOUT_FILENO)) { | ||
| if (ofile == stdout && isatty(STDOUT_FILENO)) { | ||
| #ifndef WIN32 | ||
| dumpopts |= JV_PRINT_ISATTY | JV_PRINT_COLOR; | ||
| #else | ||
|
|
@@ -590,7 +618,7 @@ int main(int argc, char* argv[]) { | |
| jq_set_attr(jq, jv_string("VERSION_DIR"), jv_string_fmt("%.*s-master", (int)(strchr(JQ_VERSION, '-') - JQ_VERSION), JQ_VERSION)); | ||
|
|
||
| #ifdef USE_ISATTY | ||
| if (!program && !(options & FROM_FILE) && (!isatty(STDOUT_FILENO) || !isatty(STDIN_FILENO))) | ||
| if (!program && !(options & FROM_FILE) && (ofile != stdout || !isatty(STDOUT_FILENO) || !isatty(STDIN_FILENO))) | ||
| program = "."; | ||
| #endif | ||
|
|
||
|
|
@@ -664,13 +692,13 @@ int main(int argc, char* argv[]) { | |
| jq_util_input_add_input(input_state, "-"); | ||
|
|
||
| if (options & PROVIDE_NULL) { | ||
| ret = process(jq, jv_null(), jq_flags, dumpopts, options); | ||
| ret = process(jq, jv_null(), ofile, jq_flags, dumpopts, options); | ||
| } 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, options); | ||
| ret = process(jq, value, ofile, jq_flags, dumpopts, options); | ||
| if (ret <= 0 && ret != JQ_OK_NO_OUTPUT) | ||
| last_result = (ret != JQ_OK_NULL_KIND); | ||
| if (jq_halted(jq)) | ||
|
|
@@ -697,10 +725,17 @@ int main(int argc, char* argv[]) { | |
|
|
||
| out: | ||
| badwrite = ferror(stdout); | ||
| if (fclose(stdout)!=0 || badwrite) { | ||
| fprintf(stderr,"jq: error: writing output failed: %s\n", strerror(errno)); | ||
| if (fclose(stdout) != 0 || badwrite) { | ||
| fprintf(stderr, "jq: error: writing output failed: %s\n", strerror(errno)); | ||
| ret = JQ_ERROR_SYSTEM; | ||
| } | ||
| if (ofile != stdout) { | ||
| badwrite = ferror(ofile); | ||
| if (fclose(ofile) != 0 || badwrite) { | ||
| fprintf(stderr, "jq: error: writing output failed: %s\n", strerror(errno)); | ||
| ret = JQ_ERROR_SYSTEM; | ||
| } | ||
| } | ||
|
|
||
| jv_free(ARGS); | ||
| jv_free(program_arguments); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -807,6 +807,104 @@ printf '[\n {\n "a": 1\n }\n]\n' > $d/expected | |
| $JQ --indent 6 -n "[{a:1}]" > $d/out | ||
| cmp $d/out $d/expected | ||
|
|
||
| # Simple --output-file | ||
| printf '{"wide":"👋"}' > $d/expected | ||
| $JQ -j -c -o $d/out -n '{wide:"👋"}' | ||
| cmp $d/out $d/expected | ||
|
|
||
| # --output-file creates a file | ||
| rm $d/out | ||
| echo '"create"' > $d/expected | ||
| $JQ -o $d/out -n '"create"' | ||
| cmp $d/out $d/expected | ||
|
|
||
| # Program errors are not written to --output-file | ||
| echo 42 > $d/expected | ||
| echo 'jq: error (at <unknown>): error!' > $d/expected_err | ||
| ! $JQ -o $d/out -n '42, ("error!" | error)' 2> $d/err | ||
| cmp $d/out $d/expected | ||
| cmp $d/err $d/expected_err | ||
|
|
||
| # Disassembly is not written to --output-file | ||
| echo 42 > $d/expected | ||
| printf '0000 TOP\n0001 LOADK 42\n0003 RET\n\n' > $d/expected_disasm | ||
| $JQ --debug-dump-disasm -o $d/out -n 42 > $d/disasm | ||
| cmp $d/out $d/expected | ||
| cmp $d/disasm $d/expected_disasm | ||
|
|
||
| # Debug trace is not written to --output-file | ||
| echo 42 > $d/expected | ||
| printf '0000 TOP\t\n0001 LOADK 42\tnull\n0003 RET\t42\n0003 RET\t\t<backtracking>\n' > $d/expected_trace | ||
| $JQ --debug-trace -o $d/out -n 42 > $d/trace | ||
| cmp $d/out $d/expected | ||
| cmp $d/trace $d/expected_trace | ||
|
|
||
| # --output-file is one of the inputs | ||
| printf '1\n2\n' > $d/out | ||
| printf '' > $d/expected | ||
| $JQ -o $d/out '.+3' $d/out | ||
| cmp $d/out $d/expected | ||
|
|
||
| # --output-file with --from-file | ||
| echo hello > $d/out | ||
| printf '' > $d/expected | ||
| cat > $d/expected_err <<EOF | ||
| jq: error: Top-level program not given (try ".") | ||
| jq: 1 compile error | ||
| EOF | ||
| ! $JQ -f -o $d/out $d/out 2> $d/err | ||
| diff $d/out $d/expected | ||
| diff $d/err $d/expected_err | ||
|
|
||
| # --output-file before and after --rawfile | ||
| printf hello > $d/out | ||
| echo '""' > $d/expected | ||
| $JQ -o $d/out --rawfile arg $d/out -n '$arg' | ||
| diff $d/out $d/expected | ||
| printf hello > $d/out | ||
| echo '"hello"' > $d/expected | ||
| $JQ --rawfile arg $d/out -o $d/out -n '$arg' | ||
| diff $d/out $d/expected | ||
|
|
||
| # --output-file before and after --slurpfile | ||
| printf '"hello"\n"world"' > $d/out | ||
| echo '[]' > $d/expected | ||
| $JQ -o $d/out --slurpfile arg $d/out -nc '$arg' | ||
| diff $d/out $d/expected | ||
| printf '"hello"\n"world"' > $d/out | ||
| echo '["hello","world"]' > $d/expected | ||
| $JQ --slurpfile arg $d/out -o $d/out -nc '$arg' | ||
| diff $d/out $d/expected | ||
|
|
||
| # --output-file before and after --binary | ||
| printf '1\n2\n' > $d/expected | ||
| $JQ_NO_B -o $d/out --binary -n '1,2' | ||
| cmp $d/out $d/expected | ||
| $JQ_NO_B --binary -o $d/out -n '1,2' | ||
| cmp $d/out $d/expected | ||
|
|
||
| # --output-file before and after --color-output | ||
| echo '"hello"' > $d/expected | ||
| $JQ -o $d/out --color-output -n '"hello"' | ||
| cmp $d/out $d/expected | ||
| $JQ --color-output -o $d/out -n '"hello"' | ||
| cmp $d/out $d/expected | ||
|
|
||
| # --output-file without a filter uses . as the filter | ||
| printf '[\n 1,\n 2\n]\n' > $d/expected | ||
| echo '[1,2]' | $JQ -o $d/out | ||
| cmp $d/out $d/expected | ||
|
|
||
| # --output-file handles failure | ||
| ! $JQ -o $d/notfound/out -n . 2> $d/err | ||
| grep "jq: Could not open --output-file .*/notfound/out: No such file or directory" $d/err > /dev/null | ||
|
|
||
| # --output-file does not allow directories | ||
| mkdir $d/dir | ||
| ! $JQ -o $d/dir -n . 2> $d/err | ||
| grep "jq: Could not open --output-file .*/dir" $d/err > /dev/null | ||
| rmdir $d/dir | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is there any concerns to think about if output file is one of the input files? will get truncated before reading? streaming mode?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we can give a good experience for this, as the input files are opened after the output file is opened. Perhaps there's some way the file descriptors can be opened with copy-on-write semantics, so the input can still read the existing contents? I should write a test for this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that opening the file for reading, then opening it again for writing and truncating it, will allow the read fd to read the original contents. But we open them in the other order and inputs are opened lazily, so I don't know how to do this without eagerly opening all inputs, since they could all alias with sym/hardlinks. I'm skeptical it's worth it. Thoughts?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeap seems messy :( tiny bit worried someone will try to use this as a "--in-place" workaround and be disappointed :)
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's just add There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about storing the output file path and lazily opening the file when the full output is ready to be written? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This approach would mean all output must be kept in a buffer before it can be written. As such, streaming becomes impossible and the amount of data to be written is constrained by the amount of memory of the machine.I do not think there will be a great approach here. I can see these options
I am not sure how much work it would be. also there is the workaround of sorting the input files in case writing to one of these is desired. It is an edge case. Maybe documenting it for now could be good enough? Edit: In case this did not come through, I share @thaliaarchi s scepticism.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is a big deal. In general I expect tools that support |
||
|
|
||
| if ! $msys && ! $mingw; then | ||
| # Test handling of timezones -- #2429, #2475 | ||
| if ! r=$(TZ=Asia/Tokyo $JQ -rn '1731627341 | strflocaltime("%F %T %z %Z")') \ | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What are the rename-into-place or truncate-and-rewrite semantics? I think that needs to be stated clearly, though it's true that many tools that have
-o FILEoptions don't say so clearly.