| #include <assert.h> |
| #include <ctype.h> |
| #include <errno.h> |
| #include <libgen.h> |
| #include <stdlib.h> |
| #include <stdio.h> |
| #include <string.h> |
| #include <unistd.h> |
| |
| #ifdef WIN32 |
| #include <windows.h> |
| #include <io.h> |
| #include <fcntl.h> |
| #include <processenv.h> |
| #include <shellapi.h> |
| #include <wchar.h> |
| #include <wtypes.h> |
| #endif |
| |
| #include "compile.h" |
| #include "jv.h" |
| #include "jq.h" |
| #include "jv_alloc.h" |
| #include "util.h" |
| #include "version.h" |
| |
| int jq_testsuite(jv lib_dirs, int verbose, int argc, char* argv[]); |
| |
| static const char* progname; |
| |
| /* |
| * For a longer help message we could use a better option parsing |
| * strategy, one that lets stack options. |
| */ |
| static void usage(int code) { |
| FILE *f = stderr; |
| |
| if (code == 0) |
| f = stdout; |
| |
| int ret = fprintf(f, |
| "jq - commandline JSON processor [version %s]\n" |
| "Usage: %s [options] <jq filter> [file...]\n\n" |
| "\tjq is a tool for processing JSON inputs, applying the\n" |
| "\tgiven filter to its JSON text inputs and producing the\n" |
| "\tfilter's results as JSON on standard output.\n" |
| "\tThe simplest filter is ., which is the identity filter,\n" |
| "\tcopying jq's input to its output unmodified (except for\n" |
| "\tformatting).\n" |
| "\tFor more advanced filters see the jq(1) manpage (\"man jq\")\n" |
| "\tand/or http://stedolan.github.io/jq\n\n" |
| "\tSome of the options include:\n" |
| "\t -c\t\tcompact instead of pretty-printed output;\n" |
| "\t -n\t\tuse `null` as the single input value;\n" |
| "\t -e\t\tset the exit status code based on the output;\n" |
| "\t -s\t\tread (slurp) all inputs into an array; apply filter to it;\n" |
| "\t -r\t\toutput raw strings, not JSON texts;\n" |
| "\t -R\t\tread raw strings, not JSON texts;\n" |
| "\t -C\t\tcolorize JSON;\n" |
| "\t -M\t\tmonochrome (don't colorize JSON);\n" |
| "\t -S\t\tsort keys of objects on output;\n" |
| "\t --tab\tuse tabs for indentation;\n" |
| "\t --arg a v\tset variable $a to value <v>;\n" |
| "\t --argjson a v\tset variable $a to JSON value <v>;\n" |
| "\t --slurpfile a f\tset variable $a to an array of JSON texts read from <f>;\n" |
| "\tSee the manpage for more options.\n", JQ_VERSION, progname); |
| exit((ret < 0 && code == 0) ? 2 : code); |
| } |
| |
| static void die() { |
| fprintf(stderr, "Use %s --help for help with command-line options,\n", progname); |
| fprintf(stderr, "or see the jq manpage, or online docs at http://stedolan.github.io/jq\n"); |
| exit(2); |
| } |
| |
| |
| |
| |
| static int isoptish(const char* text) { |
| return text[0] == '-' && (text[1] == '-' || isalpha(text[1])); |
| } |
| |
| static int isoption(const char* text, char shortopt, const char* longopt, size_t *short_opts) { |
| if (text[0] != '-' || text[1] == '-') |
| *short_opts = 0; |
| if (text[0] != '-') return 0; |
| |
| // check long option |
| if (text[1] == '-' && !strcmp(text+2, longopt)) return 1; |
| else if (text[1] == '-') return 0; |
| |
| // must be short option; check it and... |
| if (!shortopt) return 0; |
| if (strchr(text, shortopt) != NULL) { |
| (*short_opts)++; // ...count it (for option stacking) |
| return 1; |
| } |
| return 0; |
| } |
| |
| enum { |
| SLURP = 1, |
| RAW_INPUT = 2, |
| PROVIDE_NULL = 4, |
| RAW_OUTPUT = 8, |
| ASCII_OUTPUT = 32, |
| COLOUR_OUTPUT = 64, |
| NO_COLOUR_OUTPUT = 128, |
| SORTED_OUTPUT = 256, |
| FROM_FILE = 512, |
| RAW_NO_LF = 1024, |
| UNBUFFERED_OUTPUT = 2048, |
| EXIT_STATUS = 4096, |
| SEQ = 8192, |
| RUN_TESTS = 16384, |
| /* debugging only */ |
| DUMP_DISASM = 32768, |
| }; |
| static int options = 0; |
| |
| static int process(jq_state *jq, jv value, int flags, int dumpopts) { |
| int ret = 14; // 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) { |
| fwrite(jv_string_value(result), 1, jv_string_length_bytes(jv_copy(result)), stdout); |
| ret = 0; |
| jv_free(result); |
| } else { |
| if (jv_get_kind(result) == JV_KIND_FALSE || jv_get_kind(result) == JV_KIND_NULL) |
| ret = 11; |
| else |
| ret = 0; |
| if (options & SEQ) |
| priv_fwrite("\036", 1, stdout, dumpopts & JV_PRINT_ISATTY); |
| jv_dump(result, dumpopts); |
| } |
| if (!(options & RAW_NO_LF)) |
| priv_fwrite("\n", 1, stdout, dumpopts & JV_PRINT_ISATTY); |
| if (options & UNBUFFERED_OUTPUT) |
| fflush(stdout); |
| } |
| 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 (at %s): %s\n", |
| jv_string_value(input_pos), jv_string_value(msg)); |
| } else { |
| msg = jv_dump_string(msg, 0); |
| 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); |
| return ret; |
| } |
| |
| static void debug_cb(void *data, jv input) { |
| int dumpopts = *(int *)data; |
| jv_dumpf(JV_ARRAY(jv_string("DEBUG:"), input), stderr, dumpopts & ~(JV_PRINT_PRETTY)); |
| fprintf(stderr, "\n"); |
| } |
| |
| int main(int argc, char* argv[]) { |
| jq_state *jq = NULL; |
| int ret = 0; |
| int compiled = 0; |
| int parser_flags = 0; |
| int nfiles = 0; |
| int badwrite; |
| jv program_arguments = jv_array(); |
| |
| #ifdef WIN32 |
| SetConsoleOutputCP(CP_UTF8); |
| fflush(stdout); |
| fflush(stderr); |
| _setmode(fileno(stdout), _O_TEXT | _O_U8TEXT); |
| _setmode(fileno(stderr), _O_TEXT | _O_U8TEXT); |
| int wargc; |
| wchar_t **wargv = CommandLineToArgvW(GetCommandLineW(), &wargc); |
| assert(wargc == argc); |
| size_t arg_sz; |
| for (int i = 0; i < argc; i++) { |
| argv[i] = alloca((arg_sz = WideCharToMultiByte(CP_UTF8, |
| 0, |
| wargv[i], |
| -1, 0, 0, 0, 0))); |
| WideCharToMultiByte(CP_UTF8, 0, wargv[i], -1, argv[i], arg_sz, 0, 0); |
| } |
| #endif |
| |
| if (argc) progname = argv[0]; |
| |
| jq = jq_init(); |
| if (jq == NULL) { |
| perror("malloc"); |
| ret = 2; |
| goto out; |
| } |
| |
| int dumpopts = JV_PRINT_INDENT_FLAGS(2); |
| const char* program = 0; |
| |
| jq_util_input_state *input_state = jq_util_input_init(NULL, NULL); // XXX add err_cb |
| |
| int further_args_are_files = 0; |
| int jq_flags = 0; |
| size_t short_opts = 0; |
| jv lib_search_paths = jv_null(); |
| for (int i=1; i<argc; i++, short_opts = 0) { |
| if (further_args_are_files) { |
| jq_util_input_add_input(input_state, argv[i]); |
| nfiles++; |
| } else if (!strcmp(argv[i], "--")) { |
| if (!program) usage(2); |
| further_args_are_files = 1; |
| } else if (!isoptish(argv[i])) { |
| if (program) { |
| jq_util_input_add_input(input_state, argv[i]); |
| nfiles++; |
| } else { |
| program = argv[i]; |
| } |
| } else { |
| if (argv[i][1] == 'L') { |
| if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) |
| lib_search_paths = jv_array(); |
| if (argv[i][2] != 0) { // -Lname (faster check than strlen) |
| lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i]+2)); |
| } else if (i >= argc - 1) { |
| fprintf(stderr, "-L takes a parameter: (e.g. -L /search/path or -L/search/path)\n"); |
| die(); |
| } else { |
| lib_search_paths = jv_array_append(lib_search_paths, jv_string(argv[i+1])); |
| i++; |
| } |
| continue; |
| } |
| |
| if (isoption(argv[i], 's', "slurp", &short_opts)) { |
| options |= SLURP; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'r', "raw-output", &short_opts)) { |
| options |= RAW_OUTPUT; |
| 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; |
| } |
| if (isoption(argv[i], 'C', "color-output", &short_opts)) { |
| options |= COLOUR_OUTPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'M', "monochrome-output", &short_opts)) { |
| options |= NO_COLOUR_OUTPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'a', "ascii-output", &short_opts)) { |
| options |= ASCII_OUTPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "unbuffered", &short_opts)) { |
| options |= UNBUFFERED_OUTPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'S', "sort-keys", &short_opts)) { |
| options |= SORTED_OUTPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'R', "raw-input", &short_opts)) { |
| options |= RAW_INPUT; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'n', "null-input", &short_opts)) { |
| options |= PROVIDE_NULL; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'f', "from-file", &short_opts)) { |
| 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, "tab", &short_opts)) { |
| dumpopts &= ~JV_PRINT_INDENT_FLAGS(7); |
| dumpopts |= JV_PRINT_TAB | JV_PRINT_PRETTY; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "indent", &short_opts)) { |
| if (i >= argc - 1) { |
| fprintf(stderr, "%s: --indent takes one parameter\n", progname); |
| die(); |
| } |
| dumpopts &= ~(JV_PRINT_TAB | JV_PRINT_INDENT_FLAGS(7)); |
| int indent = atoi(argv[i+1]); |
| if (indent < -1 || indent > 7) { |
| fprintf(stderr, "%s: --indent takes a number between -1 and 7\n", progname); |
| die(); |
| } |
| dumpopts |= JV_PRINT_INDENT_FLAGS(indent); |
| i++; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "seq", &short_opts)) { |
| 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; |
| } |
| // FIXME: For --arg* we should check that the varname is acceptable |
| if (isoption(argv[i], 0, "arg", &short_opts)) { |
| if (i >= argc - 2) { |
| fprintf(stderr, "%s: --arg takes two parameters (e.g. --arg varname value)\n", progname); |
| die(); |
| } |
| jv arg = jv_object(); |
| arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1])); |
| arg = jv_object_set(arg, jv_string("value"), jv_string(argv[i+2])); |
| program_arguments = jv_array_append(program_arguments, arg); |
| i += 2; // skip the next two arguments |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "argjson", &short_opts)) { |
| if (i >= argc - 2) { |
| fprintf(stderr, "%s: --argjson takes two parameters (e.g. --argjson varname text)\n", progname); |
| die(); |
| } |
| jv v = jv_parse(argv[i+2]); |
| if (!jv_is_valid(v)) { |
| fprintf(stderr, "%s: invalid JSON text passed to --argjson\n", progname); |
| die(); |
| } |
| jv arg = jv_object(); |
| arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1])); |
| arg = jv_object_set(arg, jv_string("value"), v); |
| program_arguments = jv_array_append(program_arguments, arg); |
| i += 2; // skip the next two arguments |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "argfile", &short_opts) || |
| isoption(argv[i], 0, "slurpfile", &short_opts)) { |
| const char *which; |
| if (isoption(argv[i], 0, "argfile", &short_opts)) |
| which = "argfile"; |
| else |
| which = "slurpfile"; |
| if (i >= argc - 2) { |
| fprintf(stderr, "%s: --%s takes two parameters (e.g. --%s varname filename)\n", progname, which, which); |
| die(); |
| } |
| jv arg = jv_object(); |
| arg = jv_object_set(arg, jv_string("name"), jv_string(argv[i+1])); |
| jv data = jv_load_file(argv[i+2], 0); |
| if (!jv_is_valid(data)) { |
| data = jv_invalid_get_msg(data); |
| fprintf(stderr, "%s: Bad JSON in --%s %s %s: %s\n", progname, which, |
| argv[i+1], argv[i+2], jv_string_value(data)); |
| jv_free(data); |
| jv_free(arg); |
| ret = 2; |
| goto out; |
| } |
| if (isoption(argv[i], 0, "argfile", &short_opts) && |
| jv_get_kind(data) == JV_KIND_ARRAY && jv_array_length(jv_copy(data)) == 1) |
| data = jv_array_get(data, 0); |
| arg = jv_object_set(arg, jv_string("value"), data); |
| program_arguments = jv_array_append(program_arguments, arg); |
| i += 2; // skip the next two arguments |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "debug-dump-disasm", &short_opts)) { |
| options |= DUMP_DISASM; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 0, "debug-trace", &short_opts)) { |
| jq_flags |= JQ_DEBUG_TRACE; |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'h', "help", &short_opts)) { |
| usage(0); |
| if (!short_opts) continue; |
| } |
| if (isoption(argv[i], 'V', "version", &short_opts)) { |
| printf("jq-%s\n", JQ_VERSION); |
| ret = 0; |
| goto out; |
| } |
| if (isoption(argv[i], 0, "run-tests", &short_opts)) { |
| i++; |
| // XXX Pass program_arguments, even a whole jq_state *, through; |
| // could be useful for testing |
| ret = jq_testsuite(lib_search_paths, |
| (options & DUMP_DISASM) || (jq_flags & JQ_DEBUG_TRACE), |
| argc - i, argv + i); |
| goto out; |
| } |
| |
| // check for unknown options... if this argument was a short option |
| if (strlen(argv[i]) != short_opts + 1) { |
| fprintf(stderr, "%s: Unknown option %s\n", progname, argv[i]); |
| die(); |
| } |
| } |
| } |
| |
| if (isatty(fileno(stdout))) { |
| dumpopts |= JV_PRINT_ISATTY; |
| #ifndef WIN32 |
| /* Disable colour by default on Windows builds as Windows |
| terminals tend not to display it correctly */ |
| dumpopts |= JV_PRINT_COLOUR; |
| #endif |
| } |
| if (options & SORTED_OUTPUT) dumpopts |= JV_PRINT_SORTED; |
| if (options & ASCII_OUTPUT) dumpopts |= JV_PRINT_ASCII; |
| if (options & COLOUR_OUTPUT) dumpopts |= JV_PRINT_COLOUR; |
| if (options & NO_COLOUR_OUTPUT) dumpopts &= ~JV_PRINT_COLOUR; |
| |
| if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) { |
| // Default search path list |
| lib_search_paths = JV_ARRAY(jv_string("~/.jq"), |
| jv_string("$ORIGIN/../lib/jq"), |
| jv_string("$ORIGIN/lib")); |
| } |
| jq_set_attr(jq, jv_string("JQ_LIBRARY_PATH"), lib_search_paths); |
| |
| char *origin = strdup(argv[0]); |
| if (origin == NULL) { |
| fprintf(stderr, "Error: out of memory\n"); |
| exit(1); |
| } |
| jq_set_attr(jq, jv_string("JQ_ORIGIN"), jv_string(dirname(origin))); |
| free(origin); |
| |
| if (strchr(JQ_VERSION, '-') == NULL) |
| jq_set_attr(jq, jv_string("VERSION_DIR"), jv_string(JQ_VERSION)); |
| else |
| jq_set_attr(jq, jv_string("VERSION_DIR"), jv_string_fmt("%.*s-master", (int)(strchr(JQ_VERSION, '-') - JQ_VERSION), JQ_VERSION)); |
| |
| #if (!defined(WIN32) && defined(HAVE_ISATTY)) || defined(HAVE__ISATTY) |
| |
| #if defined(HAVE__ISATTY) && defined(isatty) |
| #undef isatty |
| #define isatty _isatty |
| #endif |
| |
| if (!program && isatty(STDOUT_FILENO) && !isatty(STDIN_FILENO)) |
| program = "."; |
| #endif |
| |
| if (!program) usage(2); |
| |
| if (options & FROM_FILE) { |
| char *program_origin = strdup(program); |
| if (program_origin == NULL) { |
| perror("malloc"); |
| exit(2); |
| } |
| |
| jv data = jv_load_file(program, 1); |
| if (!jv_is_valid(data)) { |
| data = jv_invalid_get_msg(data); |
| fprintf(stderr, "%s: %s\n", progname, jv_string_value(data)); |
| jv_free(data); |
| ret = 2; |
| goto out; |
| } |
| jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string(dirname(program_origin)))); |
| compiled = jq_compile_args(jq, jv_string_value(data), jv_copy(program_arguments)); |
| free(program_origin); |
| jv_free(data); |
| } else { |
| jq_set_attr(jq, jv_string("PROGRAM_ORIGIN"), jq_realpath(jv_string("."))); // XXX is this good? |
| compiled = jq_compile_args(jq, program, jv_copy(program_arguments)); |
| } |
| if (!compiled){ |
| ret = 3; |
| goto out; |
| } |
| |
| if (options & DUMP_DISASM) { |
| jq_dump_disassembly(jq, 0); |
| printf("\n"); |
| } |
| |
| if ((options & SEQ)) |
| parser_flags |= JV_PARSE_SEQ; |
| |
| if ((options & RAW_INPUT)) |
| jq_util_input_set_parser(input_state, NULL, (options & SLURP) ? 1 : 0); |
| else |
| jq_util_input_set_parser(input_state, jv_parser_new(parser_flags), (options & SLURP) ? 1 : 0); |
| |
| // Let jq program read from inputs |
| jq_set_input_cb(jq, jq_util_input_next_input_cb, input_state); |
| |
| // Let jq program call `debug` builtin and have that go somewhere |
| jq_set_debug_cb(jq, debug_cb, &dumpopts); |
| |
| if (nfiles == 0) |
| jq_util_input_add_input(input_state, "-"); |
| |
| if (options & PROVIDE_NULL) { |
| ret = process(jq, jv_null(), jq_flags, dumpopts); |
| } 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); |
| continue; |
| } |
| |
| // 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; |
| } |
| fprintf(stderr, "ignoring parse error: %s\n", jv_string_value(msg)); |
| jv_free(msg); |
| } |
| } |
| |
| if (jq_util_input_errors(input_state) != 0) |
| ret = 2; |
| |
| out: |
| badwrite = ferror(stdout); |
| if (fclose(stdout)!=0 || badwrite) { |
| fprintf(stderr,"Error: writing output failed: %s\n", strerror(errno)); |
| ret = 2; |
| } |
| |
| jv_free(program_arguments); |
| jq_util_input_free(&input_state); |
| jq_teardown(&jq); |
| if (ret >= 10 && (options & EXIT_STATUS)) |
| return ret - 10; |
| if (ret >= 10) |
| return 0; |
| return ret; |
| } |