Parse short options in order given

Fixes:
* Short options cannot be repeated in the same group:
  e.g., `-nn` fails, but `--null-input --null-input` works
* Short options are processed in an arbitrary fixed order, not the order
  given by the user: e.g., `-hV`, `-Vh`, `-h -V`, and `--help --version`
  are processed as `-h`, but `-V -h` and `--version --help` are
  processed as `-V`
* `-L` cannot be a part of a short option group (as the last one):
  e.g., `-nLlib` and `-nL lib` fail
This commit is contained in:
Thalia Archibald authored and Emanuele Torre committed 2024-11-08 19:07:55 +01:00
1 parent 562d5c5222
commit ff4acba769
2 files changed
+193 -225

No files matched your search

+171 -225
View File
@@ -129,20 +129,18 @@ static int isoptish(const char* text) {
return text[0] == '-' && (text[1] == '-' || isalpha((unsigned char)text[1])); return text[0] == '-' && (text[1] == '-' || isalpha((unsigned char)text[1]));
} }
static int isoption(const char* text, char shortopt, const char* longopt, size_t *short_opts) { static int isoption(const char** text, char shortopt, const char* longopt, int is_short) {
if (text[0] != '-' || text[1] == '-') if (is_short) {
*short_opts = 0; if (shortopt && **text == shortopt) {
if (text[0] != '-') return 0; (*text)++;
if (!**text) *text = NULL;
// check long option return 1;
if (text[1] == '-' && !strcmp(text+2, longopt)) return 1; }
else if (text[1] == '-') return 0; } else {
if (longopt != NULL && !strcmp(*text, longopt)) {
// must be short option; check it and... *text = NULL;
if (!shortopt) return 0; return 1;
if (strchr(text, shortopt) != NULL) { }
(*short_opts)++; // ...count it (for option stacking)
return 1;
} }
return 0; return 0;
} }
@@ -344,9 +342,8 @@ int main(int argc, char* argv[]) {
int further_args_are_json = 0; int further_args_are_json = 0;
int args_done = 0; int args_done = 0;
int jq_flags = 0; int jq_flags = 0;
size_t short_opts = 0;
jv lib_search_paths = jv_null(); jv lib_search_paths = jv_null();
for (int i=1; i<argc; i++, short_opts = 0) { for (int i=1; i<argc; i++) {
if (args_done || !isoptish(argv[i])) { if (args_done || !isoptish(argv[i])) {
if (!program) { if (!program) {
program = argv[i]; program = argv[i];
@@ -366,223 +363,172 @@ int main(int argc, char* argv[]) {
} else if (!strcmp(argv[i], "--")) { } else if (!strcmp(argv[i], "--")) {
args_done = 1; args_done = 1;
} else { } else {
if (argv[i][1] == 'L') { const char* text = argv[i];
if (jv_get_kind(lib_search_paths) == JV_KIND_NULL) int is_short;
lib_search_paths = jv_array(); // First '-' already checked by isoptish
if (argv[i][2] != 0) { // -Lname (faster check than strlen) if (text[1] == '-') {
lib_search_paths = jv_array_append(lib_search_paths, jq_realpath(jv_string(argv[i]+2))); text += 2;
} else if (i >= argc - 1) { is_short = 0;
fprintf(stderr, "-L takes a parameter: (e.g. -L /search/path or -L/search/path)\n"); } else {
die(); text++;
} else { is_short = 1;
lib_search_paths = jv_array_append(lib_search_paths, jq_realpath(jv_string(argv[i+1])));
i++;
}
continue;
} }
int raw; // Temporary for --rawfile
if (isoption(argv[i], 's', "slurp", &short_opts)) { // Parse a long option in one iteration or iterate over short options
options |= SLURP; while (text != NULL) {
if (!short_opts) continue; if (isoption(&text, 's', "slurp", is_short)) {
} options |= SLURP;
if (isoption(argv[i], 'r', "raw-output", &short_opts)) { } else if (isoption(&text, 'r', "raw-output", is_short)) {
options |= RAW_OUTPUT; options |= RAW_OUTPUT;
if (!short_opts) continue; } else if (isoption(&text, 0, "raw-output0", is_short)) {
} options |= RAW_OUTPUT | RAW_NO_LF | RAW_OUTPUT0;
if (isoption(argv[i], 0, "raw-output0", &short_opts)) { } else if (isoption(&text, 'j', "join-output", is_short)) {
options |= RAW_OUTPUT | RAW_NO_LF | RAW_OUTPUT0; options |= RAW_OUTPUT | RAW_NO_LF;
if (!short_opts) continue; } else if (isoption(&text, 'c', "compact-output", is_short)) {
} dumpopts &= ~(JV_PRINT_TAB | JV_PRINT_INDENT_FLAGS(7));
if (isoption(argv[i], 'j', "join-output", &short_opts)) { } else if (isoption(&text, 'C', "color-output", is_short)) {
options |= RAW_OUTPUT | RAW_NO_LF; options |= COLOR_OUTPUT;
if (!short_opts) continue; } else if (isoption(&text, 'M', "monochrome-output", is_short)) {
} options |= NO_COLOR_OUTPUT;
if (isoption(argv[i], 'c', "compact-output", &short_opts)) { } else if (isoption(&text, 'a', "ascii-output", is_short)) {
dumpopts &= ~(JV_PRINT_TAB | JV_PRINT_INDENT_FLAGS(7)); options |= ASCII_OUTPUT;
if (!short_opts) continue; } else if (isoption(&text, 0, "unbuffered", is_short)) {
} options |= UNBUFFERED_OUTPUT;
if (isoption(argv[i], 'C', "color-output", &short_opts)) { } else if (isoption(&text, 'S', "sort-keys", is_short)) {
options |= COLOR_OUTPUT; options |= SORTED_OUTPUT;
if (!short_opts) continue; } else if (isoption(&text, 'R', "raw-input", is_short)) {
} options |= RAW_INPUT;
if (isoption(argv[i], 'M', "monochrome-output", &short_opts)) { } else if (isoption(&text, 'n', "null-input", is_short)) {
options |= NO_COLOR_OUTPUT; options |= PROVIDE_NULL;
if (!short_opts) continue; } else if (isoption(&text, 'f', "from-file", is_short)) {
} options |= FROM_FILE;
if (isoption(argv[i], 'a', "ascii-output", &short_opts)) { } else if (isoption(&text, 'L', NULL, is_short)) {
options |= ASCII_OUTPUT; if (jv_get_kind(lib_search_paths) == JV_KIND_NULL)
if (!short_opts) continue; lib_search_paths = jv_array();
} if (text != NULL) { // -Lname
if (isoption(argv[i], 0, "unbuffered", &short_opts)) { lib_search_paths = jv_array_append(lib_search_paths, jq_realpath(jv_string(text)));
options |= UNBUFFERED_OUTPUT; text = NULL;
continue; } else if (i >= argc - 1) {
} fprintf(stderr, "-L takes a parameter: (e.g. -L /search/path or -L/search/path)\n");
if (isoption(argv[i], 'S', "sort-keys", &short_opts)) { die();
options |= SORTED_OUTPUT; } else {
if (!short_opts) continue; lib_search_paths = jv_array_append(lib_search_paths, jq_realpath(jv_string(argv[i+1])));
} i++;
if (isoption(argv[i], 'R', "raw-input", &short_opts)) { }
options |= RAW_INPUT; } else if (isoption(&text, 'b', "binary", is_short)) {
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], 'b', "binary", &short_opts)) {
#ifdef WIN32 #ifdef WIN32
fflush(stdout); fflush(stdout);
fflush(stderr); fflush(stderr);
_setmode(fileno(stdin), _O_BINARY); _setmode(fileno(stdin), _O_BINARY);
_setmode(fileno(stdout), _O_BINARY); _setmode(fileno(stdout), _O_BINARY);
_setmode(fileno(stderr), _O_BINARY); _setmode(fileno(stderr), _O_BINARY);
#endif #endif
if (!short_opts) continue; } else if (isoption(&text, 0, "tab", is_short)) {
} dumpopts &= ~JV_PRINT_INDENT_FLAGS(7);
if (isoption(argv[i], 0, "tab", &short_opts)) { dumpopts |= JV_PRINT_TAB | JV_PRINT_PRETTY;
dumpopts &= ~JV_PRINT_INDENT_FLAGS(7); } else if (isoption(&text, 0, "indent", is_short)) {
dumpopts |= JV_PRINT_TAB | JV_PRINT_PRETTY; if (i >= argc - 1) {
continue; fprintf(stderr, "%s: --indent takes one parameter\n", progname);
}
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++;
continue;
}
if (isoption(argv[i], 0, "seq", &short_opts)) {
options |= SEQ;
continue;
}
if (isoption(argv[i], 0, "stream", &short_opts)) {
parser_flags |= JV_PARSE_STREAMING;
continue;
}
if (isoption(argv[i], 0, "stream-errors", &short_opts)) {
parser_flags |= JV_PARSE_STREAMING | JV_PARSE_STREAM_ERRORS;
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, "args", &short_opts)) {
further_args_are_strings = 1;
further_args_are_json = 0;
continue;
}
if (isoption(argv[i], 0, "jsonargs", &short_opts)) {
further_args_are_strings = 0;
further_args_are_json = 1;
continue;
}
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();
}
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1])))
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), jv_string(argv[i+2]));
i += 2; // skip the next two arguments
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();
}
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1]))) {
jv v = jv_parse(argv[i+2]);
if (!jv_is_valid(v)) {
fprintf(stderr, "%s: invalid JSON text passed to --argjson\n", progname);
die(); die();
} }
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), v); dumpopts &= ~(JV_PRINT_TAB | JV_PRINT_INDENT_FLAGS(7));
} int indent = atoi(argv[i+1]);
i += 2; // skip the next two arguments if (indent < -1 || indent > 7) {
continue; fprintf(stderr, "%s: --indent takes a number between -1 and 7\n", progname);
} die();
if (isoption(argv[i], 0, "rawfile", &short_opts) || }
isoption(argv[i], 0, "slurpfile", &short_opts)) { dumpopts |= JV_PRINT_INDENT_FLAGS(indent);
int raw = isoption(argv[i], 0, "rawfile", &short_opts); i++;
const char *which; } else if (isoption(&text, 0, "seq", is_short)) {
if (raw) options |= SEQ;
which = "rawfile"; } else if (isoption(&text, 0, "stream", is_short)) {
else parser_flags |= JV_PARSE_STREAMING;
which = "slurpfile"; } else if (isoption(&text, 0, "stream-errors", is_short)) {
if (i >= argc - 2) { parser_flags |= JV_PARSE_STREAMING | JV_PARSE_STREAM_ERRORS;
fprintf(stderr, "%s: --%s takes two parameters (e.g. --%s varname filename)\n", progname, which, which); } else if (isoption(&text, 'e', "exit-status", is_short)) {
options |= EXIT_STATUS;
} else if (isoption(&text, 0, "args", is_short)) {
further_args_are_strings = 1;
further_args_are_json = 0;
} else if (isoption(&text, 0, "jsonargs", is_short)) {
further_args_are_strings = 0;
further_args_are_json = 1;
} else if (isoption(&text, 0, "arg", is_short)) {
// FIXME: For --arg* we should check that the varname is acceptable
if (i >= argc - 2) {
fprintf(stderr, "%s: --arg takes two parameters (e.g. --arg varname value)\n", progname);
die();
}
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1])))
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), jv_string(argv[i+2]));
i += 2; // skip the next two arguments
} else if (isoption(&text, 0, "argjson", is_short)) {
if (i >= argc - 2) {
fprintf(stderr, "%s: --argjson takes two parameters (e.g. --argjson varname text)\n", progname);
die();
}
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1]))) {
jv v = jv_parse(argv[i+2]);
if (!jv_is_valid(v)) {
fprintf(stderr, "%s: invalid JSON text passed to --argjson\n", progname);
die();
}
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), v);
}
i += 2; // skip the next two arguments
} else if ((raw = isoption(&text, 0, "rawfile", is_short)) ||
isoption(&text, 0, "slurpfile", is_short)) {
const char *which = raw ? "rawfile" : "slurpfile";
if (i >= argc - 2) {
fprintf(stderr, "%s: --%s takes two parameters (e.g. --%s varname filename)\n", progname, which, which);
die();
}
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1]))) {
jv data = jv_load_file(argv[i+2], raw);
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);
ret = JQ_ERROR_SYSTEM;
goto out;
}
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), data);
}
i += 2; // skip the next two arguments
} else if (isoption(&text, 0, "debug-dump-disasm", is_short)) {
options |= DUMP_DISASM;
} else if (isoption(&text, 0, "debug-trace=all", is_short)) {
jq_flags |= JQ_DEBUG_TRACE_ALL;
} else if (isoption(&text, 0, "debug-trace", is_short)) {
jq_flags |= JQ_DEBUG_TRACE;
} else if (isoption(&text, 'h', "help", is_short)) {
usage(0, 0);
} else if (isoption(&text, 'V', "version", is_short)) {
printf("jq-%s\n", JQ_VERSION);
ret = JQ_OK;
goto out;
} else if (isoption(&text, 0, "build-configuration", is_short)) {
printf("%s\n", JQ_CONFIG);
ret = JQ_OK;
goto out;
} else if (isoption(&text, 0, "run-tests", is_short)) {
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;
} else {
if (is_short) {
fprintf(stderr, "%s: Unknown option -%c\n", progname, text[0]);
} else {
fprintf(stderr, "%s: Unknown option --%s\n", progname, text);
}
die(); die();
} }
if (!jv_object_has(jv_copy(program_arguments), jv_string(argv[i+1]))) {
jv data = jv_load_file(argv[i+2], raw);
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);
ret = JQ_ERROR_SYSTEM;
goto out;
}
program_arguments = jv_object_set(program_arguments, jv_string(argv[i+1]), data);
}
i += 2; // skip the next two arguments
continue;
}
if (isoption(argv[i], 0, "debug-dump-disasm", &short_opts)) {
options |= DUMP_DISASM;
continue;
}
if (isoption(argv[i], 0, "debug-trace=all", &short_opts)) {
jq_flags |= JQ_DEBUG_TRACE_ALL;
if (!short_opts) continue;
}
if (isoption(argv[i], 0, "debug-trace", &short_opts)) {
jq_flags |= JQ_DEBUG_TRACE;
continue;
}
if (isoption(argv[i], 'h', "help", &short_opts)) {
usage(0, 0);
if (!short_opts) continue;
}
if (isoption(argv[i], 'V', "version", &short_opts)) {
printf("jq-%s\n", JQ_VERSION);
ret = JQ_OK;
goto out;
}
if (isoption(argv[i], 0, "build-configuration", &short_opts)) {
printf("%s\n", JQ_CONFIG);
ret = JQ_OK;
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();
} }
} }
} }
+22
View File
@@ -210,6 +210,28 @@ else
cmp $d/out $d/expected cmp $d/out $d/expected
fi fi
# Regression tests for #3194
echo 42 > $d/expected
$JQ -nn 42 > $d/out 2>&1
cmp $d/out $d/expected
$JQ -nL. 42 > $d/out 2>&1
cmp $d/out $d/expected
$JQ -nL . 42 > $d/out 2>&1
cmp $d/out $d/expected
$JQ -h > $d/expected 2>&1
$JQ -hV > $d/out 2>&1
cmp $d/out $d/expected
$JQ -h -V > $d/out 2>&1
cmp $d/out $d/expected
$JQ -V > $d/expected 2>&1
$JQ -Vh > $d/out 2>&1
cmp $d/out $d/expected
$JQ -V -h > $d/out 2>&1
cmp $d/out $d/expected
## Test streaming parser ## Test streaming parser
## If we add an option to stream to the `import ... as $symbol;` directive ## If we add an option to stream to the `import ... as $symbol;` directive