{"thread":{"id":"63234","subject":"[PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","startedAt":"2025-04-01T15:01:36Z","lastAt":"2025-04-17T08:15:05Z","messageCount":37,"participants":["Patrick Steinhardt","René Scharfe","Phillip Wood","Junio C Hamano","phillip.wood123@gmail.com"],"isPatch":true,"patchVersion":1,"patchTotal":5},"messages":[{"id":"515434","messageId":"20250401-b4-pks-parse-options-integers-v1-2-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:17Z","receivedAt":"2025-04-01T15:01:36Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `OPTION_INTEGER` option type accepts a signed integer. The type of\nthe underlying integer is a simple `int`, which restricts the range of\nvalues accepted by such options. But there is a catch: because the\ncaller provides a pointer to the value via the `.value` field, which is\na simple void pointer. This has two consequences:\n\n  - There is no check whether the passed value is sufficiently long to\n    store the entire range of `int`. This can lead to integer wraparound\n    in the best case and out-of-bounds writes in the worst case.\n\n  - Even when a caller knows that they want to store a value larger than\n    `INT_MAX` they don't have a way to do so.\n\nFunny enough, even if the caller gets everything correct the parsing\nlogic is still insufficient because we use `strtol()` to parse the\nargument, which returns a `long`. But as that value is implicitly cast\nwhen assigning it to the `int` field we may still get invalid results.\n\nIn practice this doesn't tend to be a huge issue because users typically\ndon't end up passing huge values to most commands. But the parsing logic\nis demonstrably broken, and it is too easy to get the calling convention\nwrong.\n\nImprove the situation by introducing a new `precision` field into the\nstructure. This field gets assigned automatically by `OPT_INTEGER_F()`\nand tracks the size of the passed value. Like this it becomes possible\nfor the caller to pass arbitrarily-sized integers and the underlying\nlogic knows to handle it correctly by doing range checks. Furthermore,\nconvert the code to use `strtoimax()` intstead of `strtol()` so that we\ncan also parse values larger than `LONG_MAX`.\n\nNote that we do not yet assert signedness of the passed variable, which\nis another source of bugs. This will be handled in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fmt-merge-msg.c       |  2 ++\n builtin/merge.c               |  1 +\n builtin/show-branch.c         |  1 +\n builtin/tag.c                 |  1 +\n parse-options.c               | 53 +++++++++++++++++++++++++++++++------------\n parse-options.h               |  6 +++++\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 23 ++++++++++++++++++-\n 8 files changed, 75 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 240cdb474bc..3b6aac2cf7f 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"log\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\n@@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"summary\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"alias for --log (deprecated)\"),\n \t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 21787d45165..9ab10c7db0a 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = {\n \t\t.type = OPTION_INTEGER,\n \t\t.long_name = \"log\",\n \t\t.value = &shortlog_len,\n+\t\t.precision = sizeof(shortlog_len),\n \t\t.argh = N_(\"n\"),\n \t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n \t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex dab37019d29..b549d8c3f5b 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -671,6 +671,7 @@ int cmd_show_branch(int ac,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"more\",\n \t\t\t.value = &extra,\n+\t\t\t.precision = sizeof(extra),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex b266f12bb48..7597d93c71b 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -483,6 +483,7 @@ int cmd_tag(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.short_name = 'n',\n \t\t\t.value = &filter.lines,\n+\t\t\t.precision = sizeof(filter.lines),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"print <n> lines of each tag message\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/parse-options.c b/parse-options.c\nindex 35fbb3b0d63..dbda9b7cfe7 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\treturn (*opt->ll_callback)(p, opt, p_arg, p_unset);\n \t}\n \tcase OPTION_INTEGER:\n+\t{\n+\t\tintmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1);\n+\t\tintmax_t lower_bound = -upper_bound - 1;\n+\t\tintmax_t value;\n+\n \t\tif (unset) {\n-\t\t\t*(int *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(int *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!*arg)\n+\t\t} else if (!*arg) {\n \t\t\treturn error(_(\"%s expects a numerical value\"),\n \t\t\t\t     optname(opt, flags));\n-\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n-\t\tif (*s)\n-\t\t\treturn error(_(\"%s expects a numerical value\"),\n-\t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\t} else {\n+\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\n+\t\t}\n \n+\t\tif (value < lower_bound || value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n+\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(int8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(int16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(int32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(int64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t\tif (unset) {\n \t\t\t*(unsigned long *)opt->value = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 997ffbee805..8d5f9c95f9c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n  * `value`::\n  *   stores pointers to the values to be filled.\n  *\n+ * `precision`::\n+ *   precision of the integer pointed to by `value`. Should typically be its\n+ *   `sizeof()`.\n+ *\n  * `argh`::\n  *   token to explain the kind of argument this option wants. Does not\n  *   begin in capital letter, and does not end with a full stop.\n@@ -151,6 +155,7 @@ struct option {\n \tint short_name;\n \tconst char *long_name;\n \tvoid *value;\n+\tsize_t precision;\n \tconst char *argh;\n \tconst char *help;\n \n@@ -214,6 +219,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = (f), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 997f55fd45b..b1275dfade4 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tint16_t i16 = 0;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n@@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_NEGBIT(0, \"neg-or4\", &boolean, \"same as --no-or4\", 4),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n+\t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n@@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t}\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n+\tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 2fe3522305f..e3ca7a27738 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -22,6 +22,7 @@ usage: test-tool parse-options <options>\n \n     -i, --[no-]integer <n>\n                           get a integer\n+    --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --[no-]set23          set integer to 23\n@@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 0\n magnitude: 16384\n timestamp: 0\n string: 123\n@@ -156,6 +158,7 @@ test_expect_success 'short options' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 9000\n magnitude: 16384\n timestamp: 0\n string: 321\n@@ -167,7 +170,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n \t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tcat >expect <<-EOF &&\n \tboolean: 0\n \tinteger: 0\n+\ti16: 0\n \tmagnitude: 0\n \ttimestamp: 0\n \tstring: (not set)\n@@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 13\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: 123\n@@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 2\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -343,6 +349,7 @@ cat >expect <<\\EOF\n Callback: \"four\", 0\n boolean: 5\n integer: 4\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 23\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 0\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' '\n \ttest_must_be_empty out\n '\n \n+test_expect_success 'i16 limits range' '\n+\ttest-tool parse-options --i16 32767 >out &&\n+\ttest_grep \"i16: 32767\" out &&\n+\ttest_must_fail test-tool parse-options --i16 32768 2>err &&\n+\ttest_grep \"value 32768 for option .i16. not in range \\[-32768,32767\\]\" err &&\n+\n+\ttest-tool parse-options --i16 -32768 >out &&\n+\ttest_grep \"i16: -32768\" out &&\n+\ttest_must_fail test-tool parse-options --i16 -32769 2>err &&\n+\ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n+'\n+\n test_done\n\n-- \n2.49.0.604.gff1f9ca942.dirty\n\n"},{"id":"515435","messageId":"20250401-b4-pks-parse-options-integers-v1-3-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:18Z","receivedAt":"2025-04-01T15:01:38Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit is the equivalent to the preceding commit, but instead of\nintroducing precision handling for `OPTION_INTEGER` we introduce it for\n`OPTION_MAGNITUDE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 50 ++++++++++++++++++++++++++++++++++---------\n parse-options.h               |  1 +\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 18 +++++++++++++++-\n 4 files changed, 61 insertions(+), 11 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex dbda9b7cfe7..3954ee0e570 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -217,21 +217,51 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t}\n \t}\n \tcase OPTION_MAGNITUDE:\n+\t{\n+\t\tuintmax_t upper_bound = 0;\n+\t\tunsigned long value;\n+\n+\t\t/*\n+\t\t * It's stupid, but the obvious way of calculating the upper\n+\t\t * bound via `2 ^ n - 1` overflows.\n+\t\t */\n+\t\tfor (size_t i = 0; i < opt->precision * 8; i++)\n+\t\t\tupper_bound |= ((uintmax_t) 1 << i);\n+\n \t\tif (unset) {\n-\t\t\t*(unsigned long *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(unsigned long *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!git_parse_ulong(arg, opt->value))\n+\t\t} else if (!git_parse_ulong(arg, &value)) {\n \t\t\treturn error(_(\"%s expects a non-negative integer value\"\n \t\t\t\t       \" with an optional k/m/g suffix\"),\n \t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n+\t\t\t\t     (uintmax_t) value, optname(opt, flags), upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(uint8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(uint16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(uint32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(uint64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \n \tdefault:\n \t\tBUG(\"opt->type %d should not happen\", opt->type);\ndiff --git a/parse-options.h b/parse-options.h\nindex 8d5f9c95f9c..4b561679581 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -281,6 +281,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = PARSE_OPT_NONEG, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex b1275dfade4..46deb4317ef 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tuint16_t m16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n+\t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n \t\tOPT_CMDMODE(0, \"mode1\", &integer, \"set integer to 1 (cmdmode option)\", 1),\n \t\tOPT_CMDMODE(0, \"mode2\", &integer, \"set integer to 2 (cmdmode option)\", 2),\n@@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n+\tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\n \tshow(&expect, &ret, \"abbrev: %d\", abbrev);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex e3ca7a27738..5f503b26cc8 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -25,6 +25,7 @@ usage: test-tool parse-options <options>\n     --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n+    --m16 <n>             get a 16 bit magnitude\n     --[no-]set23          set integer to 23\n     --mode1               set integer to 1 (cmdmode option)\n     --mode2               set integer to 2 (cmdmode option)\n@@ -139,6 +140,7 @@ boolean: 2\n integer: 1729\n i16: 0\n magnitude: 16384\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -160,6 +162,7 @@ boolean: 2\n integer: 1729\n i16: 9000\n magnitude: 16384\n+m16: 32768\n timestamp: 0\n string: 321\n abbrev: 10\n@@ -171,7 +174,7 @@ EOF\n \n test_expect_success 'long options' '\n \ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n-\t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n+\t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n \ttest_must_be_empty output.err &&\n@@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tinteger: 0\n \ti16: 0\n \tmagnitude: 0\n+\tm16: 0\n \ttimestamp: 0\n \tstring: (not set)\n \tabbrev: 100\n@@ -259,6 +263,7 @@ boolean: 1\n integer: 13\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -283,6 +288,7 @@ boolean: 0\n integer: 2\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -351,6 +357,7 @@ boolean: 5\n integer: 4\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -377,6 +384,7 @@ boolean: 1\n integer: 23\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -457,6 +465,7 @@ boolean: 0\n integer: 0\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -804,4 +813,11 @@ test_expect_success 'i16 limits range' '\n \ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n '\n \n+test_expect_success 'm16 limits range' '\n+\ttest-tool parse-options --m16 65535 >out &&\n+\ttest_grep \"m16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --m16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n+'\n+\n test_done\n\n-- \n2.49.0.604.gff1f9ca942.dirty\n\n"},{"id":"515436","messageId":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":null,"subject":"[PATCH 0/5] parse-options: harden handling of integer values","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:15Z","receivedAt":"2025-04-01T15:01:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series addresses the issues raised in [1] and [2]. As\ndiscussed in [1], the series also introduces a couple of safeguards to\nmake it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`:\n\n  - We now track the precision of the underlying integer types. This\n    makes it possible to pass arbitrarily-sized integers to those\n    options, not only `int` and `unsigned long`, respectively.\n\n  - We introduce a build assert to verify that the passed variable has\n    correct signedness.\n\nFurthermore, the series introduces `OPT_UNSIGNED()` to adapt all\ncallsites that previously used variables with the wrong signedness.\n\nThanks!\n\nPatrick\n\n[1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de>\n[2]: <Z8HW6petWuMRWSXf@teonanacatl.net>\n\n---\nPatrick Steinhardt (5):\n      global: use designated initializers for options\n      parse-options: introduce precision handling for `OPTION_INTEGER`\n      parse-options: introduce precision handling for `OPTION_MAGNITUDE`\n      parse-options: introduce `OPTION_UNSIGNED`\n      parse-options: detect mismatches in integer signedness\n\n apply.c                       |   4 +-\n archive.c                     |  35 +++++++---\n builtin/am.c                  |  28 +++++---\n builtin/backfill.c            |   4 +-\n builtin/clone.c               |  13 +++-\n builtin/column.c              |   2 +-\n builtin/commit-tree.c         |  12 +++-\n builtin/commit.c              |  62 ++++++++++++++----\n builtin/config.c              |  13 +++-\n builtin/describe.c            |  24 +++++--\n builtin/fetch.c               |  10 ++-\n builtin/fmt-merge-msg.c       |  27 ++++++--\n builtin/gc.c                  |  12 +++-\n builtin/grep.c                |  18 ++++--\n builtin/init-db.c             |  13 ++--\n builtin/ls-remote.c           |  11 +++-\n builtin/merge.c               |  38 ++++++++---\n builtin/read-tree.c           |  11 +++-\n builtin/rebase.c              |  25 ++++++--\n builtin/revert.c              |  12 +++-\n builtin/show-branch.c         |  13 +++-\n builtin/tag.c                 |  24 +++++--\n builtin/update-index.c        | 131 ++++++++++++++++++++++++++------------\n builtin/write-tree.c          |  12 ++--\n diff.c                        |  13 ++--\n git-compat-util.h             |   7 ++\n parse-options.c               | 145 ++++++++++++++++++++++++++++++++++++------\n parse-options.h               |  23 ++++++-\n ref-filter.h                  |  15 +++--\n t/helper/test-parse-options.c |  46 +++++++++++---\n t/t0040-parse-options.sh      |  57 ++++++++++++++++-\n 31 files changed, 671 insertions(+), 189 deletions(-)\n\n\n---\nbase-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff\nchange-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011\n\n"},{"id":"515437","messageId":"20250401-b4-pks-parse-options-integers-v1-4-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:19Z","receivedAt":"2025-04-01T15:01:39Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two generic ways to parse integers in the \"parse-options\"\nsubsytem:\n\n  - `OPTION_INTEGER` parses a signed integer.\n\n  - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n    interprets suffixes like \"k\" or \"g\".\n\nNotably missing is a middle ground that parses unsigned integers without\ninterpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\nplug this gap. This option type will be used in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 50 +++++++++++++++++++++++++++++++++++++++++++\n parse-options.h               | 12 +++++++++++\n t/helper/test-parse-options.c |  4 +++-\n t/t0040-parse-options.sh      | 18 +++++++++++++++-\n 4 files changed, 82 insertions(+), 2 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 3954ee0e570..fcc415b43fb 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -216,6 +216,56 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\t    optname(opt, flags));\n \t\t}\n \t}\n+\tcase OPTION_UNSIGNED:\n+\t{\n+\t\tuintmax_t upper_bound = 0;\n+\t\tuintmax_t value;\n+\n+\t\t/*\n+\t\t * It's stupid, but the obvious way of calculating the upper\n+\t\t * bound via `2 ^ n - 1` overflows.\n+\t\t */\n+\t\tfor (size_t i = 0; i < opt->precision * 8; i++)\n+\t\t\tupper_bound |= ((uintmax_t) 1 << i);\n+\n+\t\tif (unset) {\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n+\t\t\treturn -1;\n+\t\t} else if (!*arg) {\n+\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t     optname(opt, flags));\n+\t\t} else {\n+\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n+\t\t\t\t     value, optname(opt, flags), upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(int8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(int16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(int32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(int64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t{\n \t\tuintmax_t upper_bound = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 4b561679581..20ea7d2ab13 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -25,6 +25,7 @@ enum parse_opt_type {\n \t/* options with arguments (usually) */\n \tOPTION_STRING,\n \tOPTION_INTEGER,\n+\tOPTION_UNSIGNED,\n \tOPTION_MAGNITUDE,\n \tOPTION_CALLBACK,\n \tOPTION_LOWLEVEL_CALLBACK,\n@@ -224,6 +225,16 @@ struct option {\n \t.help = (h), \\\n \t.flags = (f), \\\n }\n+#define OPT_UNSIGNED_F(s, l, v, h, f) { \\\n+\t.type = OPTION_UNSIGNED, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = (f), \\\n+}\n \n #define OPT_END() { \\\n \t.type = OPTION_END, \\\n@@ -276,6 +287,7 @@ struct option {\n #define OPT_CMDMODE(s, l, v, h, i)  OPT_CMDMODE_F(s, l, v, h, i, 0)\n \n #define OPT_INTEGER(s, l, v, h)     OPT_INTEGER_F(s, l, v, h, 0)\n+#define OPT_UNSIGNED(s, l, v, h)    OPT_UNSIGNED_F(s, l, v, h, 0)\n #define OPT_MAGNITUDE(s, l, v, h) { \\\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 46deb4317ef..0d559288d9c 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n-\tuint16_t m16 = 0;\n+\tuint16_t m16 = 0, u16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n+\t\tOPT_UNSIGNED(0, \"u16\", &u16, \"get a 16 bit unsigned integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n@@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n+\tshow(&expect, &ret, \"u16: %\"PRIuMAX, (uintmax_t) u16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 5f503b26cc8..9946e69f586 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -23,6 +23,7 @@ usage: test-tool parse-options <options>\n     -i, --[no-]integer <n>\n                           get a integer\n     --[no-]i16 <n>        get a 16 bit integer\n+    --[no-]u16 <n>        get a 16 bit unsigned integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --m16 <n>             get a 16 bit magnitude\n@@ -139,6 +140,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 0\n+u16: 0\n magnitude: 16384\n m16: 0\n timestamp: 0\n@@ -161,6 +163,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 9000\n+u16: 5432\n magnitude: 16384\n m16: 32768\n timestamp: 0\n@@ -173,7 +176,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \\\n \t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tboolean: 0\n \tinteger: 0\n \ti16: 0\n+\tu16: 0\n \tmagnitude: 0\n \tm16: 0\n \ttimestamp: 0\n@@ -262,6 +266,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 13\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -287,6 +292,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 2\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -356,6 +362,7 @@ Callback: \"four\", 0\n boolean: 5\n integer: 4\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -383,6 +390,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 23\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -464,6 +472,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 0\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' '\n \ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n '\n \n+test_expect_success 'u16 limits range' '\n+\ttest-tool parse-options --u16 65535 >out &&\n+\ttest_grep \"u16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --u16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .u16. exceeds 65535\" err\n+'\n+\n test_done\n\n-- \n2.49.0.604.gff1f9ca942.dirty\n\n"},{"id":"515438","messageId":"20250401-b4-pks-parse-options-integers-v1-1-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH 1/5] global: use designated initializers for options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:16Z","receivedAt":"2025-04-01T15:01:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we expose macros for most of our different option types understood\nby the \"parse-options\" subsystem, not every combination of fields that\nhas one as that would otherwise quickly lead to an explosion of macros.\nInstead, we just initialize structures manually for those variants of\nfields that don't have a macro.\n\nCallsites that open-code these structure initialization don't use\ndesignated initializers though and instead just provide values for each\nof the fields that they want to initialize. This has three significant\ndownsides:\n\n  - Callsites need to specify all values up to the last field that they\n    care about. This often includes fields that should simply be left at\n    their default zero-initialized state, which adds distraction.\n\n  - Any reader not deeply familiar with the layout of the structure\n    has a hard time figuring out what the respective initializers mean.\n\n  - Reordering or introducing new fields in the middle of the structure\n    is impossible without adapting all callsites.\n\nConvert all sites to instead use designated initializers, which we have\nstarted using in our codebase quite a while ago. This allows us to skip\nany default-initialized fields, gives the reader context by specifying\nthe field names and allows us to reorder or introduce new fields where\nwe want to.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n archive.c                     |  35 ++++++++---\n builtin/am.c                  |  28 ++++++---\n builtin/clone.c               |  13 ++++-\n builtin/commit-tree.c         |  12 +++-\n builtin/commit.c              |  62 +++++++++++++++-----\n builtin/config.c              |  13 ++++-\n builtin/describe.c            |  24 ++++++--\n builtin/fetch.c               |  10 +++-\n builtin/fmt-merge-msg.c       |  25 +++++---\n builtin/gc.c                  |  12 +++-\n builtin/grep.c                |  14 +++--\n builtin/init-db.c             |  13 +++--\n builtin/ls-remote.c           |  11 +++-\n builtin/merge.c               |  37 +++++++++---\n builtin/read-tree.c           |  11 +++-\n builtin/rebase.c              |  25 ++++++--\n builtin/revert.c              |  12 +++-\n builtin/show-branch.c         |  12 +++-\n builtin/tag.c                 |  23 ++++++--\n builtin/update-index.c        | 131 +++++++++++++++++++++++++++++-------------\n builtin/write-tree.c          |  12 ++--\n diff.c                        |  13 +++--\n ref-filter.h                  |  15 +++--\n t/helper/test-parse-options.c |  38 +++++++++---\n 24 files changed, 443 insertions(+), 158 deletions(-)\n\ndiff --git a/archive.c b/archive.c\nindex 8be4e7ac8db..67bba3cd301 100644\n--- a/archive.c\n+++ b/archive.c\n@@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv,\n \t\tOPT_STRING(0, \"format\", &format, N_(\"fmt\"), N_(\"archive format\")),\n \t\tOPT_STRING(0, \"prefix\", &base, N_(\"prefix\"),\n \t\t\tN_(\"prepend prefix to each pathname in the archive\")),\n-\t\t{ OPTION_CALLBACK, 0, \"add-file\", args, N_(\"file\"),\n-\t\t  N_(\"add untracked file to archive\"), 0, add_file_cb,\n-\t\t  (intptr_t)&base },\n-\t\t{ OPTION_CALLBACK, 0, \"add-virtual-file\", args,\n-\t\t  N_(\"path:content\"), N_(\"add untracked file to archive\"), 0,\n-\t\t  add_file_cb, (intptr_t)&base },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"file\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-virtual-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"path:content\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n \t\tOPT_STRING('o', \"output\", &output, N_(\"file\"),\n \t\t\tN_(\"write the archive to this file\")),\n \t\tOPT_BOOL(0, \"worktree-attributes\", &worktree_attributes,\n \t\t\tN_(\"read .gitattributes in working directory\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report archived files on stderr\")),\n-\t\t{ OPTION_STRING, 0, \"mtime\", &mtime_option, N_(\"time\"),\n-\t\t  N_(\"set modification time of archive entries\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"mtime\",\n+\t\t\t.value = &mtime_option,\n+\t\t\t.argh = N_(\"time\"),\n+\t\t\t.help = N_(\"set modification time of archive entries\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_NUMBER_CALLBACK(&compression_level,\n \t\t\tN_(\"set compression level\"), number_callback),\n \t\tOPT_GROUP(\"\"),\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 3b61bd4c333..4afb519830f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2400,11 +2400,16 @@ int cmd_am(int argc,\n \t\tOPT_CMDMODE(0, \"quit\", &resume_mode,\n \t\t\tN_(\"abort the patching operation but keep HEAD where it is\"),\n \t\t\tRESUME_QUIT),\n-\t\t{ OPTION_CALLBACK, 0, \"show-current-patch\", &resume_mode,\n-\t\t  \"(diff|raw)\",\n-\t\t  N_(\"show the patch being applied\"),\n-\t\t  PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t  parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"show-current-patch\",\n+\t\t\t.value = &resume_mode,\n+\t\t\t.argh = \"(diff|raw)\",\n+\t\t\t.help = N_(\"show the patch being applied\"),\n+\t\t\t.flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.callback = parse_opt_show_current_patch,\n+\t\t\t.defval = RESUME_SHOW_PATCH_RAW,\n+\t\t},\n \t\tOPT_CMDMODE(0, \"retry\", &resume_mode,\n \t\t\tN_(\"try to apply current patch again\"),\n \t\t\tRESUME_APPLY),\n@@ -2417,9 +2422,16 @@ int cmd_am(int argc,\n \t\tOPT_BOOL(0, \"ignore-date\", &state.ignore_date,\n \t\t\tN_(\"use current timestamp for author date\")),\n \t\tOPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &state.sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG-sign commits\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &state.sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"empty\", &state.empty_type, \"(stop|drop|keep)\",\n \t\t  N_(\"how to handle empty patches\"),\n \t\t  PARSE_OPT_NONEG, am_option_parse_empty),\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 88276e5b7ab..9c3547f41e3 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -930,9 +930,16 @@ int cmd_clone(int argc,\n \t\t\t N_(\"don't use local hardlinks, always copy\")),\n \t\tOPT_BOOL('s', \"shared\", &option_shared,\n \t\t\t N_(\"setup as shared repository\")),\n-\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"recurse-submodules\",\n+\t\t\t.value = &option_recurse_submodules,\n+\t\t\t.argh = N_(\"pathspec\"),\n+\t\t\t.help = N_(\"initialize submodules in the clone\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.callback = recurse_submodules_cb,\n+\t\t\t.defval = (intptr_t)\".\",\n+\t\t},\n \t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n \t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n \t\t\t    N_(\"number of submodules cloned in parallel\")),\ndiff --git a/builtin/commit-tree.c b/builtin/commit-tree.c\nindex 38457600a4e..c787133d004 100644\n--- a/builtin/commit-tree.c\n+++ b/builtin/commit-tree.c\n@@ -111,8 +111,16 @@ int cmd_commit_tree(int argc,\n \t\tOPT_CALLBACK_F('F', NULL, &buffer, N_(\"file\"),\n \t\t\tN_(\"read commit log message from file\"), PARSE_OPT_NONEG,\n \t\t\tparse_file_arg_callback),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t\tN_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tint ret;\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 2f459682221..66bd91fd523 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1542,17 +1542,34 @@ struct repository *repo UNUSED)\n \t\t\t    STATUS_FORMAT_LONG),\n \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n \t\t\t N_(\"terminate entries with NUL\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n-\t\t{ OPTION_STRING, 0, \"ignored\", &ignored_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"traditional\" },\n-\t\t{ OPTION_STRING, 0, \"ignore-submodules\", &ignore_submodule_arg, N_(\"when\"),\n-\t\t  N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignored\",\n+\t\t\t.value = &ignored_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"traditional\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignore-submodules\",\n+\t\t\t.value = &ignore_submodule_arg,\n+\t\t\t.argh = N_(\"when\"),\n+\t\t\t.help = N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_COLUMN(0, \"column\", &s.colopts, N_(\"list untracked files in columns\")),\n \t\tOPT_BOOL(0, \"no-renames\", &no_renames, N_(\"do not detect renames\")),\n \t\tOPT_CALLBACK_F('M', \"find-renames\", &rename_score_arg,\n@@ -1688,8 +1705,16 @@ int cmd_commit(int argc,\n \t\tOPT_BOOL('e', \"edit\", &edit_flag, N_(\"force edit of commit\")),\n \t\tOPT_CLEANUP(&cleanup_arg),\n \t\tOPT_BOOL(0, \"status\", &include_status, N_(\"include status in commit message template\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\t/* end commit message options */\n \n \t\tOPT_GROUP(N_(\"Commit contents options\")),\n@@ -1714,7 +1739,16 @@ int cmd_commit(int argc,\n \t\t\t N_(\"terminate entries with NUL\")),\n \t\tOPT_BOOL(0, \"amend\", &amend, N_(\"amend previous commit\")),\n \t\tOPT_BOOL(0, \"no-post-rewrite\", &no_post_rewrite, N_(\"bypass post-rewrite hook\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg, N_(\"mode\"), N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"), PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_PATHSPEC_FROM_FILE(&pathspec_from_file),\n \t\tOPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),\n \t\t/* end commit contents options */\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 53a90094e31..f70d6354772 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -131,9 +131,16 @@ struct config_display_options {\n #define TYPE_COLOR\t\t6\n #define TYPE_BOOL_OR_STR\t7\n \n-#define OPT_CALLBACK_VALUE(s, l, v, h, i) \\\n-\t{ OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \\\n-\tPARSE_OPT_NONEG, option_parse_type, (i) }\n+#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \\\n+\t.callback = option_parse_type, \\\n+\t.defval = (i), \\\n+}\n \n static int option_parse_type(const struct option *opt, const char *arg,\n \t\t\t     int unset)\ndiff --git a/builtin/describe.c b/builtin/describe.c\nindex e2e73f3d757..2da9f4fed01 100644\n--- a/builtin/describe.c\n+++ b/builtin/describe.c\n@@ -601,12 +601,24 @@ int cmd_describe(int argc,\n \t\t\t   N_(\"do not consider tags matching <pattern>\")),\n \t\tOPT_BOOL(0, \"always\",        &always,\n \t\t\tN_(\"show abbreviated commit object as fallback\")),\n-\t\t{OPTION_STRING, 0, \"dirty\",  &dirty, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-dirty\"},\n-\t\t{OPTION_STRING, 0, \"broken\",  &broken, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-broken\"},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"dirty\",\n+\t\t\t.value = &dirty,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-dirty\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"broken\",\n+\t\t\t.value = &broken,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-broken\",\n+\t\t},\n \t\tOPT_END(),\n \t};\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 02af5054690..3a5159d9e69 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2359,8 +2359,14 @@ int cmd_fetch(int argc,\n \t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n \t\t\t      N_(\"re-fetch without negotiating common commits\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n-\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"submodule-prefix\",\n+\t\t\t.value = &submodule_prefix,\n+\t\t\t.argh = N_(\"dir\"),\n+\t\t\t.help = N_(\"prepend this to submodule path output\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n \t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n \t\t\t   N_(\"default for recursive fetching of submodules \"\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 189cd1096a0..240cdb474bc 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc,\n \tchar *into_name = NULL;\n \tint shortlog_len = -1;\n \tstruct option options[] = {\n-\t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"populate log with at most <n> entries from shortlog\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n-\t\t{ OPTION_INTEGER, 0, \"summary\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"alias for --log (deprecated)\"),\n-\t\t  PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL,\n-\t\t  DEFAULT_MERGE_LOG_LEN },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"log\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"summary\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"alias for --log (deprecated)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n \t\t\tN_(\"use <text> as start of message\")),\n \t\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 99431fd4674..6707a26bc6e 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -699,9 +699,15 @@ struct repository *repo UNUSED)\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n-\t\t{ OPTION_STRING, 0, \"prune\", &prune_expire_arg, N_(\"date\"),\n-\t\t\tN_(\"prune unreferenced objects\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prune\",\n+\t\t\t.value = &prune_expire_arg,\n+\t\t\t.argh = N_(\"date\"),\n+\t\t\t.help = N_(\"prune unreferenced objects\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)prune_expire_arg,\n+\t\t},\n \t\tOPT_BOOL(0, \"cruft\", &cfg.cruft_packs, N_(\"pack unreferenced objects separately\")),\n \t\tOPT_MAGNITUDE(0, \"max-cruft-size\", &cfg.max_cruft_size,\n \t\t\t      N_(\"with --cruft, limit the size of new cruft packs\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex d1427290f77..c4869733e1b 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -1017,10 +1017,16 @@ int cmd_grep(int argc,\n \t\tOPT_BOOL(0, \"all-match\", &opt.all_match,\n \t\t\tN_(\"show only matches from files that match all patterns\")),\n \t\tOPT_GROUP(\"\"),\n-\t\t{ OPTION_STRING, 'O', \"open-files-in-pager\", &show_in_pager,\n-\t\t\tN_(\"pager\"), N_(\"show matching files in the pager\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n-\t\t\tNULL, (intptr_t)default_pager },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'O',\n+\t\t\t.long_name = \"open-files-in-pager\",\n+\t\t\t.value = &show_in_pager,\n+\t\t\t.argh = N_(\"pager\"),\n+\t\t\t.help = N_(\"show matching files in the pager\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n+\t\t\t.defval = (intptr_t)default_pager,\n+\t\t},\n \t\tOPT_BOOL_F(0, \"ext-grep\", &external_grep_allowed__ignored,\n \t\t\t   N_(\"allow calling of grep(1) (ignored by this build)\"),\n \t\t\t   PARSE_OPT_NOCOMPLETE),\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 196dccdd77a..4a950e44d8d 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -93,10 +93,15 @@ int cmd_init_db(int argc,\n \t\t\t\tN_(\"directory from which templates will be used\")),\n \t\tOPT_SET_INT(0, \"bare\", &is_bare_repository_cfg,\n \t\t\t\tN_(\"create a bare repository\"), 1),\n-\t\t{ OPTION_CALLBACK, 0, \"shared\", &init_shared_repository,\n-\t\t\tN_(\"permissions\"),\n-\t\t\tN_(\"specify that the git repository is to be shared amongst several users\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"shared\",\n+\t\t\t.value = &init_shared_repository,\n+\t\t\t.argh = N_(\"permissions\"),\n+\t\t\t.help = N_(\"specify that the git repository is to be shared amongst several users\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n+\t\t\t.callback = shared_callback\n+\t\t},\n \t\tOPT_BIT('q', \"quiet\", &flags, N_(\"be quiet\"), INIT_DB_QUIET),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n \t\t\t   N_(\"separate git dir from working tree\")),\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 42f34e12361..01a4d4daa1f 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -67,9 +67,14 @@ int cmd_ls_remote(int argc,\n \t\tOPT__QUIET(&quiet, N_(\"do not print remote URL\")),\n \t\tOPT_STRING(0, \"upload-pack\", &uploadpack, N_(\"exec\"),\n \t\t\t   N_(\"path of git-upload-pack on the remote host\")),\n-\t\t{ OPTION_STRING, 0, \"exec\", &uploadpack, N_(\"exec\"),\n-\t\t\t   N_(\"path of git-upload-pack on the remote host\"),\n-\t\t\t   PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"exec\",\n+\t\t\t.value = &uploadpack,\n+\t\t\t.argh = N_(\"exec\"),\n+\t\t\t.help = N_(\"path of git-upload-pack on the remote host\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_BIT('t', \"tags\", &flags, N_(\"limit to tags\"), REF_TAGS),\n \t\tOPT_BIT('b', \"branches\", &flags, N_(\"limit to branches\"), REF_BRANCHES),\n \t\tOPT_BIT_F('h', \"heads\", &flags,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex ba9faf126aa..21787d45165 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"stat\", &show_diffstat,\n \t\tN_(\"show a diffstat at the end of the merge\")),\n \tOPT_BOOL(0, \"summary\", &show_diffstat, N_(\"(synonym to --stat)\")),\n-\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t  N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n-\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n+\t{\n+\t\t.type = OPTION_INTEGER,\n+\t\t.long_name = \"log\",\n+\t\t.value = &shortlog_len,\n+\t\t.argh = N_(\"n\"),\n+\t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t},\n \tOPT_BOOL(0, \"squash\", &squash,\n \t\tN_(\"create a single commit instead of doing a merge\")),\n \tOPT_BOOL(0, \"commit\", &option_commit,\n@@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_CALLBACK('m', \"message\", &merge_msg, N_(\"message\"),\n \t\tN_(\"merge commit message (for a non-fast-forward merge)\"),\n \t\toption_parse_message),\n-\t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n-\t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n-\t\tNULL, 0, option_read_message },\n+\t{\n+\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t.short_name = 'F',\n+\t\t.long_name = \"file\",\n+\t\t.value = &merge_msg,\n+\t\t.argh = N_(\"path\"),\n+\t\t.help = N_(\"read message from file\"),\n+\t\t.flags = PARSE_OPT_NONEG,\n+\t\t.ll_callback = option_read_message,\n+\t},\n \tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n \t\t   N_(\"use <name> instead of the real target\")),\n \tOPT__VERBOSITY(&verbosity),\n@@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"allow-unrelated-histories\", &allow_unrelated_histories,\n \t\t N_(\"allow merging unrelated histories\")),\n \tOPT_SET_INT(0, \"progress\", &show_progress, N_(\"force progress reporting\"), 1),\n-\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t{\n+\t\t.type = OPTION_STRING,\n+\t\t.short_name = 'S',\n+\t\t.long_name = \"gpg-sign\",\n+\t\t.value = &sign_commit,\n+\t\t.argh = N_(\"key-id\"),\n+\t\t.help = N_(\"GPG sign commit\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = (intptr_t) \"\",\n+\t},\n \tOPT_AUTOSTASH(&autostash),\n \tOPT_BOOL(0, \"overwrite-ignore\", &overwrite_ignore, N_(\"update ignored files (default)\")),\n \tOPT_BOOL(0, \"signoff\", &signoff, N_(\"add a Signed-off-by trailer\")),\ndiff --git a/builtin/read-tree.c b/builtin/read-tree.c\nindex d2a807a828b..a8f352f7cd9 100644\n--- a/builtin/read-tree.c\n+++ b/builtin/read-tree.c\n@@ -135,9 +135,14 @@ int cmd_read_tree(int argc,\n \t\t\t N_(\"3-way merge in presence of adds and removes\")),\n \t\tOPT_BOOL(0, \"reset\", &opts.reset,\n \t\t\t N_(\"same as -m, but discard unmerged entries\")),\n-\t\t{ OPTION_STRING, 0, \"prefix\", &opts.prefix, N_(\"<subdirectory>/\"),\n-\t\t  N_(\"read the tree into the index under <subdirectory>/\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prefix\",\n+\t\t\t.value = &opts.prefix,\n+\t\t\t.argh = N_(\"<subdirectory>/\"),\n+\t\t\t.help = N_(\"read the tree into the index under <subdirectory>/\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_BOOL('u', NULL, &opts.update,\n \t\t\t N_(\"update working tree with merge result\")),\n \t\tOPT_CALLBACK_F(0, \"exclude-per-directory\", &opts,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex d4715ed35d7..d4083350090 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1122,9 +1122,15 @@ int cmd_rebase(int argc,\n \t\tOPT_BIT('v', \"verbose\", &options.flags,\n \t\t\tN_(\"display a diffstat of what changed upstream\"),\n \t\t\tREBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT),\n-\t\t{OPTION_NEGBIT, 'n', \"no-stat\", &options.flags, NULL,\n-\t\t\tN_(\"do not show diffstat of what changed upstream\"),\n-\t\t\tPARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT },\n+\t\t{\n+\t\t\t.type = OPTION_NEGBIT,\n+\t\t\t.short_name = 'n',\n+\t\t\t.long_name = \"no-stat\",\n+\t\t\t.value = &options.flags,\n+\t\t\t.help = N_(\"do not show diffstat of what changed upstream\"),\n+\t\t\t.flags = PARSE_OPT_NOARG,\n+\t\t\t.defval = REBASE_DIFFSTAT,\n+\t\t},\n \t\tOPT_BOOL(0, \"signoff\", &options.signoff,\n \t\t\t N_(\"add a Signed-off-by trailer to each commit\")),\n \t\tOPT_BOOL(0, \"committer-date-is-author-date\",\n@@ -1190,9 +1196,16 @@ int cmd_rebase(int argc,\n \t\tOPT_BOOL(0, \"update-refs\", &options.update_refs,\n \t\t\t N_(\"update branches that point to commits \"\n \t\t\t    \"that are being rebased\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t\tN_(\"GPG-sign commits\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_AUTOSTASH(&options.autostash),\n \t\tOPT_STRING_LIST('x', \"exec\", &options.exec, N_(\"exec\"),\n \t\t\t\tN_(\"add exec lines after each commit of the \"\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex aca6c293cdf..4f5ef975494 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\tOPT_STRING(0, \"strategy\", &strategy, N_(\"strategy\"), N_(\"merge strategy\")),\n \t\tOPT_STRVEC('X', \"strategy-option\", &opts->xopts, N_(\"option\"),\n \t\t\tN_(\"option for merge strategy\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tstruct option *options = base_options;\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex fce6b404e92..dab37019d29 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -667,9 +667,15 @@ int cmd_show_branch(int ac,\n \t\t\t N_(\"show remote-tracking branches\")),\n \t\tOPT__COLOR(&showbranch_use_color,\n \t\t\t    N_(\"color '*!+-' corresponding to the branch\")),\n-\t\t{ OPTION_INTEGER, 0, \"more\", &extra, N_(\"n\"),\n-\t\t\t    N_(\"show <n> more commits after the common ancestor\"),\n-\t\t\t    PARSE_OPT_OPTARG, NULL, (intptr_t)1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"more\",\n+\t\t\t.value = &extra,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_SET_INT(0, \"list\", &extra, N_(\"synonym to more=-1\"), -1),\n \t\tOPT_BOOL(0, \"no-name\", &no_name, N_(\"suppress naming strings\")),\n \t\tOPT_BOOL(0, \"current\", &with_current_branch,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex d3e0943b734..b266f12bb48 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -479,9 +479,15 @@ int cmd_tag(int argc,\n \tint edit_flag = 0;\n \tstruct option options[] = {\n \t\tOPT_CMDMODE('l', \"list\", &cmdmode, N_(\"list tag names\"), 'l'),\n-\t\t{ OPTION_INTEGER, 'n', NULL, &filter.lines, N_(\"n\"),\n-\t\t\t\tN_(\"print <n> lines of each tag message\"),\n-\t\t\t\tPARSE_OPT_OPTARG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.short_name = 'n',\n+\t\t\t.value = &filter.lines,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"print <n> lines of each tag message\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_CMDMODE('d', \"delete\", &cmdmode, N_(\"delete tags\"), 'd'),\n \t\tOPT_CMDMODE('v', \"verify\", &cmdmode, N_(\"verify tags\"), 'v'),\n \n@@ -513,9 +519,14 @@ int cmd_tag(int argc,\n \t\t\tN_(\"do not output a newline after empty formatted refs\")),\n \t\tOPT_REF_SORT(&sorting_options),\n \t\t{\n-\t\t\tOPTION_CALLBACK, 0, \"points-at\", &filter.points_at, N_(\"object\"),\n-\t\t\tN_(\"print only tags of the object\"), PARSE_OPT_LASTARG_DEFAULT,\n-\t\t\tparse_opt_object_name, (intptr_t) \"HEAD\"\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"points-at\",\n+\t\t\t.value = &filter.points_at,\n+\t\t\t.argh = N_(\"object\"),\n+\t\t\t.help = N_(\"print only tags of the object\"),\n+\t\t\t.flags = PARSE_OPT_LASTARG_DEFAULT,\n+\t\t\t.callback = parse_opt_object_name,\n+\t\t\t.defval = (intptr_t) \"HEAD\",\n \t\t},\n \t\tOPT_STRING(  0 , \"format\", &format.format, N_(\"format\"),\n \t\t\t   N_(\"format to use for the output\")),\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex b2f6b1a3fbb..ee64b022679 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -964,29 +964,51 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"like --refresh, but ignore assume-unchanged setting\"),\n \t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG,\n \t\t\treally_refresh_callback),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"cacheinfo\", NULL,\n-\t\t\tN_(\"<mode>,<object>,<path>\"),\n-\t\t\tN_(\"add the specified entry to the index\"),\n-\t\t\tPARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t\tNULL, 0,\n-\t\t\tcacheinfo_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"cacheinfo\",\n+\t\t\t.argh = N_(\"<mode>,<object>,<path>\"),\n+\t\t\t.help = N_(\"add the specified entry to the index\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n+\t\t\t\t PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.ll_callback = cacheinfo_callback,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"chmod\", &set_executable_bit, \"(+|-)x\",\n \t\t\tN_(\"override the executable bit of the listed files\"),\n \t\t\tPARSE_OPT_NONEG,\n \t\t\tchmod_callback),\n-\t\t{OPTION_SET_INT, 0, \"assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"mark files as \\\"not changing\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"clear assumed-unchanged bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"mark files as \\\"index-only\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"clear skip-worktree bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"mark files as \\\"not changing\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"clear assumed-unchanged bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"mark files as \\\"index-only\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"clear skip-worktree bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_BOOL(0, \"ignore-skip-worktree-entries\", &ignore_skip_worktree_entries,\n \t\t\t N_(\"do not touch index-only entries\")),\n \t\tOPT_SET_INT(0, \"info-only\", &info_only,\n@@ -995,22 +1017,39 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"remove named paths even if present in worktree\"), 1),\n \t\tOPT_BOOL('z', NULL, &nul_term_line,\n \t\t\t N_(\"with --stdin: input lines are terminated by null bytes\")),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"stdin\", &read_from_stdin, NULL,\n-\t\t\tN_(\"read list of paths to be updated from standard input\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"index-info\", &nul_term_line, NULL,\n-\t\t\tN_(\"add entries from standard input to the index\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_cacheinfo_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"unresolve\", &has_errors, NULL,\n-\t\t\tN_(\"repopulate stages #2 and #3 for the listed paths\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, unresolve_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 'g', \"again\", &has_errors, NULL,\n-\t\t\tN_(\"only update entries that differ from HEAD\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, reupdate_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"stdin\",\n+\t\t\t.value = &read_from_stdin,\n+\t\t\t.help = N_(\"read list of paths to be updated from standard input\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"index-info\",\n+\t\t\t.value = &nul_term_line,\n+\t\t\t.help = N_(\"add entries from standard input to the index\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_cacheinfo_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"unresolve\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"repopulate stages #2 and #3 for the listed paths\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = unresolve_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.short_name = 'g',\n+\t\t\t.long_name = \"again\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"only update entries that differ from HEAD\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = reupdate_callback,\n+\t\t},\n \t\tOPT_BIT(0, \"ignore-missing\", &refresh_args.flags,\n \t\t\tN_(\"ignore files missing from worktree\"),\n \t\t\tREFRESH_IGNORE_MISSING),\n@@ -1036,12 +1075,22 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"write out the index even if is not flagged as changed\"), 1),\n \t\tOPT_BOOL(0, \"fsmonitor\", &fsmonitor,\n \t\t\tN_(\"enable or disable file system monitor\")),\n-\t\t{OPTION_SET_INT, 0, \"fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"mark files as fsmonitor valid\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"clear fsmonitor valid bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"mark files as fsmonitor valid\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"clear fsmonitor valid bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/builtin/write-tree.c b/builtin/write-tree.c\nindex 43f233e69b0..5a8dc377ec0 100644\n--- a/builtin/write-tree.c\n+++ b/builtin/write-tree.c\n@@ -31,10 +31,14 @@ int cmd_write_tree(int argc,\n \t\t\tWRITE_TREE_MISSING_OK),\n \t\tOPT_STRING(0, \"prefix\", &tree_prefix, N_(\"<prefix>/\"),\n \t\t\t   N_(\"write tree object for a subdirectory <prefix>\")),\n-\t\t{ OPTION_BIT, 0, \"ignore-cache-tree\", &flags, NULL,\n-\t\t  N_(\"only useful for debugging\"),\n-\t\t  PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL,\n-\t\t  WRITE_TREE_IGNORE_CACHE_TREE },\n+\t\t{\n+\t\t\t.type = OPTION_BIT,\n+\t\t\t.long_name = \"ignore-cache-tree\",\n+\t\t\t.value = &flags,\n+\t\t\t.help = N_(\"only useful for debugging\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG,\n+\t\t\t.defval = WRITE_TREE_IGNORE_CACHE_TREE,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/diff.c b/diff.c\nindex 08f5e00a2cc..f2fcc7f3c22 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts,\n \t\tOPT_CALLBACK_F(0, \"diff-filter\", options, N_(\"[(A|C|D|M|R|T|U|X|B)...[*]]\"),\n \t\t\t       N_(\"select files by diff type\"),\n \t\t\t       PARSE_OPT_NONEG, diff_opt_diff_filter),\n-\t\t{ OPTION_CALLBACK, 0, \"output\", options, N_(\"<file>\"),\n-\t\t  N_(\"output to a specific file\"),\n-\t\t  PARSE_OPT_NONEG, NULL, 0, diff_opt_output },\n-\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"output\",\n+\t\t\t.value = options,\n+\t\t\t.argh = N_(\"<file>\"),\n+\t\t\t.help = N_(\"output to a specific file\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t\t.ll_callback = diff_opt_output,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/ref-filter.h b/ref-filter.h\nindex 013d4cfa64b..c98c4fbd4c1 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -114,11 +114,16 @@ struct ref_format {\n }\n \n /*  Macros for checking --merged and --no-merged options */\n-#define _OPT_MERGED_NO_MERGED(option, filter, h) \\\n-\t{ OPTION_CALLBACK, 0, option, (filter), N_(\"commit\"), (h), \\\n-\t  PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n-\t  parse_opt_merge_filter, (intptr_t) \"HEAD\" \\\n-\t}\n+#define _OPT_MERGED_NO_MERGED(option, filter, h) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.long_name = option, \\\n+\t.value = (filter), \\\n+\t.argh = N_(\"commit\"), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n+\t.callback = parse_opt_merge_filter, \\\n+\t.defval = (intptr_t) \"HEAD\", \\\n+}\n #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"merged\", f, h)\n #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"no-merged\", f, h)\n \ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex bfe45ec68b0..997f55fd45b 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv)\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n \t\tOPT_BOOL('D', \"no-doubt\", &boolean, \"begins with 'no-'\"),\n-\t\t{ OPTION_SET_INT, 'B', \"no-fear\", &boolean, NULL,\n-\t\t  \"be brave\", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.short_name = 'B',\n+\t\t\t.long_name = \"no-fear\",\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"be brave\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_COUNTUP('b', \"boolean\", &boolean, \"increment by one\"),\n \t\tOPT_BIT('4', \"or4\", &boolean,\n \t\t\t\"bitwise-or boolean with ...0100\", 4),\n@@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Magic arguments\"),\n \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n \t\t\tnumber_callback),\n-\t\t{ OPTION_COUNTUP, '+', NULL, &boolean, NULL, \"same as -b\",\n-\t\t  PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH },\n-\t\t{ OPTION_COUNTUP, 0, \"ambiguous\", &ambiguous, NULL,\n-\t\t  \"positive ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n-\t\t{ OPTION_COUNTUP, 0, \"no-ambiguous\", &ambiguous, NULL,\n-\t\t  \"negative ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.short_name = '+',\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"same as -b\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"positive ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"no-ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"negative ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_GROUP(\"Standard options\"),\n \t\tOPT__ABBREV(&abbrev),\n \t\tOPT__VERBOSE(&verbose, \"be verbose\"),\n\n-- \n2.49.0.604.gff1f9ca942.dirty\n\n"},{"id":"515439","messageId":"20250401-b4-pks-parse-options-integers-v1-5-a628ad40c3b4@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH 5/5] parse-options: detect mismatches in integer signedness","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-01T15:01:20Z","receivedAt":"2025-04-01T15:01:41Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It was reported that \"t5620-backfill.sh\" fails on s390x and sparc64 in a\ntest that exercises the \"--min-batch-size\" command line option. The\nsymptom was that the option didn't seem to have an effect: we didn't\nfetch objects with a batch size of 20, but instead fetched all objects\nat once.\n\nAs it turns out, the root cause is that `--min-batch-size` uses\n`OPT_INTEGER()` to parse the command line option. While this macro\nexpects the caller to pass a pointer to an integer, we instead pass a\npointer to a `size_t`. This coincidentally works on most platforms, but\nit breaks apart on the mentioned platforms because they are big endian.\n\nThis issue isn't specific to git-backfill(1): there are a couple of\nother places where we have the same type confusion going on. This\nindicates that the issue really is the interface that the parse-options\nsubsystem provides -- it is simply too easy to get this wrong as there\nisn't any kind of compiler warning, and things just work on the most\ncommon systems.\n\nAddress the systemic issue by introducing two new build asserts\n`BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names\nalready hint at, those macros will cause a compiler error when passed a\nvalue that is not signed or unsigned, respectively.\n\nAdapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to\nuse those asserts. This uncovers a small set of sites where we indeed\nhave the same bug as in git-backfill(1). Adapt all of them to use the\ncorrect option.\n\nReported-by: Todd Zullinger <tmz@pobox.com>\nReported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>\nHelped-by: SZEDER Gábor <szeder.dev@gmail.com>\nHelped-by: Jeff King <peff@peff.net>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n apply.c            | 4 ++--\n builtin/backfill.c | 4 ++--\n builtin/column.c   | 2 +-\n builtin/grep.c     | 4 ++--\n git-compat-util.h  | 7 +++++++\n parse-options.h    | 6 +++---\n 6 files changed, 17 insertions(+), 10 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex f274a379487..a850c7d75fe 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv,\n \t\t/* Think twice before adding \"--nul\" synonym to this */\n \t\tOPT_SET_INT('z', NULL, &state->line_termination,\n \t\t\tN_(\"paths are separated with NUL character\"), '\\0'),\n-\t\tOPT_INTEGER('C', NULL, &state->p_context,\n-\t\t\t\tN_(\"ensure at least <n> lines of context match\")),\n+\t\tOPT_UNSIGNED('C', NULL, &state->p_context,\n+\t\t\t     N_(\"ensure at least <n> lines of context match\")),\n \t\tOPT_CALLBACK(0, \"whitespace\", state, N_(\"action\"),\n \t\t\tN_(\"detect new or modified lines that have whitespace errors\"),\n \t\t\tapply_option_parse_whitespace),\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 33e1ea2f84f..d95d7a2d4d6 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n \t\t.sparse = 0,\n \t};\n \tstruct option options[] = {\n-\t\tOPT_INTEGER(0, \"min-batch-size\", &ctx.min_batch_size,\n-\t\t\t    N_(\"Minimum number of objects to request at a time\")),\n+\t\tOPT_UNSIGNED(0, \"min-batch-size\", &ctx.min_batch_size,\n+\t\t\t     N_(\"Minimum number of objects to request at a time\")),\n \t\tOPT_BOOL(0, \"sparse\", &ctx.sparse,\n \t\t\t N_(\"Restrict the missing objects to the current sparse-checkout\")),\n \t\tOPT_END(),\ndiff --git a/builtin/column.c b/builtin/column.c\nindex 50314cc2559..ce6443d5fac 100644\n--- a/builtin/column.c\n+++ b/builtin/column.c\n@@ -31,7 +31,7 @@ int cmd_column(int argc,\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"command\", &real_command, N_(\"name\"), N_(\"lookup config vars\")),\n \t\tOPT_COLUMN(0, \"mode\", &colopts, N_(\"layout to use\")),\n-\t\tOPT_INTEGER(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n+\t\tOPT_UNSIGNED(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n \t\tOPT_INTEGER(0, \"width\", &copts.width, N_(\"maximum width\")),\n \t\tOPT_STRING(0, \"indent\", &copts.indent, N_(\"string\"), N_(\"padding space on left border\")),\n \t\tOPT_STRING(0, \"nl\", &copts.nl, N_(\"string\"), N_(\"padding space on right border\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex c4869733e1b..f23a6f1dc86 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -983,9 +983,9 @@ int cmd_grep(int argc,\n \t\tOPT_CALLBACK('C', \"context\", &opt, N_(\"n\"),\n \t\t\tN_(\"show <n> context lines before and after matches\"),\n \t\t\tcontext_callback),\n-\t\tOPT_INTEGER('B', \"before-context\", &opt.pre_context,\n+\t\tOPT_UNSIGNED('B', \"before-context\", &opt.pre_context,\n \t\t\tN_(\"show <n> context lines before matches\")),\n-\t\tOPT_INTEGER('A', \"after-context\", &opt.post_context,\n+\t\tOPT_UNSIGNED('A', \"after-context\", &opt.post_context,\n \t\t\tN_(\"show <n> context lines after matches\")),\n \t\tOPT_INTEGER(0, \"threads\", &num_threads,\n \t\t\tN_(\"use <n> worker threads\")),\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex cf733b38acd..1218fcf81a4 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare)\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \\\n \t\t\t\t\t\t\t  __typeof__(*(src))))\n+\n+# define BARF_UNLESS_SIGNED(var)   BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0)\n+# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0)\n #else\n # define BARF_UNLESS_AN_ARRAY(arr) 0\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \\\n \t\t\t\t sizeof(*(dst)) == sizeof(*(src)))\n+\n+# define BARF_UNLESS_SIGNED(var)   0\n+# define BARF_UNLESS_UNSIGNED(var) 0\n #endif\n+\n /*\n  * ARRAY_SIZE - get the number of elements in a visible array\n  * @x: the array whose size you want.\ndiff --git a/parse-options.h b/parse-options.h\nindex 20ea7d2ab13..7b7c9d901cb 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -219,7 +219,7 @@ struct option {\n \t.type = OPTION_INTEGER, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_SIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -229,7 +229,7 @@ struct option {\n \t.type = OPTION_UNSIGNED, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -292,7 +292,7 @@ struct option {\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n\n-- \n2.49.0.604.gff1f9ca942.dirty\n\n"},{"id":"515461","messageId":"f6c7c92b-931c-4be4-8d76-9d4ed147e983@web.de","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-2-a628ad40c3b4@pks.im","subject":"Re: [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2025-04-01T18:47:12Z","receivedAt":"2025-04-01T18:47:26Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 01.04.25 um 17:01 schrieb Patrick Steinhardt:\n> diff --git a/parse-options.c b/parse-options.c\n> index 35fbb3b0d63..dbda9b7cfe7 100644\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n>  \t\t\treturn (*opt->ll_callback)(p, opt, p_arg, p_unset);\n>  \t}\n>  \tcase OPTION_INTEGER:\n> +\t{\n> +\t\tintmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1);\n\nUgh, how does this not overflow?  The macro maximum_signed_value_of_type\ndoes a similar calculation better.\n\n> +\t\tintmax_t lower_bound = -upper_bound - 1;\n\nThis depends on two's complement being used, which is bad for purity and\nportability to obsolete machines, but probably OK in practice.\n\n> +\t\tintmax_t value;\n> +\n>  \t\tif (unset) {\n> -\t\t\t*(int *)opt->value = 0;\n> -\t\t\treturn 0;\n> -\t\t}\n> -\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> -\t\t\t*(int *)opt->value = opt->defval;\n> -\t\t\treturn 0;\n> -\t\t}\n> -\t\tif (get_arg(p, opt, flags, &arg))\n> +\t\t\tvalue = 0;\n> +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> +\t\t\tvalue = opt->defval;\n> +\t\t} else if (get_arg(p, opt, flags, &arg)) {\n>  \t\t\treturn -1;\n> -\t\tif (!*arg)\n> +\t\t} else if (!*arg) {\n>  \t\t\treturn error(_(\"%s expects a numerical value\"),\n>  \t\t\t\t     optname(opt, flags));\n> -\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n> -\t\tif (*s)\n> -\t\t\treturn error(_(\"%s expects a numerical value\"),\n> -\t\t\t\t     optname(opt, flags));\n> -\t\treturn 0;\n> +\t\t} else {\n> +\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n> +\t\t\tif (*s)\n> +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t\t     optname(opt, flags));\n> +\n> +\t\t}\n>\n> +\t\tif (value < lower_bound || value > upper_bound)\n> +\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n> +\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n> +\n> +\t\tswitch (opt->precision) {\n> +\t\tcase 1:\n> +\t\t\t*(int8_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 2:\n> +\t\t\t*(int16_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 4:\n> +\t\t\t*(int32_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 8:\n> +\t\t\t*(int64_t *)opt->value = value;\n> +\t\t\treturn 0;\n\nDo we even need all these sizes?  Or can we whittle it down to ssize_t?\n\nAnd for which quantities do we need to accept negative values anyway?\n\nRené\n\n"},{"id":"516185","messageId":"Z_40Zv_0HCscTo0M@pks.im","threadId":"63234","inReplyTo":"f6c7c92b-931c-4be4-8d76-9d4ed147e983@web.de","subject":"Re: [PATCH 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T10:26:46Z","receivedAt":"2025-04-15T10:26:55Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 01, 2025 at 08:47:12PM +0200, René Scharfe wrote:\n> Am 01.04.25 um 17:01 schrieb Patrick Steinhardt:\n> > diff --git a/parse-options.c b/parse-options.c\n> > index 35fbb3b0d63..dbda9b7cfe7 100644\n> > --- a/parse-options.c\n> > +++ b/parse-options.c\n> > @@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n> >  \t\t\treturn (*opt->ll_callback)(p, opt, p_arg, p_unset);\n> >  \t}\n> >  \tcase OPTION_INTEGER:\n> > +\t{\n> > +\t\tintmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1);\n> \n> Ugh, how does this not overflow?  The macro maximum_signed_value_of_type\n> does a similar calculation better.\n\nOh, nice :) I'll definitely use this, thanks!\n\n> > +\t\tintmax_t lower_bound = -upper_bound - 1;\n> \n> This depends on two's complement being used, which is bad for purity and\n> portability to obsolete machines, but probably OK in practice.\n\nAgreed. We would notice quite fast in case there are any machines out\nthere that don't handle this well as our tests exercise this logic.\n\n> > +\t\tintmax_t value;\n> > +\n> >  \t\tif (unset) {\n> > -\t\t\t*(int *)opt->value = 0;\n> > -\t\t\treturn 0;\n> > -\t\t}\n> > -\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> > -\t\t\t*(int *)opt->value = opt->defval;\n> > -\t\t\treturn 0;\n> > -\t\t}\n> > -\t\tif (get_arg(p, opt, flags, &arg))\n> > +\t\t\tvalue = 0;\n> > +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> > +\t\t\tvalue = opt->defval;\n> > +\t\t} else if (get_arg(p, opt, flags, &arg)) {\n> >  \t\t\treturn -1;\n> > -\t\tif (!*arg)\n> > +\t\t} else if (!*arg) {\n> >  \t\t\treturn error(_(\"%s expects a numerical value\"),\n> >  \t\t\t\t     optname(opt, flags));\n> > -\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n> > -\t\tif (*s)\n> > -\t\t\treturn error(_(\"%s expects a numerical value\"),\n> > -\t\t\t\t     optname(opt, flags));\n> > -\t\treturn 0;\n> > +\t\t} else {\n> > +\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n> > +\t\t\tif (*s)\n> > +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> > +\t\t\t\t\t     optname(opt, flags));\n> > +\n> > +\t\t}\n> >\n> > +\t\tif (value < lower_bound || value > upper_bound)\n> > +\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n> > +\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n> > +\n> > +\t\tswitch (opt->precision) {\n> > +\t\tcase 1:\n> > +\t\t\t*(int8_t *)opt->value = value;\n> > +\t\t\treturn 0;\n> > +\t\tcase 2:\n> > +\t\t\t*(int16_t *)opt->value = value;\n> > +\t\t\treturn 0;\n> > +\t\tcase 4:\n> > +\t\t\t*(int32_t *)opt->value = value;\n> > +\t\t\treturn 0;\n> > +\t\tcase 8:\n> > +\t\t\t*(int64_t *)opt->value = value;\n> > +\t\t\treturn 0;\n> \n> Do we even need all these sizes?  Or can we whittle it down to ssize_t?\n\nSome of them. We already pass both `int` and `size_t`, which means that\nwe definitely need at least `int32` and `int64`. Whether we also need to\nhandle `int8` or `int16` is a different question, but it doesn't add\nmuch complexity anyway.\n\n> And for which quantities do we need to accept negative values anyway?\n\nIn most cases we probably don't need that, agreed. But there are\ncallsites where we use negative values to indicate the default value,\nand I bet there are some options where negative values do make sense\nindeed.\n\nBut in the end I think we should harden our integer handling for\noptions, which is where the next patch comes in that introduces\n`OPTION_UNSIGNED()`. I don't want to convert all users in this patch\nseries, but I definitely think that we should adapt them over time so\nthat they stop accepting signed integers.\n\nPatrick\n"},{"id":"516186","messageId":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH v2 0/5] parse-options: harden handling of integer values","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:04Z","receivedAt":"2025-04-15T12:14:16Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series addresses the issues raised in [1] and [2]. As\ndiscussed in [1], the series also introduces a couple of safeguards to\nmake it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`:\n\n  - We now track the precision of the underlying integer types. This\n    makes it possible to pass arbitrarily-sized integers to those\n    options, not only `int` and `unsigned long`, respectively.\n\n  - We introduce a build assert to verify that the passed variable has\n    correct signedness.\n\nFurthermore, the series introduces `OPT_UNSIGNED()` to adapt all\ncallsites that previously used variables with the wrong signedness.\n\nChanges in v2:\n  - Adapt computation of upper bounds to use similar logic to\n    `maximum_signed_value_of_type()`.\n  - Link to v1: https://lore.kernel.org/r/20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de>\n[2]: <Z8HW6petWuMRWSXf@teonanacatl.net>\n\n---\nPatrick Steinhardt (5):\n      global: use designated initializers for options\n      parse-options: introduce precision handling for `OPTION_INTEGER`\n      parse-options: introduce precision handling for `OPTION_MAGNITUDE`\n      parse-options: introduce `OPTION_UNSIGNED`\n      parse-options: detect mismatches in integer signedness\n\n apply.c                       |   4 +-\n archive.c                     |  35 ++++++++---\n builtin/am.c                  |  28 ++++++---\n builtin/backfill.c            |   4 +-\n builtin/clone.c               |  13 ++++-\n builtin/column.c              |   2 +-\n builtin/commit-tree.c         |  12 +++-\n builtin/commit.c              |  62 +++++++++++++++-----\n builtin/config.c              |  13 ++++-\n builtin/describe.c            |  24 ++++++--\n builtin/fetch.c               |  10 +++-\n builtin/fmt-merge-msg.c       |  27 ++++++---\n builtin/gc.c                  |  12 +++-\n builtin/grep.c                |  18 ++++--\n builtin/init-db.c             |  13 +++--\n builtin/ls-remote.c           |  11 +++-\n builtin/merge.c               |  38 +++++++++---\n builtin/read-tree.c           |  11 +++-\n builtin/rebase.c              |  25 ++++++--\n builtin/revert.c              |  12 +++-\n builtin/show-branch.c         |  13 ++++-\n builtin/tag.c                 |  24 ++++++--\n builtin/update-index.c        | 131 +++++++++++++++++++++++++++++-------------\n builtin/write-tree.c          |  12 ++--\n diff.c                        |  13 +++--\n git-compat-util.h             |   7 +++\n parse-options.c               | 131 +++++++++++++++++++++++++++++++++++-------\n parse-options.h               |  23 +++++++-\n ref-filter.h                  |  15 +++--\n t/helper/test-parse-options.c |  46 ++++++++++++---\n t/t0040-parse-options.sh      |  57 +++++++++++++++++-\n 31 files changed, 657 insertions(+), 189 deletions(-)\n\nRange-diff versus v1:\n\n1:  632c627b9ba = 1:  92d1cbd1153 global: use designated initializers for options\n2:  65c450a4395 ! 2:  8d2400d7470 parse-options: introduce precision handling for `OPTION_INTEGER`\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n      \t}\n      \tcase OPTION_INTEGER:\n     +\t{\n    -+\t\tintmax_t upper_bound = (((intmax_t) 1 << (opt->precision * 8 - 1)) - 1);\n    ++\t\tintmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision);\n     +\t\tintmax_t lower_bound = -upper_bound - 1;\n     +\t\tintmax_t value;\n     +\n3:  4946ca0f702 ! 3:  a9b0d8c1127 parse-options: introduce precision handling for `OPTION_MAGNITUDE`\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n      \t}\n      \tcase OPTION_MAGNITUDE:\n     +\t{\n    -+\t\tuintmax_t upper_bound = 0;\n    ++\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n     +\t\tunsigned long value;\n    -+\n    -+\t\t/*\n    -+\t\t * It's stupid, but the obvious way of calculating the upper\n    -+\t\t * bound via `2 ^ n - 1` overflows.\n    -+\t\t */\n    -+\t\tfor (size_t i = 0; i < opt->precision * 8; i++)\n    -+\t\t\tupper_bound |= ((uintmax_t) 1 << i);\n     +\n      \t\tif (unset) {\n     -\t\t\t*(unsigned long *)opt->value = 0;\n4:  9728d57d5df ! 4:  743afbf539e parse-options: introduce `OPTION_UNSIGNED`\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n      \t}\n     +\tcase OPTION_UNSIGNED:\n     +\t{\n    -+\t\tuintmax_t upper_bound = 0;\n    ++\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n     +\t\tuintmax_t value;\n     +\n    -+\t\t/*\n    -+\t\t * It's stupid, but the obvious way of calculating the upper\n    -+\t\t * bound via `2 ^ n - 1` overflows.\n    -+\t\t */\n    -+\t\tfor (size_t i = 0; i < opt->precision * 8; i++)\n    -+\t\t\tupper_bound |= ((uintmax_t) 1 << i);\n    -+\n     +\t\tif (unset) {\n     +\t\t\tvalue = 0;\n     +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n     +\t}\n      \tcase OPTION_MAGNITUDE:\n      \t{\n    - \t\tuintmax_t upper_bound = 0;\n    + \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n     \n      ## parse-options.h ##\n     @@ parse-options.h: enum parse_opt_type {\n5:  59aacc72548 = 5:  1e42672a439 parse-options: detect mismatches in integer signedness\n\n---\nbase-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff\nchange-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011\n\n"},{"id":"516189","messageId":"20250415-b4-pks-parse-options-integers-v2-1-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","subject":"[PATCH v2 1/5] global: use designated initializers for options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:05Z","receivedAt":"2025-04-15T12:14:17Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we expose macros for most of our different option types understood\nby the \"parse-options\" subsystem, not every combination of fields that\nhas one as that would otherwise quickly lead to an explosion of macros.\nInstead, we just initialize structures manually for those variants of\nfields that don't have a macro.\n\nCallsites that open-code these structure initialization don't use\ndesignated initializers though and instead just provide values for each\nof the fields that they want to initialize. This has three significant\ndownsides:\n\n  - Callsites need to specify all values up to the last field that they\n    care about. This often includes fields that should simply be left at\n    their default zero-initialized state, which adds distraction.\n\n  - Any reader not deeply familiar with the layout of the structure\n    has a hard time figuring out what the respective initializers mean.\n\n  - Reordering or introducing new fields in the middle of the structure\n    is impossible without adapting all callsites.\n\nConvert all sites to instead use designated initializers, which we have\nstarted using in our codebase quite a while ago. This allows us to skip\nany default-initialized fields, gives the reader context by specifying\nthe field names and allows us to reorder or introduce new fields where\nwe want to.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n archive.c                     |  35 ++++++++---\n builtin/am.c                  |  28 ++++++---\n builtin/clone.c               |  13 ++++-\n builtin/commit-tree.c         |  12 +++-\n builtin/commit.c              |  62 +++++++++++++++-----\n builtin/config.c              |  13 ++++-\n builtin/describe.c            |  24 ++++++--\n builtin/fetch.c               |  10 +++-\n builtin/fmt-merge-msg.c       |  25 +++++---\n builtin/gc.c                  |  12 +++-\n builtin/grep.c                |  14 +++--\n builtin/init-db.c             |  13 +++--\n builtin/ls-remote.c           |  11 +++-\n builtin/merge.c               |  37 +++++++++---\n builtin/read-tree.c           |  11 +++-\n builtin/rebase.c              |  25 ++++++--\n builtin/revert.c              |  12 +++-\n builtin/show-branch.c         |  12 +++-\n builtin/tag.c                 |  23 ++++++--\n builtin/update-index.c        | 131 +++++++++++++++++++++++++++++-------------\n builtin/write-tree.c          |  12 ++--\n diff.c                        |  13 +++--\n ref-filter.h                  |  15 +++--\n t/helper/test-parse-options.c |  38 +++++++++---\n 24 files changed, 443 insertions(+), 158 deletions(-)\n\ndiff --git a/archive.c b/archive.c\nindex 8be4e7ac8db..67bba3cd301 100644\n--- a/archive.c\n+++ b/archive.c\n@@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv,\n \t\tOPT_STRING(0, \"format\", &format, N_(\"fmt\"), N_(\"archive format\")),\n \t\tOPT_STRING(0, \"prefix\", &base, N_(\"prefix\"),\n \t\t\tN_(\"prepend prefix to each pathname in the archive\")),\n-\t\t{ OPTION_CALLBACK, 0, \"add-file\", args, N_(\"file\"),\n-\t\t  N_(\"add untracked file to archive\"), 0, add_file_cb,\n-\t\t  (intptr_t)&base },\n-\t\t{ OPTION_CALLBACK, 0, \"add-virtual-file\", args,\n-\t\t  N_(\"path:content\"), N_(\"add untracked file to archive\"), 0,\n-\t\t  add_file_cb, (intptr_t)&base },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"file\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-virtual-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"path:content\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n \t\tOPT_STRING('o', \"output\", &output, N_(\"file\"),\n \t\t\tN_(\"write the archive to this file\")),\n \t\tOPT_BOOL(0, \"worktree-attributes\", &worktree_attributes,\n \t\t\tN_(\"read .gitattributes in working directory\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report archived files on stderr\")),\n-\t\t{ OPTION_STRING, 0, \"mtime\", &mtime_option, N_(\"time\"),\n-\t\t  N_(\"set modification time of archive entries\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"mtime\",\n+\t\t\t.value = &mtime_option,\n+\t\t\t.argh = N_(\"time\"),\n+\t\t\t.help = N_(\"set modification time of archive entries\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_NUMBER_CALLBACK(&compression_level,\n \t\t\tN_(\"set compression level\"), number_callback),\n \t\tOPT_GROUP(\"\"),\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 3b61bd4c333..4afb519830f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2400,11 +2400,16 @@ int cmd_am(int argc,\n \t\tOPT_CMDMODE(0, \"quit\", &resume_mode,\n \t\t\tN_(\"abort the patching operation but keep HEAD where it is\"),\n \t\t\tRESUME_QUIT),\n-\t\t{ OPTION_CALLBACK, 0, \"show-current-patch\", &resume_mode,\n-\t\t  \"(diff|raw)\",\n-\t\t  N_(\"show the patch being applied\"),\n-\t\t  PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t  parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"show-current-patch\",\n+\t\t\t.value = &resume_mode,\n+\t\t\t.argh = \"(diff|raw)\",\n+\t\t\t.help = N_(\"show the patch being applied\"),\n+\t\t\t.flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.callback = parse_opt_show_current_patch,\n+\t\t\t.defval = RESUME_SHOW_PATCH_RAW,\n+\t\t},\n \t\tOPT_CMDMODE(0, \"retry\", &resume_mode,\n \t\t\tN_(\"try to apply current patch again\"),\n \t\t\tRESUME_APPLY),\n@@ -2417,9 +2422,16 @@ int cmd_am(int argc,\n \t\tOPT_BOOL(0, \"ignore-date\", &state.ignore_date,\n \t\t\tN_(\"use current timestamp for author date\")),\n \t\tOPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &state.sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG-sign commits\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &state.sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"empty\", &state.empty_type, \"(stop|drop|keep)\",\n \t\t  N_(\"how to handle empty patches\"),\n \t\t  PARSE_OPT_NONEG, am_option_parse_empty),\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 88276e5b7ab..9c3547f41e3 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -930,9 +930,16 @@ int cmd_clone(int argc,\n \t\t\t N_(\"don't use local hardlinks, always copy\")),\n \t\tOPT_BOOL('s', \"shared\", &option_shared,\n \t\t\t N_(\"setup as shared repository\")),\n-\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"recurse-submodules\",\n+\t\t\t.value = &option_recurse_submodules,\n+\t\t\t.argh = N_(\"pathspec\"),\n+\t\t\t.help = N_(\"initialize submodules in the clone\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.callback = recurse_submodules_cb,\n+\t\t\t.defval = (intptr_t)\".\",\n+\t\t},\n \t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n \t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n \t\t\t    N_(\"number of submodules cloned in parallel\")),\ndiff --git a/builtin/commit-tree.c b/builtin/commit-tree.c\nindex 38457600a4e..c787133d004 100644\n--- a/builtin/commit-tree.c\n+++ b/builtin/commit-tree.c\n@@ -111,8 +111,16 @@ int cmd_commit_tree(int argc,\n \t\tOPT_CALLBACK_F('F', NULL, &buffer, N_(\"file\"),\n \t\t\tN_(\"read commit log message from file\"), PARSE_OPT_NONEG,\n \t\t\tparse_file_arg_callback),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t\tN_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tint ret;\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 2f459682221..66bd91fd523 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1542,17 +1542,34 @@ struct repository *repo UNUSED)\n \t\t\t    STATUS_FORMAT_LONG),\n \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n \t\t\t N_(\"terminate entries with NUL\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n-\t\t{ OPTION_STRING, 0, \"ignored\", &ignored_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"traditional\" },\n-\t\t{ OPTION_STRING, 0, \"ignore-submodules\", &ignore_submodule_arg, N_(\"when\"),\n-\t\t  N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignored\",\n+\t\t\t.value = &ignored_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"traditional\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignore-submodules\",\n+\t\t\t.value = &ignore_submodule_arg,\n+\t\t\t.argh = N_(\"when\"),\n+\t\t\t.help = N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_COLUMN(0, \"column\", &s.colopts, N_(\"list untracked files in columns\")),\n \t\tOPT_BOOL(0, \"no-renames\", &no_renames, N_(\"do not detect renames\")),\n \t\tOPT_CALLBACK_F('M', \"find-renames\", &rename_score_arg,\n@@ -1688,8 +1705,16 @@ int cmd_commit(int argc,\n \t\tOPT_BOOL('e', \"edit\", &edit_flag, N_(\"force edit of commit\")),\n \t\tOPT_CLEANUP(&cleanup_arg),\n \t\tOPT_BOOL(0, \"status\", &include_status, N_(\"include status in commit message template\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\t/* end commit message options */\n \n \t\tOPT_GROUP(N_(\"Commit contents options\")),\n@@ -1714,7 +1739,16 @@ int cmd_commit(int argc,\n \t\t\t N_(\"terminate entries with NUL\")),\n \t\tOPT_BOOL(0, \"amend\", &amend, N_(\"amend previous commit\")),\n \t\tOPT_BOOL(0, \"no-post-rewrite\", &no_post_rewrite, N_(\"bypass post-rewrite hook\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg, N_(\"mode\"), N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"), PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_PATHSPEC_FROM_FILE(&pathspec_from_file),\n \t\tOPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),\n \t\t/* end commit contents options */\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 53a90094e31..f70d6354772 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -131,9 +131,16 @@ struct config_display_options {\n #define TYPE_COLOR\t\t6\n #define TYPE_BOOL_OR_STR\t7\n \n-#define OPT_CALLBACK_VALUE(s, l, v, h, i) \\\n-\t{ OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \\\n-\tPARSE_OPT_NONEG, option_parse_type, (i) }\n+#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \\\n+\t.callback = option_parse_type, \\\n+\t.defval = (i), \\\n+}\n \n static int option_parse_type(const struct option *opt, const char *arg,\n \t\t\t     int unset)\ndiff --git a/builtin/describe.c b/builtin/describe.c\nindex e2e73f3d757..2da9f4fed01 100644\n--- a/builtin/describe.c\n+++ b/builtin/describe.c\n@@ -601,12 +601,24 @@ int cmd_describe(int argc,\n \t\t\t   N_(\"do not consider tags matching <pattern>\")),\n \t\tOPT_BOOL(0, \"always\",        &always,\n \t\t\tN_(\"show abbreviated commit object as fallback\")),\n-\t\t{OPTION_STRING, 0, \"dirty\",  &dirty, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-dirty\"},\n-\t\t{OPTION_STRING, 0, \"broken\",  &broken, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-broken\"},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"dirty\",\n+\t\t\t.value = &dirty,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-dirty\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"broken\",\n+\t\t\t.value = &broken,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-broken\",\n+\t\t},\n \t\tOPT_END(),\n \t};\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 02af5054690..3a5159d9e69 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2359,8 +2359,14 @@ int cmd_fetch(int argc,\n \t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n \t\t\t      N_(\"re-fetch without negotiating common commits\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n-\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"submodule-prefix\",\n+\t\t\t.value = &submodule_prefix,\n+\t\t\t.argh = N_(\"dir\"),\n+\t\t\t.help = N_(\"prepend this to submodule path output\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n \t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n \t\t\t   N_(\"default for recursive fetching of submodules \"\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 189cd1096a0..240cdb474bc 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc,\n \tchar *into_name = NULL;\n \tint shortlog_len = -1;\n \tstruct option options[] = {\n-\t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"populate log with at most <n> entries from shortlog\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n-\t\t{ OPTION_INTEGER, 0, \"summary\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"alias for --log (deprecated)\"),\n-\t\t  PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL,\n-\t\t  DEFAULT_MERGE_LOG_LEN },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"log\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"summary\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"alias for --log (deprecated)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n \t\t\tN_(\"use <text> as start of message\")),\n \t\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 99431fd4674..6707a26bc6e 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -699,9 +699,15 @@ struct repository *repo UNUSED)\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n-\t\t{ OPTION_STRING, 0, \"prune\", &prune_expire_arg, N_(\"date\"),\n-\t\t\tN_(\"prune unreferenced objects\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prune\",\n+\t\t\t.value = &prune_expire_arg,\n+\t\t\t.argh = N_(\"date\"),\n+\t\t\t.help = N_(\"prune unreferenced objects\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)prune_expire_arg,\n+\t\t},\n \t\tOPT_BOOL(0, \"cruft\", &cfg.cruft_packs, N_(\"pack unreferenced objects separately\")),\n \t\tOPT_MAGNITUDE(0, \"max-cruft-size\", &cfg.max_cruft_size,\n \t\t\t      N_(\"with --cruft, limit the size of new cruft packs\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex d1427290f77..c4869733e1b 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -1017,10 +1017,16 @@ int cmd_grep(int argc,\n \t\tOPT_BOOL(0, \"all-match\", &opt.all_match,\n \t\t\tN_(\"show only matches from files that match all patterns\")),\n \t\tOPT_GROUP(\"\"),\n-\t\t{ OPTION_STRING, 'O', \"open-files-in-pager\", &show_in_pager,\n-\t\t\tN_(\"pager\"), N_(\"show matching files in the pager\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n-\t\t\tNULL, (intptr_t)default_pager },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'O',\n+\t\t\t.long_name = \"open-files-in-pager\",\n+\t\t\t.value = &show_in_pager,\n+\t\t\t.argh = N_(\"pager\"),\n+\t\t\t.help = N_(\"show matching files in the pager\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n+\t\t\t.defval = (intptr_t)default_pager,\n+\t\t},\n \t\tOPT_BOOL_F(0, \"ext-grep\", &external_grep_allowed__ignored,\n \t\t\t   N_(\"allow calling of grep(1) (ignored by this build)\"),\n \t\t\t   PARSE_OPT_NOCOMPLETE),\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 196dccdd77a..4a950e44d8d 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -93,10 +93,15 @@ int cmd_init_db(int argc,\n \t\t\t\tN_(\"directory from which templates will be used\")),\n \t\tOPT_SET_INT(0, \"bare\", &is_bare_repository_cfg,\n \t\t\t\tN_(\"create a bare repository\"), 1),\n-\t\t{ OPTION_CALLBACK, 0, \"shared\", &init_shared_repository,\n-\t\t\tN_(\"permissions\"),\n-\t\t\tN_(\"specify that the git repository is to be shared amongst several users\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"shared\",\n+\t\t\t.value = &init_shared_repository,\n+\t\t\t.argh = N_(\"permissions\"),\n+\t\t\t.help = N_(\"specify that the git repository is to be shared amongst several users\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n+\t\t\t.callback = shared_callback\n+\t\t},\n \t\tOPT_BIT('q', \"quiet\", &flags, N_(\"be quiet\"), INIT_DB_QUIET),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n \t\t\t   N_(\"separate git dir from working tree\")),\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 42f34e12361..01a4d4daa1f 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -67,9 +67,14 @@ int cmd_ls_remote(int argc,\n \t\tOPT__QUIET(&quiet, N_(\"do not print remote URL\")),\n \t\tOPT_STRING(0, \"upload-pack\", &uploadpack, N_(\"exec\"),\n \t\t\t   N_(\"path of git-upload-pack on the remote host\")),\n-\t\t{ OPTION_STRING, 0, \"exec\", &uploadpack, N_(\"exec\"),\n-\t\t\t   N_(\"path of git-upload-pack on the remote host\"),\n-\t\t\t   PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"exec\",\n+\t\t\t.value = &uploadpack,\n+\t\t\t.argh = N_(\"exec\"),\n+\t\t\t.help = N_(\"path of git-upload-pack on the remote host\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_BIT('t', \"tags\", &flags, N_(\"limit to tags\"), REF_TAGS),\n \t\tOPT_BIT('b', \"branches\", &flags, N_(\"limit to branches\"), REF_BRANCHES),\n \t\tOPT_BIT_F('h', \"heads\", &flags,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex ba9faf126aa..21787d45165 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"stat\", &show_diffstat,\n \t\tN_(\"show a diffstat at the end of the merge\")),\n \tOPT_BOOL(0, \"summary\", &show_diffstat, N_(\"(synonym to --stat)\")),\n-\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t  N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n-\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n+\t{\n+\t\t.type = OPTION_INTEGER,\n+\t\t.long_name = \"log\",\n+\t\t.value = &shortlog_len,\n+\t\t.argh = N_(\"n\"),\n+\t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t},\n \tOPT_BOOL(0, \"squash\", &squash,\n \t\tN_(\"create a single commit instead of doing a merge\")),\n \tOPT_BOOL(0, \"commit\", &option_commit,\n@@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_CALLBACK('m', \"message\", &merge_msg, N_(\"message\"),\n \t\tN_(\"merge commit message (for a non-fast-forward merge)\"),\n \t\toption_parse_message),\n-\t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n-\t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n-\t\tNULL, 0, option_read_message },\n+\t{\n+\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t.short_name = 'F',\n+\t\t.long_name = \"file\",\n+\t\t.value = &merge_msg,\n+\t\t.argh = N_(\"path\"),\n+\t\t.help = N_(\"read message from file\"),\n+\t\t.flags = PARSE_OPT_NONEG,\n+\t\t.ll_callback = option_read_message,\n+\t},\n \tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n \t\t   N_(\"use <name> instead of the real target\")),\n \tOPT__VERBOSITY(&verbosity),\n@@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"allow-unrelated-histories\", &allow_unrelated_histories,\n \t\t N_(\"allow merging unrelated histories\")),\n \tOPT_SET_INT(0, \"progress\", &show_progress, N_(\"force progress reporting\"), 1),\n-\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t{\n+\t\t.type = OPTION_STRING,\n+\t\t.short_name = 'S',\n+\t\t.long_name = \"gpg-sign\",\n+\t\t.value = &sign_commit,\n+\t\t.argh = N_(\"key-id\"),\n+\t\t.help = N_(\"GPG sign commit\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = (intptr_t) \"\",\n+\t},\n \tOPT_AUTOSTASH(&autostash),\n \tOPT_BOOL(0, \"overwrite-ignore\", &overwrite_ignore, N_(\"update ignored files (default)\")),\n \tOPT_BOOL(0, \"signoff\", &signoff, N_(\"add a Signed-off-by trailer\")),\ndiff --git a/builtin/read-tree.c b/builtin/read-tree.c\nindex d2a807a828b..a8f352f7cd9 100644\n--- a/builtin/read-tree.c\n+++ b/builtin/read-tree.c\n@@ -135,9 +135,14 @@ int cmd_read_tree(int argc,\n \t\t\t N_(\"3-way merge in presence of adds and removes\")),\n \t\tOPT_BOOL(0, \"reset\", &opts.reset,\n \t\t\t N_(\"same as -m, but discard unmerged entries\")),\n-\t\t{ OPTION_STRING, 0, \"prefix\", &opts.prefix, N_(\"<subdirectory>/\"),\n-\t\t  N_(\"read the tree into the index under <subdirectory>/\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prefix\",\n+\t\t\t.value = &opts.prefix,\n+\t\t\t.argh = N_(\"<subdirectory>/\"),\n+\t\t\t.help = N_(\"read the tree into the index under <subdirectory>/\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_BOOL('u', NULL, &opts.update,\n \t\t\t N_(\"update working tree with merge result\")),\n \t\tOPT_CALLBACK_F(0, \"exclude-per-directory\", &opts,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex d4715ed35d7..d4083350090 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1122,9 +1122,15 @@ int cmd_rebase(int argc,\n \t\tOPT_BIT('v', \"verbose\", &options.flags,\n \t\t\tN_(\"display a diffstat of what changed upstream\"),\n \t\t\tREBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT),\n-\t\t{OPTION_NEGBIT, 'n', \"no-stat\", &options.flags, NULL,\n-\t\t\tN_(\"do not show diffstat of what changed upstream\"),\n-\t\t\tPARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT },\n+\t\t{\n+\t\t\t.type = OPTION_NEGBIT,\n+\t\t\t.short_name = 'n',\n+\t\t\t.long_name = \"no-stat\",\n+\t\t\t.value = &options.flags,\n+\t\t\t.help = N_(\"do not show diffstat of what changed upstream\"),\n+\t\t\t.flags = PARSE_OPT_NOARG,\n+\t\t\t.defval = REBASE_DIFFSTAT,\n+\t\t},\n \t\tOPT_BOOL(0, \"signoff\", &options.signoff,\n \t\t\t N_(\"add a Signed-off-by trailer to each commit\")),\n \t\tOPT_BOOL(0, \"committer-date-is-author-date\",\n@@ -1190,9 +1196,16 @@ int cmd_rebase(int argc,\n \t\tOPT_BOOL(0, \"update-refs\", &options.update_refs,\n \t\t\t N_(\"update branches that point to commits \"\n \t\t\t    \"that are being rebased\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t\tN_(\"GPG-sign commits\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_AUTOSTASH(&options.autostash),\n \t\tOPT_STRING_LIST('x', \"exec\", &options.exec, N_(\"exec\"),\n \t\t\t\tN_(\"add exec lines after each commit of the \"\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex aca6c293cdf..4f5ef975494 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\tOPT_STRING(0, \"strategy\", &strategy, N_(\"strategy\"), N_(\"merge strategy\")),\n \t\tOPT_STRVEC('X', \"strategy-option\", &opts->xopts, N_(\"option\"),\n \t\t\tN_(\"option for merge strategy\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tstruct option *options = base_options;\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex fce6b404e92..dab37019d29 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -667,9 +667,15 @@ int cmd_show_branch(int ac,\n \t\t\t N_(\"show remote-tracking branches\")),\n \t\tOPT__COLOR(&showbranch_use_color,\n \t\t\t    N_(\"color '*!+-' corresponding to the branch\")),\n-\t\t{ OPTION_INTEGER, 0, \"more\", &extra, N_(\"n\"),\n-\t\t\t    N_(\"show <n> more commits after the common ancestor\"),\n-\t\t\t    PARSE_OPT_OPTARG, NULL, (intptr_t)1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"more\",\n+\t\t\t.value = &extra,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_SET_INT(0, \"list\", &extra, N_(\"synonym to more=-1\"), -1),\n \t\tOPT_BOOL(0, \"no-name\", &no_name, N_(\"suppress naming strings\")),\n \t\tOPT_BOOL(0, \"current\", &with_current_branch,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex d3e0943b734..b266f12bb48 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -479,9 +479,15 @@ int cmd_tag(int argc,\n \tint edit_flag = 0;\n \tstruct option options[] = {\n \t\tOPT_CMDMODE('l', \"list\", &cmdmode, N_(\"list tag names\"), 'l'),\n-\t\t{ OPTION_INTEGER, 'n', NULL, &filter.lines, N_(\"n\"),\n-\t\t\t\tN_(\"print <n> lines of each tag message\"),\n-\t\t\t\tPARSE_OPT_OPTARG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.short_name = 'n',\n+\t\t\t.value = &filter.lines,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"print <n> lines of each tag message\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_CMDMODE('d', \"delete\", &cmdmode, N_(\"delete tags\"), 'd'),\n \t\tOPT_CMDMODE('v', \"verify\", &cmdmode, N_(\"verify tags\"), 'v'),\n \n@@ -513,9 +519,14 @@ int cmd_tag(int argc,\n \t\t\tN_(\"do not output a newline after empty formatted refs\")),\n \t\tOPT_REF_SORT(&sorting_options),\n \t\t{\n-\t\t\tOPTION_CALLBACK, 0, \"points-at\", &filter.points_at, N_(\"object\"),\n-\t\t\tN_(\"print only tags of the object\"), PARSE_OPT_LASTARG_DEFAULT,\n-\t\t\tparse_opt_object_name, (intptr_t) \"HEAD\"\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"points-at\",\n+\t\t\t.value = &filter.points_at,\n+\t\t\t.argh = N_(\"object\"),\n+\t\t\t.help = N_(\"print only tags of the object\"),\n+\t\t\t.flags = PARSE_OPT_LASTARG_DEFAULT,\n+\t\t\t.callback = parse_opt_object_name,\n+\t\t\t.defval = (intptr_t) \"HEAD\",\n \t\t},\n \t\tOPT_STRING(  0 , \"format\", &format.format, N_(\"format\"),\n \t\t\t   N_(\"format to use for the output\")),\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex b2f6b1a3fbb..ee64b022679 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -964,29 +964,51 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"like --refresh, but ignore assume-unchanged setting\"),\n \t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG,\n \t\t\treally_refresh_callback),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"cacheinfo\", NULL,\n-\t\t\tN_(\"<mode>,<object>,<path>\"),\n-\t\t\tN_(\"add the specified entry to the index\"),\n-\t\t\tPARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t\tNULL, 0,\n-\t\t\tcacheinfo_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"cacheinfo\",\n+\t\t\t.argh = N_(\"<mode>,<object>,<path>\"),\n+\t\t\t.help = N_(\"add the specified entry to the index\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n+\t\t\t\t PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.ll_callback = cacheinfo_callback,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"chmod\", &set_executable_bit, \"(+|-)x\",\n \t\t\tN_(\"override the executable bit of the listed files\"),\n \t\t\tPARSE_OPT_NONEG,\n \t\t\tchmod_callback),\n-\t\t{OPTION_SET_INT, 0, \"assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"mark files as \\\"not changing\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"clear assumed-unchanged bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"mark files as \\\"index-only\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"clear skip-worktree bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"mark files as \\\"not changing\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"clear assumed-unchanged bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"mark files as \\\"index-only\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"clear skip-worktree bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_BOOL(0, \"ignore-skip-worktree-entries\", &ignore_skip_worktree_entries,\n \t\t\t N_(\"do not touch index-only entries\")),\n \t\tOPT_SET_INT(0, \"info-only\", &info_only,\n@@ -995,22 +1017,39 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"remove named paths even if present in worktree\"), 1),\n \t\tOPT_BOOL('z', NULL, &nul_term_line,\n \t\t\t N_(\"with --stdin: input lines are terminated by null bytes\")),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"stdin\", &read_from_stdin, NULL,\n-\t\t\tN_(\"read list of paths to be updated from standard input\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"index-info\", &nul_term_line, NULL,\n-\t\t\tN_(\"add entries from standard input to the index\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_cacheinfo_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"unresolve\", &has_errors, NULL,\n-\t\t\tN_(\"repopulate stages #2 and #3 for the listed paths\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, unresolve_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 'g', \"again\", &has_errors, NULL,\n-\t\t\tN_(\"only update entries that differ from HEAD\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, reupdate_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"stdin\",\n+\t\t\t.value = &read_from_stdin,\n+\t\t\t.help = N_(\"read list of paths to be updated from standard input\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"index-info\",\n+\t\t\t.value = &nul_term_line,\n+\t\t\t.help = N_(\"add entries from standard input to the index\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_cacheinfo_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"unresolve\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"repopulate stages #2 and #3 for the listed paths\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = unresolve_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.short_name = 'g',\n+\t\t\t.long_name = \"again\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"only update entries that differ from HEAD\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = reupdate_callback,\n+\t\t},\n \t\tOPT_BIT(0, \"ignore-missing\", &refresh_args.flags,\n \t\t\tN_(\"ignore files missing from worktree\"),\n \t\t\tREFRESH_IGNORE_MISSING),\n@@ -1036,12 +1075,22 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"write out the index even if is not flagged as changed\"), 1),\n \t\tOPT_BOOL(0, \"fsmonitor\", &fsmonitor,\n \t\t\tN_(\"enable or disable file system monitor\")),\n-\t\t{OPTION_SET_INT, 0, \"fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"mark files as fsmonitor valid\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"clear fsmonitor valid bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"mark files as fsmonitor valid\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"clear fsmonitor valid bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/builtin/write-tree.c b/builtin/write-tree.c\nindex 43f233e69b0..5a8dc377ec0 100644\n--- a/builtin/write-tree.c\n+++ b/builtin/write-tree.c\n@@ -31,10 +31,14 @@ int cmd_write_tree(int argc,\n \t\t\tWRITE_TREE_MISSING_OK),\n \t\tOPT_STRING(0, \"prefix\", &tree_prefix, N_(\"<prefix>/\"),\n \t\t\t   N_(\"write tree object for a subdirectory <prefix>\")),\n-\t\t{ OPTION_BIT, 0, \"ignore-cache-tree\", &flags, NULL,\n-\t\t  N_(\"only useful for debugging\"),\n-\t\t  PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL,\n-\t\t  WRITE_TREE_IGNORE_CACHE_TREE },\n+\t\t{\n+\t\t\t.type = OPTION_BIT,\n+\t\t\t.long_name = \"ignore-cache-tree\",\n+\t\t\t.value = &flags,\n+\t\t\t.help = N_(\"only useful for debugging\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG,\n+\t\t\t.defval = WRITE_TREE_IGNORE_CACHE_TREE,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/diff.c b/diff.c\nindex 08f5e00a2cc..f2fcc7f3c22 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts,\n \t\tOPT_CALLBACK_F(0, \"diff-filter\", options, N_(\"[(A|C|D|M|R|T|U|X|B)...[*]]\"),\n \t\t\t       N_(\"select files by diff type\"),\n \t\t\t       PARSE_OPT_NONEG, diff_opt_diff_filter),\n-\t\t{ OPTION_CALLBACK, 0, \"output\", options, N_(\"<file>\"),\n-\t\t  N_(\"output to a specific file\"),\n-\t\t  PARSE_OPT_NONEG, NULL, 0, diff_opt_output },\n-\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"output\",\n+\t\t\t.value = options,\n+\t\t\t.argh = N_(\"<file>\"),\n+\t\t\t.help = N_(\"output to a specific file\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t\t.ll_callback = diff_opt_output,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/ref-filter.h b/ref-filter.h\nindex 013d4cfa64b..c98c4fbd4c1 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -114,11 +114,16 @@ struct ref_format {\n }\n \n /*  Macros for checking --merged and --no-merged options */\n-#define _OPT_MERGED_NO_MERGED(option, filter, h) \\\n-\t{ OPTION_CALLBACK, 0, option, (filter), N_(\"commit\"), (h), \\\n-\t  PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n-\t  parse_opt_merge_filter, (intptr_t) \"HEAD\" \\\n-\t}\n+#define _OPT_MERGED_NO_MERGED(option, filter, h) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.long_name = option, \\\n+\t.value = (filter), \\\n+\t.argh = N_(\"commit\"), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n+\t.callback = parse_opt_merge_filter, \\\n+\t.defval = (intptr_t) \"HEAD\", \\\n+}\n #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"merged\", f, h)\n #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"no-merged\", f, h)\n \ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex bfe45ec68b0..997f55fd45b 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv)\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n \t\tOPT_BOOL('D', \"no-doubt\", &boolean, \"begins with 'no-'\"),\n-\t\t{ OPTION_SET_INT, 'B', \"no-fear\", &boolean, NULL,\n-\t\t  \"be brave\", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.short_name = 'B',\n+\t\t\t.long_name = \"no-fear\",\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"be brave\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_COUNTUP('b', \"boolean\", &boolean, \"increment by one\"),\n \t\tOPT_BIT('4', \"or4\", &boolean,\n \t\t\t\"bitwise-or boolean with ...0100\", 4),\n@@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Magic arguments\"),\n \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n \t\t\tnumber_callback),\n-\t\t{ OPTION_COUNTUP, '+', NULL, &boolean, NULL, \"same as -b\",\n-\t\t  PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH },\n-\t\t{ OPTION_COUNTUP, 0, \"ambiguous\", &ambiguous, NULL,\n-\t\t  \"positive ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n-\t\t{ OPTION_COUNTUP, 0, \"no-ambiguous\", &ambiguous, NULL,\n-\t\t  \"negative ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.short_name = '+',\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"same as -b\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"positive ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"no-ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"negative ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_GROUP(\"Standard options\"),\n \t\tOPT__ABBREV(&abbrev),\n \t\tOPT__VERBOSE(&verbose, \"be verbose\"),\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516187","messageId":"20250415-b4-pks-parse-options-integers-v2-2-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","subject":"[PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:06Z","receivedAt":"2025-04-15T12:14:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `OPTION_INTEGER` option type accepts a signed integer. The type of\nthe underlying integer is a simple `int`, which restricts the range of\nvalues accepted by such options. But there is a catch: because the\ncaller provides a pointer to the value via the `.value` field, which is\na simple void pointer. This has two consequences:\n\n  - There is no check whether the passed value is sufficiently long to\n    store the entire range of `int`. This can lead to integer wraparound\n    in the best case and out-of-bounds writes in the worst case.\n\n  - Even when a caller knows that they want to store a value larger than\n    `INT_MAX` they don't have a way to do so.\n\nFunny enough, even if the caller gets everything correct the parsing\nlogic is still insufficient because we use `strtol()` to parse the\nargument, which returns a `long`. But as that value is implicitly cast\nwhen assigning it to the `int` field we may still get invalid results.\n\nIn practice this doesn't tend to be a huge issue because users typically\ndon't end up passing huge values to most commands. But the parsing logic\nis demonstrably broken, and it is too easy to get the calling convention\nwrong.\n\nImprove the situation by introducing a new `precision` field into the\nstructure. This field gets assigned automatically by `OPT_INTEGER_F()`\nand tracks the size of the passed value. Like this it becomes possible\nfor the caller to pass arbitrarily-sized integers and the underlying\nlogic knows to handle it correctly by doing range checks. Furthermore,\nconvert the code to use `strtoimax()` intstead of `strtol()` so that we\ncan also parse values larger than `LONG_MAX`.\n\nNote that we do not yet assert signedness of the passed variable, which\nis another source of bugs. This will be handled in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fmt-merge-msg.c       |  2 ++\n builtin/merge.c               |  1 +\n builtin/show-branch.c         |  1 +\n builtin/tag.c                 |  1 +\n parse-options.c               | 53 +++++++++++++++++++++++++++++++------------\n parse-options.h               |  6 +++++\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 23 ++++++++++++++++++-\n 8 files changed, 75 insertions(+), 15 deletions(-)\n\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 240cdb474bc..3b6aac2cf7f 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"log\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\n@@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"summary\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"alias for --log (deprecated)\"),\n \t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 21787d45165..9ab10c7db0a 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = {\n \t\t.type = OPTION_INTEGER,\n \t\t.long_name = \"log\",\n \t\t.value = &shortlog_len,\n+\t\t.precision = sizeof(shortlog_len),\n \t\t.argh = N_(\"n\"),\n \t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n \t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex dab37019d29..b549d8c3f5b 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -671,6 +671,7 @@ int cmd_show_branch(int ac,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"more\",\n \t\t\t.value = &extra,\n+\t\t\t.precision = sizeof(extra),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex b266f12bb48..7597d93c71b 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -483,6 +483,7 @@ int cmd_tag(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.short_name = 'n',\n \t\t\t.value = &filter.lines,\n+\t\t\t.precision = sizeof(filter.lines),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"print <n> lines of each tag message\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/parse-options.c b/parse-options.c\nindex 35fbb3b0d63..6ddebc20516 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -172,25 +172,50 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\treturn (*opt->ll_callback)(p, opt, p_arg, p_unset);\n \t}\n \tcase OPTION_INTEGER:\n+\t{\n+\t\tintmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision);\n+\t\tintmax_t lower_bound = -upper_bound - 1;\n+\t\tintmax_t value;\n+\n \t\tif (unset) {\n-\t\t\t*(int *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(int *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!*arg)\n+\t\t} else if (!*arg) {\n \t\t\treturn error(_(\"%s expects a numerical value\"),\n \t\t\t\t     optname(opt, flags));\n-\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n-\t\tif (*s)\n-\t\t\treturn error(_(\"%s expects a numerical value\"),\n-\t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\t} else {\n+\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\n+\t\t}\n \n+\t\tif (value < lower_bound || value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n+\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(int8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(int16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(int32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(int64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t\tif (unset) {\n \t\t\t*(unsigned long *)opt->value = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 997ffbee805..8d5f9c95f9c 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n  * `value`::\n  *   stores pointers to the values to be filled.\n  *\n+ * `precision`::\n+ *   precision of the integer pointed to by `value`. Should typically be its\n+ *   `sizeof()`.\n+ *\n  * `argh`::\n  *   token to explain the kind of argument this option wants. Does not\n  *   begin in capital letter, and does not end with a full stop.\n@@ -151,6 +155,7 @@ struct option {\n \tint short_name;\n \tconst char *long_name;\n \tvoid *value;\n+\tsize_t precision;\n \tconst char *argh;\n \tconst char *help;\n \n@@ -214,6 +219,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = (f), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 997f55fd45b..b1275dfade4 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tint16_t i16 = 0;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n@@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_NEGBIT(0, \"neg-or4\", &boolean, \"same as --no-or4\", 4),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n+\t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n@@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t}\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n+\tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 2fe3522305f..e3ca7a27738 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -22,6 +22,7 @@ usage: test-tool parse-options <options>\n \n     -i, --[no-]integer <n>\n                           get a integer\n+    --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --[no-]set23          set integer to 23\n@@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 0\n magnitude: 16384\n timestamp: 0\n string: 123\n@@ -156,6 +158,7 @@ test_expect_success 'short options' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 9000\n magnitude: 16384\n timestamp: 0\n string: 321\n@@ -167,7 +170,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n \t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tcat >expect <<-EOF &&\n \tboolean: 0\n \tinteger: 0\n+\ti16: 0\n \tmagnitude: 0\n \ttimestamp: 0\n \tstring: (not set)\n@@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 13\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: 123\n@@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 2\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -343,6 +349,7 @@ cat >expect <<\\EOF\n Callback: \"four\", 0\n boolean: 5\n integer: 4\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 23\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 0\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' '\n \ttest_must_be_empty out\n '\n \n+test_expect_success 'i16 limits range' '\n+\ttest-tool parse-options --i16 32767 >out &&\n+\ttest_grep \"i16: 32767\" out &&\n+\ttest_must_fail test-tool parse-options --i16 32768 2>err &&\n+\ttest_grep \"value 32768 for option .i16. not in range \\[-32768,32767\\]\" err &&\n+\n+\ttest-tool parse-options --i16 -32768 >out &&\n+\ttest_grep \"i16: -32768\" out &&\n+\ttest_must_fail test-tool parse-options --i16 -32769 2>err &&\n+\ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516188","messageId":"20250415-b4-pks-parse-options-integers-v2-3-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","subject":"[PATCH v2 3/5] parse-options: introduce precision handling for `OPTION_MAGNITUDE`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:07Z","receivedAt":"2025-04-15T12:14:18Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit is the equivalent to the preceding commit, but instead of\nintroducing precision handling for `OPTION_INTEGER` we introduce it for\n`OPTION_MAGNITUDE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 43 +++++++++++++++++++++++++++++++++----------\n parse-options.h               |  1 +\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 18 +++++++++++++++++-\n 4 files changed, 54 insertions(+), 11 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 6ddebc20516..ae836c384c7 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -217,21 +217,44 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t}\n \t}\n \tcase OPTION_MAGNITUDE:\n+\t{\n+\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tunsigned long value;\n+\n \t\tif (unset) {\n-\t\t\t*(unsigned long *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(unsigned long *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!git_parse_ulong(arg, opt->value))\n+\t\t} else if (!git_parse_ulong(arg, &value)) {\n \t\t\treturn error(_(\"%s expects a non-negative integer value\"\n \t\t\t\t       \" with an optional k/m/g suffix\"),\n \t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n+\t\t\t\t     (uintmax_t) value, optname(opt, flags), upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(uint8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(uint16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(uint32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(uint64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \n \tdefault:\n \t\tBUG(\"opt->type %d should not happen\", opt->type);\ndiff --git a/parse-options.h b/parse-options.h\nindex 8d5f9c95f9c..4b561679581 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -281,6 +281,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = PARSE_OPT_NONEG, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex b1275dfade4..46deb4317ef 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tuint16_t m16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n+\t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n \t\tOPT_CMDMODE(0, \"mode1\", &integer, \"set integer to 1 (cmdmode option)\", 1),\n \t\tOPT_CMDMODE(0, \"mode2\", &integer, \"set integer to 2 (cmdmode option)\", 2),\n@@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n+\tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\n \tshow(&expect, &ret, \"abbrev: %d\", abbrev);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex e3ca7a27738..5f503b26cc8 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -25,6 +25,7 @@ usage: test-tool parse-options <options>\n     --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n+    --m16 <n>             get a 16 bit magnitude\n     --[no-]set23          set integer to 23\n     --mode1               set integer to 1 (cmdmode option)\n     --mode2               set integer to 2 (cmdmode option)\n@@ -139,6 +140,7 @@ boolean: 2\n integer: 1729\n i16: 0\n magnitude: 16384\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -160,6 +162,7 @@ boolean: 2\n integer: 1729\n i16: 9000\n magnitude: 16384\n+m16: 32768\n timestamp: 0\n string: 321\n abbrev: 10\n@@ -171,7 +174,7 @@ EOF\n \n test_expect_success 'long options' '\n \ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n-\t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n+\t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n \ttest_must_be_empty output.err &&\n@@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tinteger: 0\n \ti16: 0\n \tmagnitude: 0\n+\tm16: 0\n \ttimestamp: 0\n \tstring: (not set)\n \tabbrev: 100\n@@ -259,6 +263,7 @@ boolean: 1\n integer: 13\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -283,6 +288,7 @@ boolean: 0\n integer: 2\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -351,6 +357,7 @@ boolean: 5\n integer: 4\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -377,6 +384,7 @@ boolean: 1\n integer: 23\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -457,6 +465,7 @@ boolean: 0\n integer: 0\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -804,4 +813,11 @@ test_expect_success 'i16 limits range' '\n \ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n '\n \n+test_expect_success 'm16 limits range' '\n+\ttest-tool parse-options --m16 65535 >out &&\n+\ttest_grep \"m16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --m16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516190","messageId":"20250415-b4-pks-parse-options-integers-v2-4-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","subject":"[PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:08Z","receivedAt":"2025-04-15T12:14:19Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two generic ways to parse integers in the \"parse-options\"\nsubsytem:\n\n  - `OPTION_INTEGER` parses a signed integer.\n\n  - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n    interprets suffixes like \"k\" or \"g\".\n\nNotably missing is a middle ground that parses unsigned integers without\ninterpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\nplug this gap. This option type will be used in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 43 +++++++++++++++++++++++++++++++++++++++++++\n parse-options.h               | 12 ++++++++++++\n t/helper/test-parse-options.c |  4 +++-\n t/t0040-parse-options.sh      | 18 +++++++++++++++++-\n 4 files changed, 75 insertions(+), 2 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex ae836c384c7..9670e46a679 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\t    optname(opt, flags));\n \t\t}\n \t}\n+\tcase OPTION_UNSIGNED:\n+\t{\n+\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tuintmax_t value;\n+\n+\t\tif (unset) {\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n+\t\t\treturn -1;\n+\t\t} else if (!*arg) {\n+\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t     optname(opt, flags));\n+\t\t} else {\n+\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n+\t\t\t\t     value, optname(opt, flags), upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(int8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(int16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(int32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(int64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t{\n \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\ndiff --git a/parse-options.h b/parse-options.h\nindex 4b561679581..20ea7d2ab13 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -25,6 +25,7 @@ enum parse_opt_type {\n \t/* options with arguments (usually) */\n \tOPTION_STRING,\n \tOPTION_INTEGER,\n+\tOPTION_UNSIGNED,\n \tOPTION_MAGNITUDE,\n \tOPTION_CALLBACK,\n \tOPTION_LOWLEVEL_CALLBACK,\n@@ -224,6 +225,16 @@ struct option {\n \t.help = (h), \\\n \t.flags = (f), \\\n }\n+#define OPT_UNSIGNED_F(s, l, v, h, f) { \\\n+\t.type = OPTION_UNSIGNED, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = (f), \\\n+}\n \n #define OPT_END() { \\\n \t.type = OPTION_END, \\\n@@ -276,6 +287,7 @@ struct option {\n #define OPT_CMDMODE(s, l, v, h, i)  OPT_CMDMODE_F(s, l, v, h, i, 0)\n \n #define OPT_INTEGER(s, l, v, h)     OPT_INTEGER_F(s, l, v, h, 0)\n+#define OPT_UNSIGNED(s, l, v, h)    OPT_UNSIGNED_F(s, l, v, h, 0)\n #define OPT_MAGNITUDE(s, l, v, h) { \\\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 46deb4317ef..0d559288d9c 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n-\tuint16_t m16 = 0;\n+\tuint16_t m16 = 0, u16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n+\t\tOPT_UNSIGNED(0, \"u16\", &u16, \"get a 16 bit unsigned integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n@@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n+\tshow(&expect, &ret, \"u16: %\"PRIuMAX, (uintmax_t) u16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 5f503b26cc8..9946e69f586 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -23,6 +23,7 @@ usage: test-tool parse-options <options>\n     -i, --[no-]integer <n>\n                           get a integer\n     --[no-]i16 <n>        get a 16 bit integer\n+    --[no-]u16 <n>        get a 16 bit unsigned integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --m16 <n>             get a 16 bit magnitude\n@@ -139,6 +140,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 0\n+u16: 0\n magnitude: 16384\n m16: 0\n timestamp: 0\n@@ -161,6 +163,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 9000\n+u16: 5432\n magnitude: 16384\n m16: 32768\n timestamp: 0\n@@ -173,7 +176,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \\\n \t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tboolean: 0\n \tinteger: 0\n \ti16: 0\n+\tu16: 0\n \tmagnitude: 0\n \tm16: 0\n \ttimestamp: 0\n@@ -262,6 +266,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 13\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -287,6 +292,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 2\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -356,6 +362,7 @@ Callback: \"four\", 0\n boolean: 5\n integer: 4\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -383,6 +390,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 23\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -464,6 +472,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 0\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' '\n \ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n '\n \n+test_expect_success 'u16 limits range' '\n+\ttest-tool parse-options --u16 65535 >out &&\n+\ttest_grep \"u16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --u16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .u16. exceeds 65535\" err\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516191","messageId":"20250415-b4-pks-parse-options-integers-v2-5-ce07441a1f01@pks.im","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im","subject":"[PATCH v2 5/5] parse-options: detect mismatches in integer signedness","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-15T12:14:09Z","receivedAt":"2025-04-15T12:14:20Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It was reported that \"t5620-backfill.sh\" fails on s390x and sparc64 in a\ntest that exercises the \"--min-batch-size\" command line option. The\nsymptom was that the option didn't seem to have an effect: we didn't\nfetch objects with a batch size of 20, but instead fetched all objects\nat once.\n\nAs it turns out, the root cause is that `--min-batch-size` uses\n`OPT_INTEGER()` to parse the command line option. While this macro\nexpects the caller to pass a pointer to an integer, we instead pass a\npointer to a `size_t`. This coincidentally works on most platforms, but\nit breaks apart on the mentioned platforms because they are big endian.\n\nThis issue isn't specific to git-backfill(1): there are a couple of\nother places where we have the same type confusion going on. This\nindicates that the issue really is the interface that the parse-options\nsubsystem provides -- it is simply too easy to get this wrong as there\nisn't any kind of compiler warning, and things just work on the most\ncommon systems.\n\nAddress the systemic issue by introducing two new build asserts\n`BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names\nalready hint at, those macros will cause a compiler error when passed a\nvalue that is not signed or unsigned, respectively.\n\nAdapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to\nuse those asserts. This uncovers a small set of sites where we indeed\nhave the same bug as in git-backfill(1). Adapt all of them to use the\ncorrect option.\n\nReported-by: Todd Zullinger <tmz@pobox.com>\nReported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>\nHelped-by: SZEDER Gábor <szeder.dev@gmail.com>\nHelped-by: Jeff King <peff@peff.net>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n apply.c            | 4 ++--\n builtin/backfill.c | 4 ++--\n builtin/column.c   | 2 +-\n builtin/grep.c     | 4 ++--\n git-compat-util.h  | 7 +++++++\n parse-options.h    | 6 +++---\n 6 files changed, 17 insertions(+), 10 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex f274a379487..a850c7d75fe 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv,\n \t\t/* Think twice before adding \"--nul\" synonym to this */\n \t\tOPT_SET_INT('z', NULL, &state->line_termination,\n \t\t\tN_(\"paths are separated with NUL character\"), '\\0'),\n-\t\tOPT_INTEGER('C', NULL, &state->p_context,\n-\t\t\t\tN_(\"ensure at least <n> lines of context match\")),\n+\t\tOPT_UNSIGNED('C', NULL, &state->p_context,\n+\t\t\t     N_(\"ensure at least <n> lines of context match\")),\n \t\tOPT_CALLBACK(0, \"whitespace\", state, N_(\"action\"),\n \t\t\tN_(\"detect new or modified lines that have whitespace errors\"),\n \t\t\tapply_option_parse_whitespace),\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 33e1ea2f84f..d95d7a2d4d6 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n \t\t.sparse = 0,\n \t};\n \tstruct option options[] = {\n-\t\tOPT_INTEGER(0, \"min-batch-size\", &ctx.min_batch_size,\n-\t\t\t    N_(\"Minimum number of objects to request at a time\")),\n+\t\tOPT_UNSIGNED(0, \"min-batch-size\", &ctx.min_batch_size,\n+\t\t\t     N_(\"Minimum number of objects to request at a time\")),\n \t\tOPT_BOOL(0, \"sparse\", &ctx.sparse,\n \t\t\t N_(\"Restrict the missing objects to the current sparse-checkout\")),\n \t\tOPT_END(),\ndiff --git a/builtin/column.c b/builtin/column.c\nindex 50314cc2559..ce6443d5fac 100644\n--- a/builtin/column.c\n+++ b/builtin/column.c\n@@ -31,7 +31,7 @@ int cmd_column(int argc,\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"command\", &real_command, N_(\"name\"), N_(\"lookup config vars\")),\n \t\tOPT_COLUMN(0, \"mode\", &colopts, N_(\"layout to use\")),\n-\t\tOPT_INTEGER(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n+\t\tOPT_UNSIGNED(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n \t\tOPT_INTEGER(0, \"width\", &copts.width, N_(\"maximum width\")),\n \t\tOPT_STRING(0, \"indent\", &copts.indent, N_(\"string\"), N_(\"padding space on left border\")),\n \t\tOPT_STRING(0, \"nl\", &copts.nl, N_(\"string\"), N_(\"padding space on right border\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex c4869733e1b..f23a6f1dc86 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -983,9 +983,9 @@ int cmd_grep(int argc,\n \t\tOPT_CALLBACK('C', \"context\", &opt, N_(\"n\"),\n \t\t\tN_(\"show <n> context lines before and after matches\"),\n \t\t\tcontext_callback),\n-\t\tOPT_INTEGER('B', \"before-context\", &opt.pre_context,\n+\t\tOPT_UNSIGNED('B', \"before-context\", &opt.pre_context,\n \t\t\tN_(\"show <n> context lines before matches\")),\n-\t\tOPT_INTEGER('A', \"after-context\", &opt.post_context,\n+\t\tOPT_UNSIGNED('A', \"after-context\", &opt.post_context,\n \t\t\tN_(\"show <n> context lines after matches\")),\n \t\tOPT_INTEGER(0, \"threads\", &num_threads,\n \t\t\tN_(\"use <n> worker threads\")),\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex cf733b38acd..1218fcf81a4 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare)\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \\\n \t\t\t\t\t\t\t  __typeof__(*(src))))\n+\n+# define BARF_UNLESS_SIGNED(var)   BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0)\n+# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0)\n #else\n # define BARF_UNLESS_AN_ARRAY(arr) 0\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \\\n \t\t\t\t sizeof(*(dst)) == sizeof(*(src)))\n+\n+# define BARF_UNLESS_SIGNED(var)   0\n+# define BARF_UNLESS_UNSIGNED(var) 0\n #endif\n+\n /*\n  * ARRAY_SIZE - get the number of elements in a visible array\n  * @x: the array whose size you want.\ndiff --git a/parse-options.h b/parse-options.h\nindex 20ea7d2ab13..7b7c9d901cb 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -219,7 +219,7 @@ struct option {\n \t.type = OPTION_INTEGER, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_SIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -229,7 +229,7 @@ struct option {\n \t.type = OPTION_UNSIGNED, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -292,7 +292,7 @@ struct option {\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516205","messageId":"8e566ea1-5ff4-4854-a1dc-38626510c080@gmail.com","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-2-ce07441a1f01@pks.im","subject":"Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-04-15T15:51:59Z","receivedAt":"2025-04-15T15:52:06Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 15/04/2025 13:14, Patrick Steinhardt wrote:\n> \n> Improve the situation by introducing a new `precision` field into the\n> structure. This field gets assigned automatically by `OPT_INTEGER_F()`\n> and tracks the size of the passed value. Like this it becomes possible\n> for the caller to pass arbitrarily-sized integers and the underlying\n> logic knows to handle it correctly by doing range checks. Furthermore,\n> convert the code to use `strtoimax()` intstead of `strtol()` so that we\n> can also parse values larger than `LONG_MAX`.\n\nNice, this is a really useful improvement. I've left one comment below\n\n> Note that we do not yet assert signedness of the passed variable, which\n> is another source of bugs. This will be handled in a subsequent commit.\n> \n> +\t\t} else {\n> +\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n> +\t\t\tif (*s)\n> +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t\t     optname(opt, flags));\n\nTo catch overflow errors for arguments of intimax_t we need to do\n\n\terrno = 0\n\tvalue = strtoimax(arg, (Char **)&s, 10);\n\tif (errno || *s)\n\t\treturn error(...)\n\nto catch the error when we parse the string as the checks below only \nwork for narrower types.\n\nBest Wishes\n\nPhillip\n\n> +\n> +\t\t}\n>   \n> +\t\tif (value < lower_bound || value > upper_bound)\n> +\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n> +\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n> +\n> +\t\tswitch (opt->precision) {\n> +\t\tcase 1:\n> +\t\t\t*(int8_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 2:\n> +\t\t\t*(int16_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 4:\n> +\t\t\t*(int32_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 8:\n> +\t\t\t*(int64_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tdefault:\n> +\t\t\tBUG(\"invalid precision for option %s\",\n> +\t\t\t    optname(opt, flags));\n> +\t\t}\n> +\t}\n>   \tcase OPTION_MAGNITUDE:\n>   \t\tif (unset) {\n>   \t\t\t*(unsigned long *)opt->value = 0;\n> diff --git a/parse-options.h b/parse-options.h\n> index 997ffbee805..8d5f9c95f9c 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n>    * `value`::\n>    *   stores pointers to the values to be filled.\n>    *\n> + * `precision`::\n> + *   precision of the integer pointed to by `value`. Should typically be its\n> + *   `sizeof()`.\n> + *\n>    * `argh`::\n>    *   token to explain the kind of argument this option wants. Does not\n>    *   begin in capital letter, and does not end with a full stop.\n> @@ -151,6 +155,7 @@ struct option {\n>   \tint short_name;\n>   \tconst char *long_name;\n>   \tvoid *value;\n> +\tsize_t precision;\n>   \tconst char *argh;\n>   \tconst char *help;\n>   \n> @@ -214,6 +219,7 @@ struct option {\n>   \t.short_name = (s), \\\n>   \t.long_name = (l), \\\n>   \t.value = (v), \\\n> +\t.precision = sizeof(*v), \\\n>   \t.argh = N_(\"n\"), \\\n>   \t.help = (h), \\\n>   \t.flags = (f), \\\n> diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\n> index 997f55fd45b..b1275dfade4 100644\n> --- a/t/helper/test-parse-options.c\n> +++ b/t/helper/test-parse-options.c\n> @@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \t};\n>   \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n>   \tstruct string_list list = STRING_LIST_INIT_NODUP;\n> +\tint16_t i16 = 0;\n>   \n>   \tstruct option options[] = {\n>   \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n> @@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \t\tOPT_NEGBIT(0, \"neg-or4\", &boolean, \"same as --no-or4\", 4),\n>   \t\tOPT_GROUP(\"\"),\n>   \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n> +\t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n>   \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n>   \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n>   \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n> @@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \t}\n>   \tshow(&expect, &ret, \"boolean: %d\", boolean);\n>   \tshow(&expect, &ret, \"integer: %d\", integer);\n> +\tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n>   \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n>   \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n>   \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\n> diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\n> index 2fe3522305f..e3ca7a27738 100755\n> --- a/t/t0040-parse-options.sh\n> +++ b/t/t0040-parse-options.sh\n> @@ -22,6 +22,7 @@ usage: test-tool parse-options <options>\n>   \n>       -i, --[no-]integer <n>\n>                             get a integer\n> +    --[no-]i16 <n>        get a 16 bit integer\n>       -j <n>                get a integer, too\n>       -m, --magnitude <n>   get a magnitude\n>       --[no-]set23          set integer to 23\n> @@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' '\n>   cat >expect <<\\EOF\n>   boolean: 2\n>   integer: 1729\n> +i16: 0\n>   magnitude: 16384\n>   timestamp: 0\n>   string: 123\n> @@ -156,6 +158,7 @@ test_expect_success 'short options' '\n>   cat >expect <<\\EOF\n>   boolean: 2\n>   integer: 1729\n> +i16: 9000\n>   magnitude: 16384\n>   timestamp: 0\n>   string: 321\n> @@ -167,7 +170,7 @@ file: prefix/fi.le\n>   EOF\n>   \n>   test_expect_success 'long options' '\n> -\ttest-tool parse-options --boolean --integer 1729 --magnitude 16k \\\n> +\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n>   \t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n>   \t\t--abbrev=10 --file fi.le --obsolete \\\n>   \t\t>output 2>output.err &&\n> @@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n>   \tcat >expect <<-EOF &&\n>   \tboolean: 0\n>   \tinteger: 0\n> +\ti16: 0\n>   \tmagnitude: 0\n>   \ttimestamp: 0\n>   \tstring: (not set)\n> @@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' '\n>   cat >expect <<\\EOF\n>   boolean: 1\n>   integer: 13\n> +i16: 0\n>   magnitude: 0\n>   timestamp: 0\n>   string: 123\n> @@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' '\n>   cat >expect <<\\EOF\n>   boolean: 0\n>   integer: 2\n> +i16: 0\n>   magnitude: 0\n>   timestamp: 0\n>   string: (not set)\n> @@ -343,6 +349,7 @@ cat >expect <<\\EOF\n>   Callback: \"four\", 0\n>   boolean: 5\n>   integer: 4\n> +i16: 0\n>   magnitude: 0\n>   timestamp: 0\n>   string: (not set)\n> @@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' '\n>   cat >expect <<\\EOF\n>   boolean: 1\n>   integer: 23\n> +i16: 0\n>   magnitude: 0\n>   timestamp: 0\n>   string: (not set)\n> @@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' '\n>   cat >expect <<\\EOF\n>   boolean: 0\n>   integer: 0\n> +i16: 0\n>   magnitude: 0\n>   timestamp: 0\n>   string: (not set)\n> @@ -783,4 +792,16 @@ test_expect_success 'magnitude with units but no numbers' '\n>   \ttest_must_be_empty out\n>   '\n>   \n> +test_expect_success 'i16 limits range' '\n> +\ttest-tool parse-options --i16 32767 >out &&\n> +\ttest_grep \"i16: 32767\" out &&\n> +\ttest_must_fail test-tool parse-options --i16 32768 2>err &&\n> +\ttest_grep \"value 32768 for option .i16. not in range \\[-32768,32767\\]\" err &&\n> +\n> +\ttest-tool parse-options --i16 -32768 >out &&\n> +\ttest_grep \"i16: -32768\" out &&\n> +\ttest_must_fail test-tool parse-options --i16 -32769 2>err &&\n> +\ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n> +'\n> +\n>   test_done\n> \n\n"},{"id":"516206","messageId":"35300426-32cc-4c0e-b0c3-edb2b2ed312f@gmail.com","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-4-ce07441a1f01@pks.im","subject":"Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Phillip Wood","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-04-15T15:52:02Z","receivedAt":"2025-04-15T15:52:09Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"Hi Patrick\n\nOn 15/04/2025 13:14, Patrick Steinhardt wrote:\n> We have two generic ways to parse integers in the \"parse-options\"\n> subsytem:\n> \n>    - `OPTION_INTEGER` parses a signed integer.\n> \n>    - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n>      interprets suffixes like \"k\" or \"g\".\n> \n> Notably missing is a middle ground that parses unsigned integers without\n> interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\n> plug this gap. This option type will be used in subsequent commits.\n\nI think this is a useful addition. I wonder about the way it handles \nnegative values though. For types narrower than uintmax_t \"-1\" will be \nrejected but large negative values that parse as small positive numbers \nwill be accepted. Perhaps we should explicitly reject strings starting \nwith \"-\" as we do in git_parse_ulong() which is used by \nOPTION_MAGNITUDE. This patch also needs the fix from patch 2 to detect \noverflows for uintmax_t.\n\nBest Wishes\n\nPhillip\n\n\n[1] \nhttps://lore.kernel.org/git/NYMqsJ7uttDzFT2OOEg5LLsxCSoQhTzqBs16KrMHGEKC7LzOAiYnYTEZavRQWqGH41UgjdwScwer7MssNzI7AEDHnD8GTBWvoBIqJ2e7D6g=@proton.me/\n\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>   parse-options.c               | 43 +++++++++++++++++++++++++++++++++++++++++++\n>   parse-options.h               | 12 ++++++++++++\n>   t/helper/test-parse-options.c |  4 +++-\n>   t/t0040-parse-options.sh      | 18 +++++++++++++++++-\n>   4 files changed, 75 insertions(+), 2 deletions(-)\n> \n> diff --git a/parse-options.c b/parse-options.c\n> index ae836c384c7..9670e46a679 100644\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n>   \t\t\t    optname(opt, flags));\n>   \t\t}\n>   \t}\n> +\tcase OPTION_UNSIGNED:\n> +\t{\n> +\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n> +\t\tuintmax_t value;\n> +\n> +\t\tif (unset) {\n> +\t\t\tvalue = 0;\n> +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> +\t\t\tvalue = opt->defval;\n> +\t\t} else if (get_arg(p, opt, flags, &arg)) {\n> +\t\t\treturn -1;\n> +\t\t} else if (!*arg) {\n> +\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t     optname(opt, flags));\n> +\t\t} else {\n> +\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n> +\t\t\tif (*s)\n> +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t\t     optname(opt, flags));\n> +\t\t}\n> +\n> +\t\tif (value > upper_bound)\n> +\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n> +\t\t\t\t     value, optname(opt, flags), upper_bound);\n> +\n> +\t\tswitch (opt->precision) {\n> +\t\tcase 1:\n> +\t\t\t*(int8_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 2:\n> +\t\t\t*(int16_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 4:\n> +\t\t\t*(int32_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 8:\n> +\t\t\t*(int64_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tdefault:\n> +\t\t\tBUG(\"invalid precision for option %s\",\n> +\t\t\t    optname(opt, flags));\n> +\t\t}\n> +\t}\n>   \tcase OPTION_MAGNITUDE:\n>   \t{\n>   \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n> diff --git a/parse-options.h b/parse-options.h\n> index 4b561679581..20ea7d2ab13 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -25,6 +25,7 @@ enum parse_opt_type {\n>   \t/* options with arguments (usually) */\n>   \tOPTION_STRING,\n>   \tOPTION_INTEGER,\n> +\tOPTION_UNSIGNED,\n>   \tOPTION_MAGNITUDE,\n>   \tOPTION_CALLBACK,\n>   \tOPTION_LOWLEVEL_CALLBACK,\n> @@ -224,6 +225,16 @@ struct option {\n>   \t.help = (h), \\\n>   \t.flags = (f), \\\n>   }\n> +#define OPT_UNSIGNED_F(s, l, v, h, f) { \\\n> +\t.type = OPTION_UNSIGNED, \\\n> +\t.short_name = (s), \\\n> +\t.long_name = (l), \\\n> +\t.value = (v), \\\n> +\t.precision = sizeof(*v), \\\n> +\t.argh = N_(\"n\"), \\\n> +\t.help = (h), \\\n> +\t.flags = (f), \\\n> +}\n>   \n>   #define OPT_END() { \\\n>   \t.type = OPTION_END, \\\n> @@ -276,6 +287,7 @@ struct option {\n>   #define OPT_CMDMODE(s, l, v, h, i)  OPT_CMDMODE_F(s, l, v, h, i, 0)\n>   \n>   #define OPT_INTEGER(s, l, v, h)     OPT_INTEGER_F(s, l, v, h, 0)\n> +#define OPT_UNSIGNED(s, l, v, h)    OPT_UNSIGNED_F(s, l, v, h, 0)\n>   #define OPT_MAGNITUDE(s, l, v, h) { \\\n>   \t.type = OPTION_MAGNITUDE, \\\n>   \t.short_name = (s), \\\n> diff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\n> index 46deb4317ef..0d559288d9c 100644\n> --- a/t/helper/test-parse-options.c\n> +++ b/t/helper/test-parse-options.c\n> @@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \t};\n>   \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n>   \tstruct string_list list = STRING_LIST_INIT_NODUP;\n> -\tuint16_t m16 = 0;\n> +\tuint16_t m16 = 0, u16 = 0;\n>   \tint16_t i16 = 0;\n>   \n>   \tstruct option options[] = {\n> @@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \t\tOPT_GROUP(\"\"),\n>   \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n>   \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n> +\t\tOPT_UNSIGNED(0, \"u16\", &u16, \"get a 16 bit unsigned integer\"),\n>   \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n>   \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n>   \t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n> @@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n>   \tshow(&expect, &ret, \"boolean: %d\", boolean);\n>   \tshow(&expect, &ret, \"integer: %d\", integer);\n>   \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n> +\tshow(&expect, &ret, \"u16: %\"PRIuMAX, (uintmax_t) u16);\n>   \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n>   \tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n>   \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n> diff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\n> index 5f503b26cc8..9946e69f586 100755\n> --- a/t/t0040-parse-options.sh\n> +++ b/t/t0040-parse-options.sh\n> @@ -23,6 +23,7 @@ usage: test-tool parse-options <options>\n>       -i, --[no-]integer <n>\n>                             get a integer\n>       --[no-]i16 <n>        get a 16 bit integer\n> +    --[no-]u16 <n>        get a 16 bit unsigned integer\n>       -j <n>                get a integer, too\n>       -m, --magnitude <n>   get a magnitude\n>       --m16 <n>             get a 16 bit magnitude\n> @@ -139,6 +140,7 @@ cat >expect <<\\EOF\n>   boolean: 2\n>   integer: 1729\n>   i16: 0\n> +u16: 0\n>   magnitude: 16384\n>   m16: 0\n>   timestamp: 0\n> @@ -161,6 +163,7 @@ cat >expect <<\\EOF\n>   boolean: 2\n>   integer: 1729\n>   i16: 9000\n> +u16: 5432\n>   magnitude: 16384\n>   m16: 32768\n>   timestamp: 0\n> @@ -173,7 +176,7 @@ file: prefix/fi.le\n>   EOF\n>   \n>   test_expect_success 'long options' '\n> -\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n> +\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \\\n>   \t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n>   \t\t--abbrev=10 --file fi.le --obsolete \\\n>   \t\t>output 2>output.err &&\n> @@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n>   \tboolean: 0\n>   \tinteger: 0\n>   \ti16: 0\n> +\tu16: 0\n>   \tmagnitude: 0\n>   \tm16: 0\n>   \ttimestamp: 0\n> @@ -262,6 +266,7 @@ cat >expect <<\\EOF\n>   boolean: 1\n>   integer: 13\n>   i16: 0\n> +u16: 0\n>   magnitude: 0\n>   m16: 0\n>   timestamp: 0\n> @@ -287,6 +292,7 @@ cat >expect <<\\EOF\n>   boolean: 0\n>   integer: 2\n>   i16: 0\n> +u16: 0\n>   magnitude: 0\n>   m16: 0\n>   timestamp: 0\n> @@ -356,6 +362,7 @@ Callback: \"four\", 0\n>   boolean: 5\n>   integer: 4\n>   i16: 0\n> +u16: 0\n>   magnitude: 0\n>   m16: 0\n>   timestamp: 0\n> @@ -383,6 +390,7 @@ cat >expect <<\\EOF\n>   boolean: 1\n>   integer: 23\n>   i16: 0\n> +u16: 0\n>   magnitude: 0\n>   m16: 0\n>   timestamp: 0\n> @@ -464,6 +472,7 @@ cat >expect <<\\EOF\n>   boolean: 0\n>   integer: 0\n>   i16: 0\n> +u16: 0\n>   magnitude: 0\n>   m16: 0\n>   timestamp: 0\n> @@ -820,4 +829,11 @@ test_expect_success 'm16 limits range' '\n>   \ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n>   '\n>   \n> +test_expect_success 'u16 limits range' '\n> +\ttest-tool parse-options --u16 65535 >out &&\n> +\ttest_grep \"u16: 65535\" out &&\n> +\ttest_must_fail test-tool parse-options --u16 65536 2>err &&\n> +\ttest_grep \"value 65536 for option .u16. exceeds 65535\" err\n> +'\n> +\n>   test_done\n> \n\n"},{"id":"516210","messageId":"xmqqecxtmkdb.fsf@gitster.g","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-2-ce07441a1f01@pks.im","subject":"Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-15T16:59:12Z","receivedAt":"2025-04-15T16:59:15Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> diff --git a/parse-options.h b/parse-options.h\n> index 997ffbee805..8d5f9c95f9c 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n>   * `value`::\n>   *   stores pointers to the values to be filled.\n>   *\n> + * `precision`::\n> + *   precision of the integer pointed to by `value`. Should typically be its\n> + *   `sizeof()`.\n\nThe fact of the integer allowing to store up to 16-bit vs 32-bit, is\nthat \"precision\"?  \"My --size option runs up to 200,000, what value\nshould I set it to?\" is a natural question the readers of this\nsentence would have in their mind, if we call it \"range\" or\nsomething (which might not be a bad thing to have, but that is\ntotally outside the theme of this topic).\n\nIn any case, include a phrase \"number of bytes\" somewhere in the\ndescription to make it clear what unit we are counting.\n\nAre their common use case where this number is *not* its sizeof()\nalready in the codebase?\n\n"},{"id":"516211","messageId":"xmqqa58hmk8f.fsf@gitster.g","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-5-ce07441a1f01@pks.im","subject":"Re: [PATCH v2 5/5] parse-options: detect mismatches in integer signedness","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-15T17:02:08Z","receivedAt":"2025-04-15T17:02:11Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> Address the systemic issue by introducing two new build asserts\n> `BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names\n> already hint at, those macros will cause a compiler error when passed a\n> value that is not signed or unsigned, respectively.\n>\n> Adapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to\n> use those asserts. This uncovers a small set of sites where we indeed\n> have the same bug as in git-backfill(1). Adapt all of them to use the\n> correct option.\n\nNicely described and implemented.\n\nThanks.\n"},{"id":"516213","messageId":"94f4ba9a-81a9-4e3a-932b-faee5aa2d2f4@web.de","threadId":"63234","inReplyTo":"20250415-b4-pks-parse-options-integers-v2-4-ce07441a1f01@pks.im","subject":"Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"René Scharfe","fromEmail":"l.s.r@web.de","sentAt":"2025-04-15T17:38:04Z","receivedAt":"2025-04-15T17:38:15Z","isPatch":true,"sender":{"key":"l.s.r@web.de","avatar":"https://avatars.githubusercontent.com/u/26122331?v=4"},"body":"Am 15.04.25 um 14:14 schrieb Patrick Steinhardt:\n> We have two generic ways to parse integers in the \"parse-options\"\n> subsytem:\n\n\"subsystem\"\n\n>   - `OPTION_INTEGER` parses a signed integer.\n> \n>   - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n>     interprets suffixes like \"k\" or \"g\".\n> \n> Notably missing is a middle ground that parses unsigned integers without\n> interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\n> plug this gap. This option type will be used in subsequent commits.\n> \n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  parse-options.c               | 43 +++++++++++++++++++++++++++++++++++++++++++\n>  parse-options.h               | 12 ++++++++++++\n>  t/helper/test-parse-options.c |  4 +++-\n>  t/t0040-parse-options.sh      | 18 +++++++++++++++++-\n>  4 files changed, 75 insertions(+), 2 deletions(-)\n> \n> diff --git a/parse-options.c b/parse-options.c\n> index ae836c384c7..9670e46a679 100644\n> --- a/parse-options.c\n> +++ b/parse-options.c\n> @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n>  \t\t\t    optname(opt, flags));\n>  \t\t}\n>  \t}\n> +\tcase OPTION_UNSIGNED:\n> +\t{\n> +\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n> +\t\tuintmax_t value;\n> +\n> +\t\tif (unset) {\n> +\t\t\tvalue = 0;\n> +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> +\t\t\tvalue = opt->defval;\n> +\t\t} else if (get_arg(p, opt, flags, &arg)) {\n> +\t\t\treturn -1;\n> +\t\t} else if (!*arg) {\n> +\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t     optname(opt, flags));\n> +\t\t} else {\n> +\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n> +\t\t\tif (*s)\n> +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> +\t\t\t\t\t     optname(opt, flags));\n> +\t\t}\n> +\n> +\t\tif (value > upper_bound)\n> +\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n> +\t\t\t\t     value, optname(opt, flags), upper_bound);\n> +\n> +\t\tswitch (opt->precision) {\n> +\t\tcase 1:\n> +\t\t\t*(int8_t *)opt->value = value;\n\nuint8_t, surely.  Similarly for the other casts below.\n\n> +\t\t\treturn 0;\n> +\t\tcase 2:\n> +\t\t\t*(int16_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 4:\n> +\t\t\t*(int32_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 8:\n> +\t\t\t*(int64_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tdefault:\n> +\t\t\tBUG(\"invalid precision for option %s\",\n> +\t\t\t    optname(opt, flags));\n> +\t\t}\n> +\t}\n>  \tcase OPTION_MAGNITUDE:\n>  \t{\n>  \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n"},{"id":"516264","messageId":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im","subject":"[PATCH v3 0/7] parse-options: harden handling of integer values","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:09Z","receivedAt":"2025-04-16T10:02:27Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"Hi,\n\nthis patch series addresses the issues raised in [1] and [2]. As\ndiscussed in [1], the series also introduces a couple of safeguards to\nmake it harder to misuse `OPT_INTEGER()` and `OPT_MAGNITUDE()`:\n\n  - We now track the precision of the underlying integer types. This\n    makes it possible to pass arbitrarily-sized integers to those\n    options, not only `int` and `unsigned long`, respectively.\n\n  - We introduce a build assert to verify that the passed variable has\n    correct signedness.\n\nFurthermore, the series introduces `OPT_UNSIGNED()` to adapt all\ncallsites that previously used variables with the wrong signedness.\n\nChanges in v2:\n  - Adapt computation of upper bounds to use similar logic to\n    `maximum_signed_value_of_type()`.\n  - Link to v1: https://lore.kernel.org/r/20250401-b4-pks-parse-options-integers-v1-0-a628ad40c3b4@pks.im\n\nChanges in v3:\n  - Introduce `errno` checks for `strto{u,i}max()`.\n  - Note that the precision is in bytes.\n  - Reject leading '-' when parsing unsigned integers.\n  - Introduce bounded integer options. This patch is mostly a proof of\n    concept that demonstrates that precision and ranges are orthogonal\n    to one another, so I consider it to be an optional patch. It may be\n    useful in the future, but I haven't converted any callsites to use\n    bounds yet.\n  - Link to v2: https://lore.kernel.org/r/20250415-b4-pks-parse-options-integers-v2-0-ce07441a1f01@pks.im\n\nThanks!\n\nPatrick\n\n[1]: <89257ab82cd60d135cce02d51eacee7ec35c1c37.camel@physik.fu-berlin.de>\n[2]: <Z8HW6petWuMRWSXf@teonanacatl.net>\n\n---\nPatrick Steinhardt (7):\n      global: use designated initializers for options\n      parse-options: check for overflow when parsing integers\n      parse-options: introduce precision handling for `OPTION_INTEGER`\n      parse-options: introduce precision handling for `OPTION_MAGNITUDE`\n      parse-options: introduce `OPTION_UNSIGNED`\n      parse-options: detect mismatches in integer signedness\n      parse-options: introduce bounded integer options\n\n apply.c                       |   4 +-\n archive.c                     |  35 ++++++---\n builtin/am.c                  |  28 +++++--\n builtin/backfill.c            |   4 +-\n builtin/clone.c               |  13 +++-\n builtin/column.c              |   2 +-\n builtin/commit-tree.c         |  12 ++-\n builtin/commit.c              |  62 +++++++++++----\n builtin/config.c              |  13 +++-\n builtin/describe.c            |  24 ++++--\n builtin/fetch.c               |  10 ++-\n builtin/fmt-merge-msg.c       |  27 +++++--\n builtin/gc.c                  |  12 ++-\n builtin/grep.c                |  18 +++--\n builtin/init-db.c             |  13 +++-\n builtin/ls-remote.c           |  11 ++-\n builtin/merge.c               |  38 +++++++--\n builtin/read-tree.c           |  11 ++-\n builtin/rebase.c              |  25 ++++--\n builtin/revert.c              |  12 ++-\n builtin/show-branch.c         |  13 +++-\n builtin/tag.c                 |  24 ++++--\n builtin/update-index.c        | 131 +++++++++++++++++++++----------\n builtin/write-tree.c          |  12 ++-\n diff.c                        |  13 +++-\n git-compat-util.h             |   7 ++\n parse-options.c               | 176 +++++++++++++++++++++++++++++++++++++-----\n parse-options.h               |  75 +++++++++++++++++-\n ref-filter.h                  |  15 ++--\n t/helper/test-parse-options.c |  51 ++++++++++--\n t/t0040-parse-options.sh      | 102 +++++++++++++++++++++++-\n 31 files changed, 805 insertions(+), 188 deletions(-)\n\nRange-diff versus v2:\n\n1:  80c8b943e9b = 1:  3dc362ae91b global: use designated initializers for options\n-:  ----------- > 2:  a5a71ad6da8 parse-options: check for overflow when parsing integers\n2:  307eaeb463c ! 3:  1ad133c639e parse-options: introduce precision handling for `OPTION_INTEGER`\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n     +\t\t} else if (!*arg) {\n      \t\t\treturn error(_(\"%s expects a numerical value\"),\n      \t\t\t\t     optname(opt, flags));\n    --\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n    --\t\tif (*s)\n    --\t\t\treturn error(_(\"%s expects a numerical value\"),\n    --\t\t\t\t     optname(opt, flags));\n    --\t\treturn 0;\n     +\t\t} else {\n    ++\t\t\terrno = 0;\n     +\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n     +\t\t\tif (*s)\n     +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n     +\t\t\t\t\t     optname(opt, flags));\n    -+\n    ++\t\t\tif (errno == ERANGE)\n    ++\t\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n    ++\t\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n    ++\t\t\tif (errno)\n    ++\t\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n    ++\t\t\t\t\t\t   arg, optname(opt, flags));\n     +\t\t}\n      \n    +-\t\terrno = 0;\n    +-\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n    +-\t\tif (*s)\n    +-\t\t\treturn error(_(\"%s expects a numerical value\"),\n    +-\t\t\t\t     optname(opt, flags));\n    +-\t\tif (errno == ERANGE)\n     +\t\tif (value < lower_bound || value > upper_bound)\n    -+\t\t\treturn error(_(\"value %\"PRIdMAX\" for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n    -+\t\t\t\t     value, optname(opt, flags), lower_bound, upper_bound);\n    -+\n    + \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n    +-\t\t\t\t     arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX);\n    +-\t\tif (errno)\n    +-\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n    +-\t\t\t\t\t   arg, optname(opt, flags));\n    ++\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n    + \n    +-\t\treturn 0;\n     +\t\tswitch (opt->precision) {\n     +\t\tcase 1:\n     +\t\t\t*(int8_t *)opt->value = value;\n    @@ parse-options.h: typedef int parse_opt_subcommand_fn(int argc, const char **argv\n       *   stores pointers to the values to be filled.\n       *\n     + * `precision`::\n    -+ *   precision of the integer pointed to by `value`. Should typically be its\n    -+ *   `sizeof()`.\n    ++ *   precision of the integer pointed to by `value` in number of bytes. Should\n    ++ *   typically be its `sizeof()`.\n     + *\n       * `argh`::\n       *   token to explain the kind of argument this option wants. Does not\n    @@ t/t0040-parse-options.sh: test_expect_success 'OPT_NUMBER_CALLBACK() works' '\n      magnitude: 0\n      timestamp: 0\n      string: (not set)\n    -@@ t/t0040-parse-options.sh: test_expect_success 'magnitude with units but no numbers' '\n    +@@ t/t0040-parse-options.sh: test_expect_success 'overflowing integer' '\n      \ttest_must_be_empty out\n      '\n      \n3:  5bfb9af2262 ! 4:  01e56b43c77 parse-options: introduce precision handling for `OPTION_MAGNITUDE`\n    @@ Commit message\n     \n      ## parse-options.c ##\n     @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n    + \n    + \t\tif (value < lower_bound || value > upper_bound)\n    + \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n    +-\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n    ++\t\t\t\t     arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound);\n    + \n    + \t\tswitch (opt->precision) {\n    + \t\tcase 1:\n    +@@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n      \t\t}\n      \t}\n      \tcase OPTION_MAGNITUDE:\n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n     +\t\t}\n     +\n     +\t\tif (value > upper_bound)\n    -+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n    -+\t\t\t\t     (uintmax_t) value, optname(opt, flags), upper_bound);\n    ++\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n    ++\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n     +\n     +\t\tswitch (opt->precision) {\n     +\t\tcase 1:\n    @@ t/t0040-parse-options.sh: test_expect_success 'i16 limits range' '\n     +\ttest-tool parse-options --m16 65535 >out &&\n     +\ttest_grep \"m16: 65535\" out &&\n     +\ttest_must_fail test-tool parse-options --m16 65536 2>err &&\n    -+\ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n    ++\ttest_grep \"value 65536 for option .m16. not in range \\[0,65535\\]\" err\n     +'\n     +\n      test_done\n4:  75ff495b897 ! 5:  e7458eadea9 parse-options: introduce `OPTION_UNSIGNED`\n    @@ Commit message\n         parse-options: introduce `OPTION_UNSIGNED`\n     \n         We have two generic ways to parse integers in the \"parse-options\"\n    -    subsytem:\n    +    subsystem:\n     \n           - `OPTION_INTEGER` parses a signed integer.\n     \n    @@ parse-options.c: static enum parse_opt_result do_get_value(struct parse_opt_ctx_\n     +\t\t} else if (!*arg) {\n     +\t\t\treturn error(_(\"%s expects a numerical value\"),\n     +\t\t\t\t     optname(opt, flags));\n    ++\t\t} else if (*arg == '-') {\n    ++\t\t\treturn error(_(\"%s does not accept negative values\"),\n    ++\t\t\t\t     optname(opt, flags));\n     +\t\t} else {\n    ++\t\t\terrno = 0;\n     +\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n     +\t\t\tif (*s)\n     +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n     +\t\t\t\t\t     optname(opt, flags));\n    ++\t\t\tif (errno == ERANGE)\n    ++\t\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n    ++\t\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n    ++\t\t\tif (errno)\n    ++\t\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n    ++\t\t\t\t\t\t   arg, optname(opt, flags));\n    ++\n     +\t\t}\n     +\n     +\t\tif (value > upper_bound)\n    -+\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n    -+\t\t\t\t     value, optname(opt, flags), upper_bound);\n    ++\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n    ++\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n     +\n     +\t\tswitch (opt->precision) {\n     +\t\tcase 1:\n    -+\t\t\t*(int8_t *)opt->value = value;\n    ++\t\t\t*(uint8_t *)opt->value = value;\n     +\t\t\treturn 0;\n     +\t\tcase 2:\n    -+\t\t\t*(int16_t *)opt->value = value;\n    ++\t\t\t*(uint16_t *)opt->value = value;\n     +\t\t\treturn 0;\n     +\t\tcase 4:\n    -+\t\t\t*(int32_t *)opt->value = value;\n    ++\t\t\t*(uint32_t *)opt->value = value;\n     +\t\t\treturn 0;\n     +\t\tcase 8:\n    -+\t\t\t*(int64_t *)opt->value = value;\n    ++\t\t\t*(uint64_t *)opt->value = value;\n     +\t\t\treturn 0;\n     +\t\tdefault:\n     +\t\t\tBUG(\"invalid precision for option %s\",\n    @@ t/t0040-parse-options.sh: cat >expect <<\\EOF\n      m16: 0\n      timestamp: 0\n     @@ t/t0040-parse-options.sh: test_expect_success 'm16 limits range' '\n    - \ttest_grep \"value 65536 for option .m16. exceeds 65535\" err\n    + \ttest_grep \"value 65536 for option .m16. not in range \\[0,65535\\]\" err\n      '\n      \n     +test_expect_success 'u16 limits range' '\n     +\ttest-tool parse-options --u16 65535 >out &&\n     +\ttest_grep \"u16: 65535\" out &&\n     +\ttest_must_fail test-tool parse-options --u16 65536 2>err &&\n    -+\ttest_grep \"value 65536 for option .u16. exceeds 65535\" err\n    ++\ttest_grep \"value 65536 for option .u16. not in range \\[0,65535\\]\" err\n    ++'\n    ++\n    ++test_expect_success 'u16 does not accept negative value' '\n    ++\ttest_must_fail test-tool parse-options --u16 -1 >out 2>err &&\n    ++\ttest_grep \"option .u16. does not accept negative values\" err &&\n    ++\ttest_must_be_empty out\n     +'\n     +\n      test_done\n5:  20ab753baef = 6:  5b05451c346 parse-options: detect mismatches in integer signedness\n-:  ----------- > 7:  7115a7a31aa parse-options: introduce bounded integer options\n\n---\nbase-commit: 5b97a56fa0e7d580dc8865b73107407c9b3f0eff\nchange-id: 20250401-b4-pks-parse-options-integers-9b4bbcf21011\n\n"},{"id":"516265","messageId":"20250416-b4-pks-parse-options-integers-v3-2-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 2/7] parse-options: check for overflow when parsing integers","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:11Z","receivedAt":"2025-04-16T10:02:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We use `strtol()` to parse the argument of `OPTION_INTEGER` options. And\nwhile we do check that the argument was fully parsed, we don't check\n`errno` at all and thus may not notice cases where `strtol()` fails.\nMost importantly, this includes the case where the parsed integer does\nnot fit into a `long` at all. The consequence is that we'll happily\ncontinue with an invalid value.\n\nFix the bug by checking `errno`. Note that this change alone is not\nsufficient to detect all possible overflows: `strtol()` returns a\n`long`, but we end up assigning the value to an `int` and will thus\ntruncate the value. This will be fixed in subsequent patches.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c          | 10 +++++++++-\n t/t0040-parse-options.sh |  6 ++++++\n 2 files changed, 15 insertions(+), 1 deletion(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 35fbb3b0d63..e8c08e55e02 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -185,12 +185,20 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\tif (!*arg)\n \t\t\treturn error(_(\"%s expects a numerical value\"),\n \t\t\t\t     optname(opt, flags));\n+\n+\t\terrno = 0;\n \t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n \t\tif (*s)\n \t\t\treturn error(_(\"%s expects a numerical value\"),\n \t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\tif (errno == ERANGE)\n+\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n+\t\t\t\t     arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX);\n+\t\tif (errno)\n+\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n+\t\t\t\t\t   arg, optname(opt, flags));\n \n+\t\treturn 0;\n \tcase OPTION_MAGNITUDE:\n \t\tif (unset) {\n \t\t\t*(unsigned long *)opt->value = 0;\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 2fe3522305f..5eb1feb61b4 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -783,4 +783,10 @@ test_expect_success 'magnitude with units but no numbers' '\n \ttest_must_be_empty out\n '\n \n+test_expect_success 'overflowing integer' '\n+\ttest_must_fail test-tool parse-options --integer 9223372036854775808 >out 2>err &&\n+\ttest_grep \"value .* for option .* not in range\" err &&\n+\ttest_must_be_empty out\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516266","messageId":"20250416-b4-pks-parse-options-integers-v3-1-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 1/7] global: use designated initializers for options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:10Z","receivedAt":"2025-04-16T10:02:29Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"While we expose macros for most of our different option types understood\nby the \"parse-options\" subsystem, not every combination of fields that\nhas one as that would otherwise quickly lead to an explosion of macros.\nInstead, we just initialize structures manually for those variants of\nfields that don't have a macro.\n\nCallsites that open-code these structure initialization don't use\ndesignated initializers though and instead just provide values for each\nof the fields that they want to initialize. This has three significant\ndownsides:\n\n  - Callsites need to specify all values up to the last field that they\n    care about. This often includes fields that should simply be left at\n    their default zero-initialized state, which adds distraction.\n\n  - Any reader not deeply familiar with the layout of the structure\n    has a hard time figuring out what the respective initializers mean.\n\n  - Reordering or introducing new fields in the middle of the structure\n    is impossible without adapting all callsites.\n\nConvert all sites to instead use designated initializers, which we have\nstarted using in our codebase quite a while ago. This allows us to skip\nany default-initialized fields, gives the reader context by specifying\nthe field names and allows us to reorder or introduce new fields where\nwe want to.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n archive.c                     |  35 ++++++++---\n builtin/am.c                  |  28 ++++++---\n builtin/clone.c               |  13 ++++-\n builtin/commit-tree.c         |  12 +++-\n builtin/commit.c              |  62 +++++++++++++++-----\n builtin/config.c              |  13 ++++-\n builtin/describe.c            |  24 ++++++--\n builtin/fetch.c               |  10 +++-\n builtin/fmt-merge-msg.c       |  25 +++++---\n builtin/gc.c                  |  12 +++-\n builtin/grep.c                |  14 +++--\n builtin/init-db.c             |  13 +++--\n builtin/ls-remote.c           |  11 +++-\n builtin/merge.c               |  37 +++++++++---\n builtin/read-tree.c           |  11 +++-\n builtin/rebase.c              |  25 ++++++--\n builtin/revert.c              |  12 +++-\n builtin/show-branch.c         |  12 +++-\n builtin/tag.c                 |  23 ++++++--\n builtin/update-index.c        | 131 +++++++++++++++++++++++++++++-------------\n builtin/write-tree.c          |  12 ++--\n diff.c                        |  13 +++--\n ref-filter.h                  |  15 +++--\n t/helper/test-parse-options.c |  38 +++++++++---\n 24 files changed, 443 insertions(+), 158 deletions(-)\n\ndiff --git a/archive.c b/archive.c\nindex 8be4e7ac8db..67bba3cd301 100644\n--- a/archive.c\n+++ b/archive.c\n@@ -650,20 +650,37 @@ static int parse_archive_args(int argc, const char **argv,\n \t\tOPT_STRING(0, \"format\", &format, N_(\"fmt\"), N_(\"archive format\")),\n \t\tOPT_STRING(0, \"prefix\", &base, N_(\"prefix\"),\n \t\t\tN_(\"prepend prefix to each pathname in the archive\")),\n-\t\t{ OPTION_CALLBACK, 0, \"add-file\", args, N_(\"file\"),\n-\t\t  N_(\"add untracked file to archive\"), 0, add_file_cb,\n-\t\t  (intptr_t)&base },\n-\t\t{ OPTION_CALLBACK, 0, \"add-virtual-file\", args,\n-\t\t  N_(\"path:content\"), N_(\"add untracked file to archive\"), 0,\n-\t\t  add_file_cb, (intptr_t)&base },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"file\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"add-virtual-file\",\n+\t\t\t.value = args,\n+\t\t\t.argh = N_(\"path:content\"),\n+\t\t\t.help = N_(\"add untracked file to archive\"),\n+\t\t\t.callback = add_file_cb,\n+\t\t\t.defval = (intptr_t) &base,\n+\t\t},\n \t\tOPT_STRING('o', \"output\", &output, N_(\"file\"),\n \t\t\tN_(\"write the archive to this file\")),\n \t\tOPT_BOOL(0, \"worktree-attributes\", &worktree_attributes,\n \t\t\tN_(\"read .gitattributes in working directory\")),\n \t\tOPT__VERBOSE(&verbose, N_(\"report archived files on stderr\")),\n-\t\t{ OPTION_STRING, 0, \"mtime\", &mtime_option, N_(\"time\"),\n-\t\t  N_(\"set modification time of archive entries\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"mtime\",\n+\t\t\t.value = &mtime_option,\n+\t\t\t.argh = N_(\"time\"),\n+\t\t\t.help = N_(\"set modification time of archive entries\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_NUMBER_CALLBACK(&compression_level,\n \t\t\tN_(\"set compression level\"), number_callback),\n \t\tOPT_GROUP(\"\"),\ndiff --git a/builtin/am.c b/builtin/am.c\nindex 3b61bd4c333..4afb519830f 100644\n--- a/builtin/am.c\n+++ b/builtin/am.c\n@@ -2400,11 +2400,16 @@ int cmd_am(int argc,\n \t\tOPT_CMDMODE(0, \"quit\", &resume_mode,\n \t\t\tN_(\"abort the patching operation but keep HEAD where it is\"),\n \t\t\tRESUME_QUIT),\n-\t\t{ OPTION_CALLBACK, 0, \"show-current-patch\", &resume_mode,\n-\t\t  \"(diff|raw)\",\n-\t\t  N_(\"show the patch being applied\"),\n-\t\t  PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t  parse_opt_show_current_patch, RESUME_SHOW_PATCH_RAW },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"show-current-patch\",\n+\t\t\t.value = &resume_mode,\n+\t\t\t.argh = \"(diff|raw)\",\n+\t\t\t.help = N_(\"show the patch being applied\"),\n+\t\t\t.flags = PARSE_OPT_CMDMODE | PARSE_OPT_OPTARG | PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.callback = parse_opt_show_current_patch,\n+\t\t\t.defval = RESUME_SHOW_PATCH_RAW,\n+\t\t},\n \t\tOPT_CMDMODE(0, \"retry\", &resume_mode,\n \t\t\tN_(\"try to apply current patch again\"),\n \t\t\tRESUME_APPLY),\n@@ -2417,9 +2422,16 @@ int cmd_am(int argc,\n \t\tOPT_BOOL(0, \"ignore-date\", &state.ignore_date,\n \t\t\tN_(\"use current timestamp for author date\")),\n \t\tOPT_RERERE_AUTOUPDATE(&state.allow_rerere_autoupdate),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &state.sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG-sign commits\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &state.sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"empty\", &state.empty_type, \"(stop|drop|keep)\",\n \t\t  N_(\"how to handle empty patches\"),\n \t\t  PARSE_OPT_NONEG, am_option_parse_empty),\ndiff --git a/builtin/clone.c b/builtin/clone.c\nindex 88276e5b7ab..9c3547f41e3 100644\n--- a/builtin/clone.c\n+++ b/builtin/clone.c\n@@ -930,9 +930,16 @@ int cmd_clone(int argc,\n \t\t\t N_(\"don't use local hardlinks, always copy\")),\n \t\tOPT_BOOL('s', \"shared\", &option_shared,\n \t\t\t N_(\"setup as shared repository\")),\n-\t\t{ OPTION_CALLBACK, 0, \"recurse-submodules\", &option_recurse_submodules,\n-\t\t  N_(\"pathspec\"), N_(\"initialize submodules in the clone\"),\n-\t\t  PARSE_OPT_OPTARG, recurse_submodules_cb, (intptr_t)\".\" },\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"recurse-submodules\",\n+\t\t\t.value = &option_recurse_submodules,\n+\t\t\t.argh = N_(\"pathspec\"),\n+\t\t\t.help = N_(\"initialize submodules in the clone\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.callback = recurse_submodules_cb,\n+\t\t\t.defval = (intptr_t)\".\",\n+\t\t},\n \t\tOPT_ALIAS(0, \"recursive\", \"recurse-submodules\"),\n \t\tOPT_INTEGER('j', \"jobs\", &max_jobs,\n \t\t\t    N_(\"number of submodules cloned in parallel\")),\ndiff --git a/builtin/commit-tree.c b/builtin/commit-tree.c\nindex 38457600a4e..c787133d004 100644\n--- a/builtin/commit-tree.c\n+++ b/builtin/commit-tree.c\n@@ -111,8 +111,16 @@ int cmd_commit_tree(int argc,\n \t\tOPT_CALLBACK_F('F', NULL, &buffer, N_(\"file\"),\n \t\t\tN_(\"read commit log message from file\"), PARSE_OPT_NONEG,\n \t\t\tparse_file_arg_callback),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t\tN_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tint ret;\ndiff --git a/builtin/commit.c b/builtin/commit.c\nindex 2f459682221..66bd91fd523 100644\n--- a/builtin/commit.c\n+++ b/builtin/commit.c\n@@ -1542,17 +1542,34 @@ struct repository *repo UNUSED)\n \t\t\t    STATUS_FORMAT_LONG),\n \t\tOPT_BOOL('z', \"null\", &s.null_termination,\n \t\t\t N_(\"terminate entries with NUL\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n-\t\t{ OPTION_STRING, 0, \"ignored\", &ignored_arg,\n-\t\t  N_(\"mode\"),\n-\t\t  N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"traditional\" },\n-\t\t{ OPTION_STRING, 0, \"ignore-submodules\", &ignore_submodule_arg, N_(\"when\"),\n-\t\t  N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignored\",\n+\t\t\t.value = &ignored_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show ignored files, optional modes: traditional, matching, no. (Default: traditional)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"traditional\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"ignore-submodules\",\n+\t\t\t.value = &ignore_submodule_arg,\n+\t\t\t.argh = N_(\"when\"),\n+\t\t\t.help = N_(\"ignore changes to submodules, optional when: all, dirty, untracked. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_COLUMN(0, \"column\", &s.colopts, N_(\"list untracked files in columns\")),\n \t\tOPT_BOOL(0, \"no-renames\", &no_renames, N_(\"do not detect renames\")),\n \t\tOPT_CALLBACK_F('M', \"find-renames\", &rename_score_arg,\n@@ -1688,8 +1705,16 @@ int cmd_commit(int argc,\n \t\tOPT_BOOL('e', \"edit\", &edit_flag, N_(\"force edit of commit\")),\n \t\tOPT_CLEANUP(&cleanup_arg),\n \t\tOPT_BOOL(0, \"status\", &include_status, N_(\"include status in commit message template\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &sign_commit,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\t/* end commit message options */\n \n \t\tOPT_GROUP(N_(\"Commit contents options\")),\n@@ -1714,7 +1739,16 @@ int cmd_commit(int argc,\n \t\t\t N_(\"terminate entries with NUL\")),\n \t\tOPT_BOOL(0, \"amend\", &amend, N_(\"amend previous commit\")),\n \t\tOPT_BOOL(0, \"no-post-rewrite\", &no_post_rewrite, N_(\"bypass post-rewrite hook\")),\n-\t\t{ OPTION_STRING, 'u', \"untracked-files\", &untracked_files_arg, N_(\"mode\"), N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"), PARSE_OPT_OPTARG, NULL, (intptr_t)\"all\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'u',\n+\t\t\t.long_name = \"untracked-files\",\n+\t\t\t.value = &untracked_files_arg,\n+\t\t\t.argh = N_(\"mode\"),\n+\t\t\t.help = N_(\"show untracked files, optional modes: all, normal, no. (Default: all)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)\"all\",\n+\t\t},\n \t\tOPT_PATHSPEC_FROM_FILE(&pathspec_from_file),\n \t\tOPT_PATHSPEC_FILE_NUL(&pathspec_file_nul),\n \t\t/* end commit contents options */\ndiff --git a/builtin/config.c b/builtin/config.c\nindex 53a90094e31..f70d6354772 100644\n--- a/builtin/config.c\n+++ b/builtin/config.c\n@@ -131,9 +131,16 @@ struct config_display_options {\n #define TYPE_COLOR\t\t6\n #define TYPE_BOOL_OR_STR\t7\n \n-#define OPT_CALLBACK_VALUE(s, l, v, h, i) \\\n-\t{ OPTION_CALLBACK, (s), (l), (v), NULL, (h), PARSE_OPT_NOARG | \\\n-\tPARSE_OPT_NONEG, option_parse_type, (i) }\n+#define OPT_CALLBACK_VALUE(s, l, v, h, i) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG, \\\n+\t.callback = option_parse_type, \\\n+\t.defval = (i), \\\n+}\n \n static int option_parse_type(const struct option *opt, const char *arg,\n \t\t\t     int unset)\ndiff --git a/builtin/describe.c b/builtin/describe.c\nindex e2e73f3d757..2da9f4fed01 100644\n--- a/builtin/describe.c\n+++ b/builtin/describe.c\n@@ -601,12 +601,24 @@ int cmd_describe(int argc,\n \t\t\t   N_(\"do not consider tags matching <pattern>\")),\n \t\tOPT_BOOL(0, \"always\",        &always,\n \t\t\tN_(\"show abbreviated commit object as fallback\")),\n-\t\t{OPTION_STRING, 0, \"dirty\",  &dirty, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-dirty\"},\n-\t\t{OPTION_STRING, 0, \"broken\",  &broken, N_(\"mark\"),\n-\t\t\tN_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"-broken\"},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"dirty\",\n+\t\t\t.value = &dirty,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on dirty working tree (default: \\\"-dirty\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-dirty\",\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"broken\",\n+\t\t\t.value = &broken,\n+\t\t\t.argh = N_(\"mark\"),\n+\t\t\t.help = N_(\"append <mark> on broken working tree (default: \\\"-broken\\\")\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"-broken\",\n+\t\t},\n \t\tOPT_END(),\n \t};\n \ndiff --git a/builtin/fetch.c b/builtin/fetch.c\nindex 02af5054690..3a5159d9e69 100644\n--- a/builtin/fetch.c\n+++ b/builtin/fetch.c\n@@ -2359,8 +2359,14 @@ int cmd_fetch(int argc,\n \t\tOPT_SET_INT_F(0, \"refetch\", &refetch,\n \t\t\t      N_(\"re-fetch without negotiating common commits\"),\n \t\t\t      1, PARSE_OPT_NONEG),\n-\t\t{ OPTION_STRING, 0, \"submodule-prefix\", &submodule_prefix, N_(\"dir\"),\n-\t\t\t   N_(\"prepend this to submodule path output\"), PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"submodule-prefix\",\n+\t\t\t.value = &submodule_prefix,\n+\t\t\t.argh = N_(\"dir\"),\n+\t\t\t.help = N_(\"prepend this to submodule path output\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"recurse-submodules-default\",\n \t\t\t   &recurse_submodules_default, N_(\"on-demand\"),\n \t\t\t   N_(\"default for recursive fetching of submodules \"\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 189cd1096a0..240cdb474bc 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -20,13 +20,24 @@ int cmd_fmt_merge_msg(int argc,\n \tchar *into_name = NULL;\n \tint shortlog_len = -1;\n \tstruct option options[] = {\n-\t\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"populate log with at most <n> entries from shortlog\"),\n-\t\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n-\t\t{ OPTION_INTEGER, 0, \"summary\", &shortlog_len, N_(\"n\"),\n-\t\t  N_(\"alias for --log (deprecated)\"),\n-\t\t  PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN, NULL,\n-\t\t  DEFAULT_MERGE_LOG_LEN },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"log\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"summary\",\n+\t\t\t.value = &shortlog_len,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"alias for --log (deprecated)\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\n+\t\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t\t},\n \t\tOPT_STRING('m', \"message\", &message, N_(\"text\"),\n \t\t\tN_(\"use <text> as start of message\")),\n \t\tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\ndiff --git a/builtin/gc.c b/builtin/gc.c\nindex 99431fd4674..6707a26bc6e 100644\n--- a/builtin/gc.c\n+++ b/builtin/gc.c\n@@ -699,9 +699,15 @@ struct repository *repo UNUSED)\n \tint ret;\n \tstruct option builtin_gc_options[] = {\n \t\tOPT__QUIET(&quiet, N_(\"suppress progress reporting\")),\n-\t\t{ OPTION_STRING, 0, \"prune\", &prune_expire_arg, N_(\"date\"),\n-\t\t\tN_(\"prune unreferenced objects\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t)prune_expire_arg },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prune\",\n+\t\t\t.value = &prune_expire_arg,\n+\t\t\t.argh = N_(\"date\"),\n+\t\t\t.help = N_(\"prune unreferenced objects\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t)prune_expire_arg,\n+\t\t},\n \t\tOPT_BOOL(0, \"cruft\", &cfg.cruft_packs, N_(\"pack unreferenced objects separately\")),\n \t\tOPT_MAGNITUDE(0, \"max-cruft-size\", &cfg.max_cruft_size,\n \t\t\t      N_(\"with --cruft, limit the size of new cruft packs\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex d1427290f77..c4869733e1b 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -1017,10 +1017,16 @@ int cmd_grep(int argc,\n \t\tOPT_BOOL(0, \"all-match\", &opt.all_match,\n \t\t\tN_(\"show only matches from files that match all patterns\")),\n \t\tOPT_GROUP(\"\"),\n-\t\t{ OPTION_STRING, 'O', \"open-files-in-pager\", &show_in_pager,\n-\t\t\tN_(\"pager\"), N_(\"show matching files in the pager\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n-\t\t\tNULL, (intptr_t)default_pager },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'O',\n+\t\t\t.long_name = \"open-files-in-pager\",\n+\t\t\t.value = &show_in_pager,\n+\t\t\t.argh = N_(\"pager\"),\n+\t\t\t.help = N_(\"show matching files in the pager\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NOCOMPLETE,\n+\t\t\t.defval = (intptr_t)default_pager,\n+\t\t},\n \t\tOPT_BOOL_F(0, \"ext-grep\", &external_grep_allowed__ignored,\n \t\t\t   N_(\"allow calling of grep(1) (ignored by this build)\"),\n \t\t\t   PARSE_OPT_NOCOMPLETE),\ndiff --git a/builtin/init-db.c b/builtin/init-db.c\nindex 196dccdd77a..4a950e44d8d 100644\n--- a/builtin/init-db.c\n+++ b/builtin/init-db.c\n@@ -93,10 +93,15 @@ int cmd_init_db(int argc,\n \t\t\t\tN_(\"directory from which templates will be used\")),\n \t\tOPT_SET_INT(0, \"bare\", &is_bare_repository_cfg,\n \t\t\t\tN_(\"create a bare repository\"), 1),\n-\t\t{ OPTION_CALLBACK, 0, \"shared\", &init_shared_repository,\n-\t\t\tN_(\"permissions\"),\n-\t\t\tN_(\"specify that the git repository is to be shared amongst several users\"),\n-\t\t\tPARSE_OPT_OPTARG | PARSE_OPT_NONEG, shared_callback, 0},\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"shared\",\n+\t\t\t.value = &init_shared_repository,\n+\t\t\t.argh = N_(\"permissions\"),\n+\t\t\t.help = N_(\"specify that the git repository is to be shared amongst several users\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_NONEG,\n+\t\t\t.callback = shared_callback\n+\t\t},\n \t\tOPT_BIT('q', \"quiet\", &flags, N_(\"be quiet\"), INIT_DB_QUIET),\n \t\tOPT_STRING(0, \"separate-git-dir\", &real_git_dir, N_(\"gitdir\"),\n \t\t\t   N_(\"separate git dir from working tree\")),\ndiff --git a/builtin/ls-remote.c b/builtin/ls-remote.c\nindex 42f34e12361..01a4d4daa1f 100644\n--- a/builtin/ls-remote.c\n+++ b/builtin/ls-remote.c\n@@ -67,9 +67,14 @@ int cmd_ls_remote(int argc,\n \t\tOPT__QUIET(&quiet, N_(\"do not print remote URL\")),\n \t\tOPT_STRING(0, \"upload-pack\", &uploadpack, N_(\"exec\"),\n \t\t\t   N_(\"path of git-upload-pack on the remote host\")),\n-\t\t{ OPTION_STRING, 0, \"exec\", &uploadpack, N_(\"exec\"),\n-\t\t\t   N_(\"path of git-upload-pack on the remote host\"),\n-\t\t\t   PARSE_OPT_HIDDEN },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"exec\",\n+\t\t\t.value = &uploadpack,\n+\t\t\t.argh = N_(\"exec\"),\n+\t\t\t.help = N_(\"path of git-upload-pack on the remote host\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN,\n+\t\t},\n \t\tOPT_BIT('t', \"tags\", &flags, N_(\"limit to tags\"), REF_TAGS),\n \t\tOPT_BIT('b', \"branches\", &flags, N_(\"limit to branches\"), REF_BRANCHES),\n \t\tOPT_BIT_F('h', \"heads\", &flags,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex ba9faf126aa..21787d45165 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -250,9 +250,15 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"stat\", &show_diffstat,\n \t\tN_(\"show a diffstat at the end of the merge\")),\n \tOPT_BOOL(0, \"summary\", &show_diffstat, N_(\"(synonym to --stat)\")),\n-\t{ OPTION_INTEGER, 0, \"log\", &shortlog_len, N_(\"n\"),\n-\t  N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n-\t  PARSE_OPT_OPTARG, NULL, DEFAULT_MERGE_LOG_LEN },\n+\t{\n+\t\t.type = OPTION_INTEGER,\n+\t\t.long_name = \"log\",\n+\t\t.value = &shortlog_len,\n+\t\t.argh = N_(\"n\"),\n+\t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = DEFAULT_MERGE_LOG_LEN,\n+\t},\n \tOPT_BOOL(0, \"squash\", &squash,\n \t\tN_(\"create a single commit instead of doing a merge\")),\n \tOPT_BOOL(0, \"commit\", &option_commit,\n@@ -274,9 +280,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_CALLBACK('m', \"message\", &merge_msg, N_(\"message\"),\n \t\tN_(\"merge commit message (for a non-fast-forward merge)\"),\n \t\toption_parse_message),\n-\t{ OPTION_LOWLEVEL_CALLBACK, 'F', \"file\", &merge_msg, N_(\"path\"),\n-\t\tN_(\"read message from file\"), PARSE_OPT_NONEG,\n-\t\tNULL, 0, option_read_message },\n+\t{\n+\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t.short_name = 'F',\n+\t\t.long_name = \"file\",\n+\t\t.value = &merge_msg,\n+\t\t.argh = N_(\"path\"),\n+\t\t.help = N_(\"read message from file\"),\n+\t\t.flags = PARSE_OPT_NONEG,\n+\t\t.ll_callback = option_read_message,\n+\t},\n \tOPT_STRING(0, \"into-name\", &into_name, N_(\"name\"),\n \t\t   N_(\"use <name> instead of the real target\")),\n \tOPT__VERBOSITY(&verbosity),\n@@ -289,8 +302,16 @@ static struct option builtin_merge_options[] = {\n \tOPT_BOOL(0, \"allow-unrelated-histories\", &allow_unrelated_histories,\n \t\t N_(\"allow merging unrelated histories\")),\n \tOPT_SET_INT(0, \"progress\", &show_progress, N_(\"force progress reporting\"), 1),\n-\t{ OPTION_STRING, 'S', \"gpg-sign\", &sign_commit, N_(\"key-id\"),\n-\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t{\n+\t\t.type = OPTION_STRING,\n+\t\t.short_name = 'S',\n+\t\t.long_name = \"gpg-sign\",\n+\t\t.value = &sign_commit,\n+\t\t.argh = N_(\"key-id\"),\n+\t\t.help = N_(\"GPG sign commit\"),\n+\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t.defval = (intptr_t) \"\",\n+\t},\n \tOPT_AUTOSTASH(&autostash),\n \tOPT_BOOL(0, \"overwrite-ignore\", &overwrite_ignore, N_(\"update ignored files (default)\")),\n \tOPT_BOOL(0, \"signoff\", &signoff, N_(\"add a Signed-off-by trailer\")),\ndiff --git a/builtin/read-tree.c b/builtin/read-tree.c\nindex d2a807a828b..a8f352f7cd9 100644\n--- a/builtin/read-tree.c\n+++ b/builtin/read-tree.c\n@@ -135,9 +135,14 @@ int cmd_read_tree(int argc,\n \t\t\t N_(\"3-way merge in presence of adds and removes\")),\n \t\tOPT_BOOL(0, \"reset\", &opts.reset,\n \t\t\t N_(\"same as -m, but discard unmerged entries\")),\n-\t\t{ OPTION_STRING, 0, \"prefix\", &opts.prefix, N_(\"<subdirectory>/\"),\n-\t\t  N_(\"read the tree into the index under <subdirectory>/\"),\n-\t\t  PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.long_name = \"prefix\",\n+\t\t\t.value = &opts.prefix,\n+\t\t\t.argh = N_(\"<subdirectory>/\"),\n+\t\t\t.help = N_(\"read the tree into the index under <subdirectory>/\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_BOOL('u', NULL, &opts.update,\n \t\t\t N_(\"update working tree with merge result\")),\n \t\tOPT_CALLBACK_F(0, \"exclude-per-directory\", &opts,\ndiff --git a/builtin/rebase.c b/builtin/rebase.c\nindex d4715ed35d7..d4083350090 100644\n--- a/builtin/rebase.c\n+++ b/builtin/rebase.c\n@@ -1122,9 +1122,15 @@ int cmd_rebase(int argc,\n \t\tOPT_BIT('v', \"verbose\", &options.flags,\n \t\t\tN_(\"display a diffstat of what changed upstream\"),\n \t\t\tREBASE_NO_QUIET | REBASE_VERBOSE | REBASE_DIFFSTAT),\n-\t\t{OPTION_NEGBIT, 'n', \"no-stat\", &options.flags, NULL,\n-\t\t\tN_(\"do not show diffstat of what changed upstream\"),\n-\t\t\tPARSE_OPT_NOARG, NULL, REBASE_DIFFSTAT },\n+\t\t{\n+\t\t\t.type = OPTION_NEGBIT,\n+\t\t\t.short_name = 'n',\n+\t\t\t.long_name = \"no-stat\",\n+\t\t\t.value = &options.flags,\n+\t\t\t.help = N_(\"do not show diffstat of what changed upstream\"),\n+\t\t\t.flags = PARSE_OPT_NOARG,\n+\t\t\t.defval = REBASE_DIFFSTAT,\n+\t\t},\n \t\tOPT_BOOL(0, \"signoff\", &options.signoff,\n \t\t\t N_(\"add a Signed-off-by trailer to each commit\")),\n \t\tOPT_BOOL(0, \"committer-date-is-author-date\",\n@@ -1190,9 +1196,16 @@ int cmd_rebase(int argc,\n \t\tOPT_BOOL(0, \"update-refs\", &options.update_refs,\n \t\t\t N_(\"update branches that point to commits \"\n \t\t\t    \"that are being rebased\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t\tN_(\"GPG-sign commits\"),\n-\t\t\tPARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG-sign commits\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_AUTOSTASH(&options.autostash),\n \t\tOPT_STRING_LIST('x', \"exec\", &options.exec, N_(\"exec\"),\n \t\t\t\tN_(\"add exec lines after each commit of the \"\ndiff --git a/builtin/revert.c b/builtin/revert.c\nindex aca6c293cdf..4f5ef975494 100644\n--- a/builtin/revert.c\n+++ b/builtin/revert.c\n@@ -132,8 +132,16 @@ static int run_sequencer(int argc, const char **argv, const char *prefix,\n \t\tOPT_STRING(0, \"strategy\", &strategy, N_(\"strategy\"), N_(\"merge strategy\")),\n \t\tOPT_STRVEC('X', \"strategy-option\", &opts->xopts, N_(\"option\"),\n \t\t\tN_(\"option for merge strategy\")),\n-\t\t{ OPTION_STRING, 'S', \"gpg-sign\", &gpg_sign, N_(\"key-id\"),\n-\t\t  N_(\"GPG sign commit\"), PARSE_OPT_OPTARG, NULL, (intptr_t) \"\" },\n+\t\t{\n+\t\t\t.type = OPTION_STRING,\n+\t\t\t.short_name = 'S',\n+\t\t\t.long_name = \"gpg-sign\",\n+\t\t\t.value = &gpg_sign,\n+\t\t\t.argh = N_(\"key-id\"),\n+\t\t\t.help = N_(\"GPG sign commit\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = (intptr_t) \"\",\n+\t\t},\n \t\tOPT_END()\n \t};\n \tstruct option *options = base_options;\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex fce6b404e92..dab37019d29 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -667,9 +667,15 @@ int cmd_show_branch(int ac,\n \t\t\t N_(\"show remote-tracking branches\")),\n \t\tOPT__COLOR(&showbranch_use_color,\n \t\t\t    N_(\"color '*!+-' corresponding to the branch\")),\n-\t\t{ OPTION_INTEGER, 0, \"more\", &extra, N_(\"n\"),\n-\t\t\t    N_(\"show <n> more commits after the common ancestor\"),\n-\t\t\t    PARSE_OPT_OPTARG, NULL, (intptr_t)1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.long_name = \"more\",\n+\t\t\t.value = &extra,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_SET_INT(0, \"list\", &extra, N_(\"synonym to more=-1\"), -1),\n \t\tOPT_BOOL(0, \"no-name\", &no_name, N_(\"suppress naming strings\")),\n \t\tOPT_BOOL(0, \"current\", &with_current_branch,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex d3e0943b734..b266f12bb48 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -479,9 +479,15 @@ int cmd_tag(int argc,\n \tint edit_flag = 0;\n \tstruct option options[] = {\n \t\tOPT_CMDMODE('l', \"list\", &cmdmode, N_(\"list tag names\"), 'l'),\n-\t\t{ OPTION_INTEGER, 'n', NULL, &filter.lines, N_(\"n\"),\n-\t\t\t\tN_(\"print <n> lines of each tag message\"),\n-\t\t\t\tPARSE_OPT_OPTARG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_INTEGER,\n+\t\t\t.short_name = 'n',\n+\t\t\t.value = &filter.lines,\n+\t\t\t.argh = N_(\"n\"),\n+\t\t\t.help = N_(\"print <n> lines of each tag message\"),\n+\t\t\t.flags = PARSE_OPT_OPTARG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_CMDMODE('d', \"delete\", &cmdmode, N_(\"delete tags\"), 'd'),\n \t\tOPT_CMDMODE('v', \"verify\", &cmdmode, N_(\"verify tags\"), 'v'),\n \n@@ -513,9 +519,14 @@ int cmd_tag(int argc,\n \t\t\tN_(\"do not output a newline after empty formatted refs\")),\n \t\tOPT_REF_SORT(&sorting_options),\n \t\t{\n-\t\t\tOPTION_CALLBACK, 0, \"points-at\", &filter.points_at, N_(\"object\"),\n-\t\t\tN_(\"print only tags of the object\"), PARSE_OPT_LASTARG_DEFAULT,\n-\t\t\tparse_opt_object_name, (intptr_t) \"HEAD\"\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"points-at\",\n+\t\t\t.value = &filter.points_at,\n+\t\t\t.argh = N_(\"object\"),\n+\t\t\t.help = N_(\"print only tags of the object\"),\n+\t\t\t.flags = PARSE_OPT_LASTARG_DEFAULT,\n+\t\t\t.callback = parse_opt_object_name,\n+\t\t\t.defval = (intptr_t) \"HEAD\",\n \t\t},\n \t\tOPT_STRING(  0 , \"format\", &format.format, N_(\"format\"),\n \t\t\t   N_(\"format to use for the output\")),\ndiff --git a/builtin/update-index.c b/builtin/update-index.c\nindex b2f6b1a3fbb..ee64b022679 100644\n--- a/builtin/update-index.c\n+++ b/builtin/update-index.c\n@@ -964,29 +964,51 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"like --refresh, but ignore assume-unchanged setting\"),\n \t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG,\n \t\t\treally_refresh_callback),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"cacheinfo\", NULL,\n-\t\t\tN_(\"<mode>,<object>,<path>\"),\n-\t\t\tN_(\"add the specified entry to the index\"),\n-\t\t\tPARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n-\t\t\tNULL, 0,\n-\t\t\tcacheinfo_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"cacheinfo\",\n+\t\t\t.argh = N_(\"<mode>,<object>,<path>\"),\n+\t\t\t.help = N_(\"add the specified entry to the index\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | /* disallow --cacheinfo=<mode> form */\n+\t\t\t\t PARSE_OPT_NONEG | PARSE_OPT_LITERAL_ARGHELP,\n+\t\t\t.ll_callback = cacheinfo_callback,\n+\t\t},\n \t\tOPT_CALLBACK_F(0, \"chmod\", &set_executable_bit, \"(+|-)x\",\n \t\t\tN_(\"override the executable bit of the listed files\"),\n \t\t\tPARSE_OPT_NONEG,\n \t\t\tchmod_callback),\n-\t\t{OPTION_SET_INT, 0, \"assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"mark files as \\\"not changing\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-assume-unchanged\", &mark_valid_only, NULL,\n-\t\t\tN_(\"clear assumed-unchanged bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"mark files as \\\"index-only\\\"\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-skip-worktree\", &mark_skip_worktree_only, NULL,\n-\t\t\tN_(\"clear skip-worktree bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"mark files as \\\"not changing\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-assume-unchanged\",\n+\t\t\t.value = &mark_valid_only,\n+\t\t\t.help = N_(\"clear assumed-unchanged bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"mark files as \\\"index-only\\\"\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-skip-worktree\",\n+\t\t\t.value = &mark_skip_worktree_only,\n+\t\t\t.help = N_(\"clear skip-worktree bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_BOOL(0, \"ignore-skip-worktree-entries\", &ignore_skip_worktree_entries,\n \t\t\t N_(\"do not touch index-only entries\")),\n \t\tOPT_SET_INT(0, \"info-only\", &info_only,\n@@ -995,22 +1017,39 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"remove named paths even if present in worktree\"), 1),\n \t\tOPT_BOOL('z', NULL, &nul_term_line,\n \t\t\t N_(\"with --stdin: input lines are terminated by null bytes\")),\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"stdin\", &read_from_stdin, NULL,\n-\t\t\tN_(\"read list of paths to be updated from standard input\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"index-info\", &nul_term_line, NULL,\n-\t\t\tN_(\"add entries from standard input to the index\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, stdin_cacheinfo_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 0, \"unresolve\", &has_errors, NULL,\n-\t\t\tN_(\"repopulate stages #2 and #3 for the listed paths\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, unresolve_callback},\n-\t\t{OPTION_LOWLEVEL_CALLBACK, 'g', \"again\", &has_errors, NULL,\n-\t\t\tN_(\"only update entries that differ from HEAD\"),\n-\t\t\tPARSE_OPT_NONEG | PARSE_OPT_NOARG,\n-\t\t\tNULL, 0, reupdate_callback},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"stdin\",\n+\t\t\t.value = &read_from_stdin,\n+\t\t\t.help = N_(\"read list of paths to be updated from standard input\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"index-info\",\n+\t\t\t.value = &nul_term_line,\n+\t\t\t.help = N_(\"add entries from standard input to the index\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = stdin_cacheinfo_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.long_name = \"unresolve\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"repopulate stages #2 and #3 for the listed paths\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = unresolve_callback,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_LOWLEVEL_CALLBACK,\n+\t\t\t.short_name = 'g',\n+\t\t\t.long_name = \"again\",\n+\t\t\t.value = &has_errors,\n+\t\t\t.help = N_(\"only update entries that differ from HEAD\"),\n+\t\t\t.flags = PARSE_OPT_NONEG | PARSE_OPT_NOARG,\n+\t\t\t.ll_callback = reupdate_callback,\n+\t\t},\n \t\tOPT_BIT(0, \"ignore-missing\", &refresh_args.flags,\n \t\t\tN_(\"ignore files missing from worktree\"),\n \t\t\tREFRESH_IGNORE_MISSING),\n@@ -1036,12 +1075,22 @@ int cmd_update_index(int argc,\n \t\t\tN_(\"write out the index even if is not flagged as changed\"), 1),\n \t\tOPT_BOOL(0, \"fsmonitor\", &fsmonitor,\n \t\t\tN_(\"enable or disable file system monitor\")),\n-\t\t{OPTION_SET_INT, 0, \"fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"mark files as fsmonitor valid\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, MARK_FLAG},\n-\t\t{OPTION_SET_INT, 0, \"no-fsmonitor-valid\", &mark_fsmonitor_only, NULL,\n-\t\t\tN_(\"clear fsmonitor valid bit\"),\n-\t\t\tPARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, UNMARK_FLAG},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"mark files as fsmonitor valid\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = MARK_FLAG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.long_name = \"no-fsmonitor-valid\",\n+\t\t\t.value = &mark_fsmonitor_only,\n+\t\t\t.help = N_(\"clear fsmonitor valid bit\"),\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = UNMARK_FLAG,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/builtin/write-tree.c b/builtin/write-tree.c\nindex 43f233e69b0..5a8dc377ec0 100644\n--- a/builtin/write-tree.c\n+++ b/builtin/write-tree.c\n@@ -31,10 +31,14 @@ int cmd_write_tree(int argc,\n \t\t\tWRITE_TREE_MISSING_OK),\n \t\tOPT_STRING(0, \"prefix\", &tree_prefix, N_(\"<prefix>/\"),\n \t\t\t   N_(\"write tree object for a subdirectory <prefix>\")),\n-\t\t{ OPTION_BIT, 0, \"ignore-cache-tree\", &flags, NULL,\n-\t\t  N_(\"only useful for debugging\"),\n-\t\t  PARSE_OPT_HIDDEN | PARSE_OPT_NOARG, NULL,\n-\t\t  WRITE_TREE_IGNORE_CACHE_TREE },\n+\t\t{\n+\t\t\t.type = OPTION_BIT,\n+\t\t\t.long_name = \"ignore-cache-tree\",\n+\t\t\t.value = &flags,\n+\t\t\t.help = N_(\"only useful for debugging\"),\n+\t\t\t.flags = PARSE_OPT_HIDDEN | PARSE_OPT_NOARG,\n+\t\t\t.defval = WRITE_TREE_IGNORE_CACHE_TREE,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/diff.c b/diff.c\nindex 08f5e00a2cc..f2fcc7f3c22 100644\n--- a/diff.c\n+++ b/diff.c\n@@ -5892,10 +5892,15 @@ struct option *add_diff_options(const struct option *opts,\n \t\tOPT_CALLBACK_F(0, \"diff-filter\", options, N_(\"[(A|C|D|M|R|T|U|X|B)...[*]]\"),\n \t\t\t       N_(\"select files by diff type\"),\n \t\t\t       PARSE_OPT_NONEG, diff_opt_diff_filter),\n-\t\t{ OPTION_CALLBACK, 0, \"output\", options, N_(\"<file>\"),\n-\t\t  N_(\"output to a specific file\"),\n-\t\t  PARSE_OPT_NONEG, NULL, 0, diff_opt_output },\n-\n+\t\t{\n+\t\t\t.type = OPTION_CALLBACK,\n+\t\t\t.long_name = \"output\",\n+\t\t\t.value = options,\n+\t\t\t.argh = N_(\"<file>\"),\n+\t\t\t.help = N_(\"output to a specific file\"),\n+\t\t\t.flags = PARSE_OPT_NONEG,\n+\t\t\t.ll_callback = diff_opt_output,\n+\t\t},\n \t\tOPT_END()\n \t};\n \ndiff --git a/ref-filter.h b/ref-filter.h\nindex 013d4cfa64b..c98c4fbd4c1 100644\n--- a/ref-filter.h\n+++ b/ref-filter.h\n@@ -114,11 +114,16 @@ struct ref_format {\n }\n \n /*  Macros for checking --merged and --no-merged options */\n-#define _OPT_MERGED_NO_MERGED(option, filter, h) \\\n-\t{ OPTION_CALLBACK, 0, option, (filter), N_(\"commit\"), (h), \\\n-\t  PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n-\t  parse_opt_merge_filter, (intptr_t) \"HEAD\" \\\n-\t}\n+#define _OPT_MERGED_NO_MERGED(option, filter, h) { \\\n+\t.type = OPTION_CALLBACK, \\\n+\t.long_name = option, \\\n+\t.value = (filter), \\\n+\t.argh = N_(\"commit\"), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_LASTARG_DEFAULT | PARSE_OPT_NONEG, \\\n+\t.callback = parse_opt_merge_filter, \\\n+\t.defval = (intptr_t) \"HEAD\", \\\n+}\n #define OPT_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"merged\", f, h)\n #define OPT_NO_MERGED(f, h) _OPT_MERGED_NO_MERGED(\"no-merged\", f, h)\n \ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex bfe45ec68b0..997f55fd45b 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -124,8 +124,15 @@ int cmd__parse_options(int argc, const char **argv)\n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n \t\tOPT_BOOL('D', \"no-doubt\", &boolean, \"begins with 'no-'\"),\n-\t\t{ OPTION_SET_INT, 'B', \"no-fear\", &boolean, NULL,\n-\t\t  \"be brave\", PARSE_OPT_NOARG | PARSE_OPT_NONEG, NULL, 1 },\n+\t\t{\n+\t\t\t.type = OPTION_SET_INT,\n+\t\t\t.short_name = 'B',\n+\t\t\t.long_name = \"no-fear\",\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"be brave\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t\t.defval = 1,\n+\t\t},\n \t\tOPT_COUNTUP('b', \"boolean\", &boolean, \"increment by one\"),\n \t\tOPT_BIT('4', \"or4\", &boolean,\n \t\t\t\"bitwise-or boolean with ...0100\", 4),\n@@ -155,12 +162,27 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"Magic arguments\"),\n \t\tOPT_NUMBER_CALLBACK(&integer, \"set integer to NUM\",\n \t\t\tnumber_callback),\n-\t\t{ OPTION_COUNTUP, '+', NULL, &boolean, NULL, \"same as -b\",\n-\t\t  PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH },\n-\t\t{ OPTION_COUNTUP, 0, \"ambiguous\", &ambiguous, NULL,\n-\t\t  \"positive ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n-\t\t{ OPTION_COUNTUP, 0, \"no-ambiguous\", &ambiguous, NULL,\n-\t\t  \"negative ambiguity\", PARSE_OPT_NOARG | PARSE_OPT_NONEG },\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.short_name = '+',\n+\t\t\t.value = &boolean,\n+\t\t\t.help = \"same as -b\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG | PARSE_OPT_NODASH,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"positive ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n+\t\t{\n+\t\t\t.type = OPTION_COUNTUP,\n+\t\t\t.long_name = \"no-ambiguous\",\n+\t\t\t.value = &ambiguous,\n+\t\t\t.help = \"negative ambiguity\",\n+\t\t\t.flags = PARSE_OPT_NOARG | PARSE_OPT_NONEG,\n+\t\t},\n \t\tOPT_GROUP(\"Standard options\"),\n \t\tOPT__ABBREV(&abbrev),\n \t\tOPT__VERBOSE(&verbose, \"be verbose\"),\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516267","messageId":"20250416-b4-pks-parse-options-integers-v3-4-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 4/7] parse-options: introduce precision handling for `OPTION_MAGNITUDE`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:13Z","receivedAt":"2025-04-16T10:02:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"This commit is the equivalent to the preceding commit, but instead of\nintroducing precision handling for `OPTION_INTEGER` we introduce it for\n`OPTION_MAGNITUDE`.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 45 ++++++++++++++++++++++++++++++++-----------\n parse-options.h               |  1 +\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 18 ++++++++++++++++-\n 4 files changed, 55 insertions(+), 12 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 2cb9bd3b5b9..259716efb17 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -202,7 +202,7 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \n \t\tif (value < lower_bound || value > upper_bound)\n \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n-\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n+\t\t\t\t     arg, optname(opt, flags), (intmax_t)lower_bound, (intmax_t)upper_bound);\n \n \t\tswitch (opt->precision) {\n \t\tcase 1:\n@@ -223,21 +223,44 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t}\n \t}\n \tcase OPTION_MAGNITUDE:\n+\t{\n+\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tunsigned long value;\n+\n \t\tif (unset) {\n-\t\t\t*(unsigned long *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(unsigned long *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!git_parse_ulong(arg, opt->value))\n+\t\t} else if (!git_parse_ulong(arg, &value)) {\n \t\t\treturn error(_(\"%s expects a non-negative integer value\"\n \t\t\t\t       \" with an optional k/m/g suffix\"),\n \t\t\t\t     optname(opt, flags));\n-\t\treturn 0;\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n+\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(uint8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(uint16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(uint32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(uint64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \n \tdefault:\n \t\tBUG(\"opt->type %d should not happen\", opt->type);\ndiff --git a/parse-options.h b/parse-options.h\nindex 8db96402c4d..55c42faa29f 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -281,6 +281,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = PARSE_OPT_NONEG, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex b1275dfade4..46deb4317ef 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tuint16_t m16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -143,6 +144,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n+\t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n \t\tOPT_CMDMODE(0, \"mode1\", &integer, \"set integer to 1 (cmdmode option)\", 1),\n \t\tOPT_CMDMODE(0, \"mode2\", &integer, \"set integer to 2 (cmdmode option)\", 2),\n@@ -214,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n+\tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\n \tshow(&expect, &ret, \"abbrev: %d\", abbrev);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 95951436cda..8daaf568485 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -25,6 +25,7 @@ usage: test-tool parse-options <options>\n     --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n+    --m16 <n>             get a 16 bit magnitude\n     --[no-]set23          set integer to 23\n     --mode1               set integer to 1 (cmdmode option)\n     --mode2               set integer to 2 (cmdmode option)\n@@ -139,6 +140,7 @@ boolean: 2\n integer: 1729\n i16: 0\n magnitude: 16384\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -160,6 +162,7 @@ boolean: 2\n integer: 1729\n i16: 9000\n magnitude: 16384\n+m16: 32768\n timestamp: 0\n string: 321\n abbrev: 10\n@@ -171,7 +174,7 @@ EOF\n \n test_expect_success 'long options' '\n \ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n-\t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n+\t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n \ttest_must_be_empty output.err &&\n@@ -184,6 +187,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tinteger: 0\n \ti16: 0\n \tmagnitude: 0\n+\tm16: 0\n \ttimestamp: 0\n \tstring: (not set)\n \tabbrev: 100\n@@ -259,6 +263,7 @@ boolean: 1\n integer: 13\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: 123\n abbrev: 7\n@@ -283,6 +288,7 @@ boolean: 0\n integer: 2\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -351,6 +357,7 @@ boolean: 5\n integer: 4\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -377,6 +384,7 @@ boolean: 1\n integer: 23\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -457,6 +465,7 @@ boolean: 0\n integer: 0\n i16: 0\n magnitude: 0\n+m16: 0\n timestamp: 0\n string: (not set)\n abbrev: 7\n@@ -810,4 +819,11 @@ test_expect_success 'i16 limits range' '\n \ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n '\n \n+test_expect_success 'm16 limits range' '\n+\ttest-tool parse-options --m16 65535 >out &&\n+\ttest_grep \"m16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --m16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .m16. not in range \\[0,65535\\]\" err\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516268","messageId":"20250416-b4-pks-parse-options-integers-v3-3-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:12Z","receivedAt":"2025-04-16T10:02:30Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"The `OPTION_INTEGER` option type accepts a signed integer. The type of\nthe underlying integer is a simple `int`, which restricts the range of\nvalues accepted by such options. But there is a catch: because the\ncaller provides a pointer to the value via the `.value` field, which is\na simple void pointer. This has two consequences:\n\n  - There is no check whether the passed value is sufficiently long to\n    store the entire range of `int`. This can lead to integer wraparound\n    in the best case and out-of-bounds writes in the worst case.\n\n  - Even when a caller knows that they want to store a value larger than\n    `INT_MAX` they don't have a way to do so.\n\nFunny enough, even if the caller gets everything correct the parsing\nlogic is still insufficient because we use `strtol()` to parse the\nargument, which returns a `long`. But as that value is implicitly cast\nwhen assigning it to the `int` field we may still get invalid results.\n\nIn practice this doesn't tend to be a huge issue because users typically\ndon't end up passing huge values to most commands. But the parsing logic\nis demonstrably broken, and it is too easy to get the calling convention\nwrong.\n\nImprove the situation by introducing a new `precision` field into the\nstructure. This field gets assigned automatically by `OPT_INTEGER_F()`\nand tracks the size of the passed value. Like this it becomes possible\nfor the caller to pass arbitrarily-sized integers and the underlying\nlogic knows to handle it correctly by doing range checks. Furthermore,\nconvert the code to use `strtoimax()` intstead of `strtol()` so that we\ncan also parse values larger than `LONG_MAX`.\n\nNote that we do not yet assert signedness of the passed variable, which\nis another source of bugs. This will be handled in a subsequent commit.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n builtin/fmt-merge-msg.c       |  2 ++\n builtin/merge.c               |  1 +\n builtin/show-branch.c         |  1 +\n builtin/tag.c                 |  1 +\n parse-options.c               | 63 +++++++++++++++++++++++++++++--------------\n parse-options.h               |  6 +++++\n t/helper/test-parse-options.c |  3 +++\n t/t0040-parse-options.sh      | 23 +++++++++++++++-\n 8 files changed, 79 insertions(+), 21 deletions(-)\n\ndiff --git a/builtin/fmt-merge-msg.c b/builtin/fmt-merge-msg.c\nindex 240cdb474bc..3b6aac2cf7f 100644\n--- a/builtin/fmt-merge-msg.c\n+++ b/builtin/fmt-merge-msg.c\n@@ -24,6 +24,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"log\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"populate log with at most <n> entries from shortlog\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\n@@ -33,6 +34,7 @@ int cmd_fmt_merge_msg(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"summary\",\n \t\t\t.value = &shortlog_len,\n+\t\t\t.precision = sizeof(shortlog_len),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"alias for --log (deprecated)\"),\n \t\t\t.flags = PARSE_OPT_OPTARG | PARSE_OPT_HIDDEN,\ndiff --git a/builtin/merge.c b/builtin/merge.c\nindex 21787d45165..9ab10c7db0a 100644\n--- a/builtin/merge.c\n+++ b/builtin/merge.c\n@@ -254,6 +254,7 @@ static struct option builtin_merge_options[] = {\n \t\t.type = OPTION_INTEGER,\n \t\t.long_name = \"log\",\n \t\t.value = &shortlog_len,\n+\t\t.precision = sizeof(shortlog_len),\n \t\t.argh = N_(\"n\"),\n \t\t.help = N_(\"add (at most <n>) entries from shortlog to merge commit message\"),\n \t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/show-branch.c b/builtin/show-branch.c\nindex dab37019d29..b549d8c3f5b 100644\n--- a/builtin/show-branch.c\n+++ b/builtin/show-branch.c\n@@ -671,6 +671,7 @@ int cmd_show_branch(int ac,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.long_name = \"more\",\n \t\t\t.value = &extra,\n+\t\t\t.precision = sizeof(extra),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"show <n> more commits after the common ancestor\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/builtin/tag.c b/builtin/tag.c\nindex b266f12bb48..7597d93c71b 100644\n--- a/builtin/tag.c\n+++ b/builtin/tag.c\n@@ -483,6 +483,7 @@ int cmd_tag(int argc,\n \t\t\t.type = OPTION_INTEGER,\n \t\t\t.short_name = 'n',\n \t\t\t.value = &filter.lines,\n+\t\t\t.precision = sizeof(filter.lines),\n \t\t\t.argh = N_(\"n\"),\n \t\t\t.help = N_(\"print <n> lines of each tag message\"),\n \t\t\t.flags = PARSE_OPT_OPTARG,\ndiff --git a/parse-options.c b/parse-options.c\nindex e8c08e55e02..2cb9bd3b5b9 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -172,33 +172,56 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\treturn (*opt->ll_callback)(p, opt, p_arg, p_unset);\n \t}\n \tcase OPTION_INTEGER:\n+\t{\n+\t\tintmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision);\n+\t\tintmax_t lower_bound = -upper_bound - 1;\n+\t\tintmax_t value;\n+\n \t\tif (unset) {\n-\t\t\t*(int *)opt->value = 0;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n-\t\t\t*(int *)opt->value = opt->defval;\n-\t\t\treturn 0;\n-\t\t}\n-\t\tif (get_arg(p, opt, flags, &arg))\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n \t\t\treturn -1;\n-\t\tif (!*arg)\n+\t\t} else if (!*arg) {\n \t\t\treturn error(_(\"%s expects a numerical value\"),\n \t\t\t\t     optname(opt, flags));\n+\t\t} else {\n+\t\t\terrno = 0;\n+\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\t\t\tif (errno == ERANGE)\n+\t\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n+\t\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n+\t\t\tif (errno)\n+\t\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n+\t\t\t\t\t\t   arg, optname(opt, flags));\n+\t\t}\n \n-\t\terrno = 0;\n-\t\t*(int *)opt->value = strtol(arg, (char **)&s, 10);\n-\t\tif (*s)\n-\t\t\treturn error(_(\"%s expects a numerical value\"),\n-\t\t\t\t     optname(opt, flags));\n-\t\tif (errno == ERANGE)\n+\t\tif (value < lower_bound || value > upper_bound)\n \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n-\t\t\t\t     arg, optname(opt, flags), (intmax_t)LONG_MIN, (intmax_t)LONG_MAX);\n-\t\tif (errno)\n-\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n-\t\t\t\t\t   arg, optname(opt, flags));\n+\t\t\t\t     arg, optname(opt, flags), lower_bound, upper_bound);\n \n-\t\treturn 0;\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(int8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(int16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(int32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(int64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t\tif (unset) {\n \t\t\t*(unsigned long *)opt->value = 0;\ndiff --git a/parse-options.h b/parse-options.h\nindex 997ffbee805..8db96402c4d 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n  * `value`::\n  *   stores pointers to the values to be filled.\n  *\n+ * `precision`::\n+ *   precision of the integer pointed to by `value` in number of bytes. Should\n+ *   typically be its `sizeof()`.\n+ *\n  * `argh`::\n  *   token to explain the kind of argument this option wants. Does not\n  *   begin in capital letter, and does not end with a full stop.\n@@ -151,6 +155,7 @@ struct option {\n \tint short_name;\n \tconst char *long_name;\n \tvoid *value;\n+\tsize_t precision;\n \tconst char *argh;\n \tconst char *help;\n \n@@ -214,6 +219,7 @@ struct option {\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n \t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n \t.flags = (f), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 997f55fd45b..b1275dfade4 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,6 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tint16_t i16 = 0;\n \n \tstruct option options[] = {\n \t\tOPT_BOOL(0, \"yes\", &boolean, \"get a boolean\"),\n@@ -139,6 +140,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_NEGBIT(0, \"neg-or4\", &boolean, \"same as --no-or4\", 4),\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n+\t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n@@ -210,6 +212,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t}\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n+\tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\n \tshow(&expect, &ret, \"string: %s\", string ? string : \"(not set)\");\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 5eb1feb61b4..95951436cda 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -22,6 +22,7 @@ usage: test-tool parse-options <options>\n \n     -i, --[no-]integer <n>\n                           get a integer\n+    --[no-]i16 <n>        get a 16 bit integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --[no-]set23          set integer to 23\n@@ -136,6 +137,7 @@ test_expect_success 'OPT_MAGNITUDE() 3giga' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 0\n magnitude: 16384\n timestamp: 0\n string: 123\n@@ -156,6 +158,7 @@ test_expect_success 'short options' '\n cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n+i16: 9000\n magnitude: 16384\n timestamp: 0\n string: 321\n@@ -167,7 +170,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n \t\t--boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -179,6 +182,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tcat >expect <<-EOF &&\n \tboolean: 0\n \tinteger: 0\n+\ti16: 0\n \tmagnitude: 0\n \ttimestamp: 0\n \tstring: (not set)\n@@ -253,6 +257,7 @@ test_expect_success 'superfluous value provided: cmdmode' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 13\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: 123\n@@ -276,6 +281,7 @@ test_expect_success 'intermingled arguments' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 2\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -343,6 +349,7 @@ cat >expect <<\\EOF\n Callback: \"four\", 0\n boolean: 5\n integer: 4\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -368,6 +375,7 @@ test_expect_success 'OPT_CALLBACK() and callback errors work' '\n cat >expect <<\\EOF\n boolean: 1\n integer: 23\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -447,6 +455,7 @@ test_expect_success 'OPT_NUMBER_CALLBACK() works' '\n cat >expect <<\\EOF\n boolean: 0\n integer: 0\n+i16: 0\n magnitude: 0\n timestamp: 0\n string: (not set)\n@@ -789,4 +798,16 @@ test_expect_success 'overflowing integer' '\n \ttest_must_be_empty out\n '\n \n+test_expect_success 'i16 limits range' '\n+\ttest-tool parse-options --i16 32767 >out &&\n+\ttest_grep \"i16: 32767\" out &&\n+\ttest_must_fail test-tool parse-options --i16 32768 2>err &&\n+\ttest_grep \"value 32768 for option .i16. not in range \\[-32768,32767\\]\" err &&\n+\n+\ttest-tool parse-options --i16 -32768 >out &&\n+\ttest_grep \"i16: -32768\" out &&\n+\ttest_must_fail test-tool parse-options --i16 -32769 2>err &&\n+\ttest_grep \"value -32769 for option .i16. not in range \\[-32768,32767\\]\" err\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516269","messageId":"20250416-b4-pks-parse-options-integers-v3-5-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:14Z","receivedAt":"2025-04-16T10:02:31Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"We have two generic ways to parse integers in the \"parse-options\"\nsubsystem:\n\n  - `OPTION_INTEGER` parses a signed integer.\n\n  - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n    interprets suffixes like \"k\" or \"g\".\n\nNotably missing is a middle ground that parses unsigned integers without\ninterpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\nplug this gap. This option type will be used in subsequent commits.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 54 +++++++++++++++++++++++++++++++++++++++++++\n parse-options.h               | 12 ++++++++++\n t/helper/test-parse-options.c |  4 +++-\n t/t0040-parse-options.sh      | 24 ++++++++++++++++++-\n 4 files changed, 92 insertions(+), 2 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex 259716efb17..e4dc22464b2 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -222,6 +222,60 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\t    optname(opt, flags));\n \t\t}\n \t}\n+\tcase OPTION_UNSIGNED:\n+\t{\n+\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tuintmax_t value;\n+\n+\t\tif (unset) {\n+\t\t\tvalue = 0;\n+\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n+\t\t\tvalue = opt->defval;\n+\t\t} else if (get_arg(p, opt, flags, &arg)) {\n+\t\t\treturn -1;\n+\t\t} else if (!*arg) {\n+\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t     optname(opt, flags));\n+\t\t} else if (*arg == '-') {\n+\t\t\treturn error(_(\"%s does not accept negative values\"),\n+\t\t\t\t     optname(opt, flags));\n+\t\t} else {\n+\t\t\terrno = 0;\n+\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n+\t\t\tif (*s)\n+\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n+\t\t\t\t\t     optname(opt, flags));\n+\t\t\tif (errno == ERANGE)\n+\t\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n+\t\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\t\t\tif (errno)\n+\t\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n+\t\t\t\t\t\t   arg, optname(opt, flags));\n+\n+\t\t}\n+\n+\t\tif (value > upper_bound)\n+\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n+\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\n+\t\tswitch (opt->precision) {\n+\t\tcase 1:\n+\t\t\t*(uint8_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 2:\n+\t\t\t*(uint16_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 4:\n+\t\t\t*(uint32_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tcase 8:\n+\t\t\t*(uint64_t *)opt->value = value;\n+\t\t\treturn 0;\n+\t\tdefault:\n+\t\t\tBUG(\"invalid precision for option %s\",\n+\t\t\t    optname(opt, flags));\n+\t\t}\n+\t}\n \tcase OPTION_MAGNITUDE:\n \t{\n \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\ndiff --git a/parse-options.h b/parse-options.h\nindex 55c42faa29f..aa37134dc72 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -25,6 +25,7 @@ enum parse_opt_type {\n \t/* options with arguments (usually) */\n \tOPTION_STRING,\n \tOPTION_INTEGER,\n+\tOPTION_UNSIGNED,\n \tOPTION_MAGNITUDE,\n \tOPTION_CALLBACK,\n \tOPTION_LOWLEVEL_CALLBACK,\n@@ -224,6 +225,16 @@ struct option {\n \t.help = (h), \\\n \t.flags = (f), \\\n }\n+#define OPT_UNSIGNED_F(s, l, v, h, f) { \\\n+\t.type = OPTION_UNSIGNED, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v), \\\n+\t.precision = sizeof(*v), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = (f), \\\n+}\n \n #define OPT_END() { \\\n \t.type = OPTION_END, \\\n@@ -276,6 +287,7 @@ struct option {\n #define OPT_CMDMODE(s, l, v, h, i)  OPT_CMDMODE_F(s, l, v, h, i, 0)\n \n #define OPT_INTEGER(s, l, v, h)     OPT_INTEGER_F(s, l, v, h, 0)\n+#define OPT_UNSIGNED(s, l, v, h)    OPT_UNSIGNED_F(s, l, v, h, 0)\n #define OPT_MAGNITUDE(s, l, v, h) { \\\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 46deb4317ef..0d559288d9c 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,7 +120,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n-\tuint16_t m16 = 0;\n+\tuint16_t m16 = 0, u16 = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -142,6 +142,7 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n+\t\tOPT_UNSIGNED(0, \"u16\", &u16, \"get a 16 bit unsigned integer\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n@@ -215,6 +216,7 @@ int cmd__parse_options(int argc, const char **argv)\n \tshow(&expect, &ret, \"boolean: %d\", boolean);\n \tshow(&expect, &ret, \"integer: %d\", integer);\n \tshow(&expect, &ret, \"i16: %\"PRIdMAX, (intmax_t) i16);\n+\tshow(&expect, &ret, \"u16: %\"PRIuMAX, (uintmax_t) u16);\n \tshow(&expect, &ret, \"magnitude: %lu\", magnitude);\n \tshow(&expect, &ret, \"m16: %\"PRIuMAX, (uintmax_t) m16);\n \tshow(&expect, &ret, \"timestamp: %\"PRItime, timestamp);\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 8daaf568485..66875ce0586 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -23,6 +23,7 @@ usage: test-tool parse-options <options>\n     -i, --[no-]integer <n>\n                           get a integer\n     --[no-]i16 <n>        get a 16 bit integer\n+    --[no-]u16 <n>        get a 16 bit unsigned integer\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --m16 <n>             get a 16 bit magnitude\n@@ -139,6 +140,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 0\n+u16: 0\n magnitude: 16384\n m16: 0\n timestamp: 0\n@@ -161,6 +163,7 @@ cat >expect <<\\EOF\n boolean: 2\n integer: 1729\n i16: 9000\n+u16: 5432\n magnitude: 16384\n m16: 32768\n timestamp: 0\n@@ -173,7 +176,7 @@ file: prefix/fi.le\n EOF\n \n test_expect_success 'long options' '\n-\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --magnitude 16k \\\n+\ttest-tool parse-options --boolean --integer 1729 --i16 9000 --u16 5432 --magnitude 16k \\\n \t\t--m16 32k --boolean --string2=321 --verbose --verbose --no-dry-run \\\n \t\t--abbrev=10 --file fi.le --obsolete \\\n \t\t>output 2>output.err &&\n@@ -186,6 +189,7 @@ test_expect_success 'abbreviate to something longer than SHA1 length' '\n \tboolean: 0\n \tinteger: 0\n \ti16: 0\n+\tu16: 0\n \tmagnitude: 0\n \tm16: 0\n \ttimestamp: 0\n@@ -262,6 +266,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 13\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -287,6 +292,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 2\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -356,6 +362,7 @@ Callback: \"four\", 0\n boolean: 5\n integer: 4\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -383,6 +390,7 @@ cat >expect <<\\EOF\n boolean: 1\n integer: 23\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -464,6 +472,7 @@ cat >expect <<\\EOF\n boolean: 0\n integer: 0\n i16: 0\n+u16: 0\n magnitude: 0\n m16: 0\n timestamp: 0\n@@ -826,4 +835,17 @@ test_expect_success 'm16 limits range' '\n \ttest_grep \"value 65536 for option .m16. not in range \\[0,65535\\]\" err\n '\n \n+test_expect_success 'u16 limits range' '\n+\ttest-tool parse-options --u16 65535 >out &&\n+\ttest_grep \"u16: 65535\" out &&\n+\ttest_must_fail test-tool parse-options --u16 65536 2>err &&\n+\ttest_grep \"value 65536 for option .u16. not in range \\[0,65535\\]\" err\n+'\n+\n+test_expect_success 'u16 does not accept negative value' '\n+\ttest_must_fail test-tool parse-options --u16 -1 >out 2>err &&\n+\ttest_grep \"option .u16. does not accept negative values\" err &&\n+\ttest_must_be_empty out\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516270","messageId":"20250416-b4-pks-parse-options-integers-v3-7-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 7/7] parse-options: introduce bounded integer options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:16Z","receivedAt":"2025-04-16T10:02:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"In the preceding commits we have introduced integer precisions. The\nprecision merely tracks bounds of the underlying data types so that we\ndon't try to for example write a `size_t` into an `unsigned`, which\ncould otherwise cause out-of-bounds writes.\n\nSome options may have bounds that are stricter than the underlying data\ntype. Right now, users of any such options would have to manually verify\nthat the value passed to such an option is inside the expected bounds.\nThis is rather tedious, and it leads to code duplication across sites\nthat wish to perform such bounds checks.\n\nIntroduce `OPT_*_BOUNDED()` options that alleviate this issue. Users\ncan optionally specify both a lower and upper bound, and if set we will\nverify that the value passed by the user is in that range.\n\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n parse-options.c               | 40 ++++++++++++++++++++++++++++-----\n parse-options.h               | 52 +++++++++++++++++++++++++++++++++++++++++++\n t/helper/test-parse-options.c |  5 +++++\n t/t0040-parse-options.sh      | 33 +++++++++++++++++++++++++++\n 4 files changed, 125 insertions(+), 5 deletions(-)\n\ndiff --git a/parse-options.c b/parse-options.c\nindex e4dc22464b2..d1dffcfdf5f 100644\n--- a/parse-options.c\n+++ b/parse-options.c\n@@ -177,6 +177,20 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\tintmax_t lower_bound = -upper_bound - 1;\n \t\tintmax_t value;\n \n+\t\tif (opt->lower_bound) {\n+\t\t\tif (opt->lower_bound < lower_bound)\n+\t\t\t\tBUG(\"invalid lower bound for option %s\", optname(opt, flags));\n+\t\t\tif (opt->lower_bound > lower_bound)\n+\t\t\t\tlower_bound = opt->lower_bound;\n+\t\t}\n+\n+\t\tif (opt->upper_bound) {\n+\t\t\tif (opt->upper_bound > (uintmax_t)upper_bound)\n+\t\t\t\tBUG(\"invalid upper bound for option %s\", optname(opt, flags));\n+\t\t\tif (opt->upper_bound < (uintmax_t)upper_bound)\n+\t\t\t\tupper_bound = opt->upper_bound;\n+\t\t}\n+\n \t\tif (unset) {\n \t\t\tvalue = 0;\n \t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n@@ -225,8 +239,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \tcase OPTION_UNSIGNED:\n \t{\n \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tuintmax_t lower_bound = 0;\n \t\tuintmax_t value;\n \n+\t\tif (opt->lower_bound < 0)\n+\t\t\tBUG(\"invalid lower bound for option %s\", optname(opt, flags));\n+\t\tif (opt->lower_bound > 0)\n+\t\t\tlower_bound = opt->lower_bound;\n+\t\tif (opt->upper_bound && opt->upper_bound < upper_bound)\n+\t\t\tupper_bound = opt->upper_bound;\n+\n \t\tif (unset) {\n \t\t\tvalue = 0;\n \t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n@@ -247,16 +269,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\t\t\t     optname(opt, flags));\n \t\t\tif (errno == ERANGE)\n \t\t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n-\t\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\t\t\t\t\t     arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound);\n \t\t\tif (errno)\n \t\t\t\treturn error_errno(_(\"value %s for %s cannot be parsed\"),\n \t\t\t\t\t\t   arg, optname(opt, flags));\n \n \t\t}\n \n-\t\tif (value > upper_bound)\n+\t\tif (value < lower_bound || value > upper_bound)\n \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n-\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\t\t\t\t     arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound);\n \n \t\tswitch (opt->precision) {\n \t\tcase 1:\n@@ -279,8 +301,16 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \tcase OPTION_MAGNITUDE:\n \t{\n \t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n+\t\tuintmax_t lower_bound = 0;\n \t\tunsigned long value;\n \n+\t\tif (opt->lower_bound < 0)\n+\t\t\tBUG(\"invalid lower bound for option %s\", optname(opt, flags));\n+\t\tif (opt->lower_bound > 0)\n+\t\t\tlower_bound = opt->lower_bound;\n+\t\tif (opt->upper_bound && opt->upper_bound < upper_bound)\n+\t\t\tupper_bound = opt->upper_bound;\n+\n \t\tif (unset) {\n \t\t\tvalue = 0;\n \t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n@@ -293,9 +323,9 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n \t\t\t\t     optname(opt, flags));\n \t\t}\n \n-\t\tif (value > upper_bound)\n+\t\tif (value < lower_bound || value > upper_bound)\n \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIuMAX\",%\"PRIuMAX\"]\"),\n-\t\t\t\t     arg, optname(opt, flags), (uintmax_t)0, (uintmax_t)upper_bound);\n+\t\t\t\t     arg, optname(opt, flags), (uintmax_t)lower_bound, (uintmax_t)upper_bound);\n \n \t\tswitch (opt->precision) {\n \t\tcase 1:\ndiff --git a/parse-options.h b/parse-options.h\nindex 168df642386..c1ebdaf7639 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -97,6 +97,13 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n  *   precision of the integer pointed to by `value` in number of bytes. Should\n  *   typically be its `sizeof()`.\n  *\n+ * `lower_bound`,`upper_bound`::\n+ *   lower and upper bound of the integer to further restrict the accepted\n+ *   range of integer values. `0` will use the minimum and maximum values for\n+ *   the integer type of the specified precision. Specifying a bound that does\n+ *   not fit into an integer type of the specified precision will trigger a\n+ *   bug.\n+ *\n  * `argh`::\n  *   token to explain the kind of argument this option wants. Does not\n  *   begin in capital letter, and does not end with a full stop.\n@@ -157,6 +164,8 @@ struct option {\n \tconst char *long_name;\n \tvoid *value;\n \tsize_t precision;\n+\tintmax_t lower_bound;\n+\tuintmax_t upper_bound;\n \tconst char *argh;\n \tconst char *help;\n \n@@ -225,6 +234,19 @@ struct option {\n \t.help = (h), \\\n \t.flags = (f), \\\n }\n+#define OPT_INTEGER_BOUNDED_F(s, l, v, lower, upper, h, f) { \\\n+\t.type = OPTION_INTEGER, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v) + BARF_UNLESS_SIGNED(*(v)), \\\n+\t.precision = sizeof(*v), \\\n+\t.lower_bound = (lower), \\\n+\t.upper_bound = (upper), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = (f), \\\n+}\n+\n #define OPT_UNSIGNED_F(s, l, v, h, f) { \\\n \t.type = OPTION_UNSIGNED, \\\n \t.short_name = (s), \\\n@@ -235,6 +257,18 @@ struct option {\n \t.help = (h), \\\n \t.flags = (f), \\\n }\n+#define OPT_UNSIGNED_BOUNDED_F(s, l, v, lower, upper, h, f) { \\\n+\t.type = OPTION_UNSIGNED, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n+\t.precision = sizeof(*v), \\\n+\t.lower_bound = (lower), \\\n+\t.upper_bound = (upper), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = (f), \\\n+}\n \n #define OPT_END() { \\\n \t.type = OPTION_END, \\\n@@ -287,7 +321,12 @@ struct option {\n #define OPT_CMDMODE(s, l, v, h, i)  OPT_CMDMODE_F(s, l, v, h, i, 0)\n \n #define OPT_INTEGER(s, l, v, h)     OPT_INTEGER_F(s, l, v, h, 0)\n+#define OPT_INTEGER_BOUNDED(s, l, v, lower, upper, h) \\\n+\tOPT_INTEGER_BOUNDED_F(s, l, v, lower, upper, h, 0)\n #define OPT_UNSIGNED(s, l, v, h)    OPT_UNSIGNED_F(s, l, v, h, 0)\n+#define OPT_UNSIGNED_BOUNDED(s, l, v, lower, upper, h) \\\n+\tOPT_UNSIGNED_BOUNDED_F(s, l, v, lower, upper, h, 0)\n+\n #define OPT_MAGNITUDE(s, l, v, h) { \\\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\n@@ -298,6 +337,19 @@ struct option {\n \t.help = (h), \\\n \t.flags = PARSE_OPT_NONEG, \\\n }\n+#define OPT_MAGNITUDE_BOUNDED(s, l, v, lower, upper, h) { \\\n+\t.type = OPTION_MAGNITUDE, \\\n+\t.short_name = (s), \\\n+\t.long_name = (l), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n+\t.precision = sizeof(*v), \\\n+\t.lower_bound = (lower), \\\n+\t.upper_bound = (upper), \\\n+\t.argh = N_(\"n\"), \\\n+\t.help = (h), \\\n+\t.flags = PARSE_OPT_NONEG, \\\n+}\n+\n #define OPT_STRING(s, l, v, a, h)   OPT_STRING_F(s, l, v, a, h, 0)\n #define OPT_STRING_LIST(s, l, v, a, h) { \\\n \t.type = OPTION_CALLBACK, \\\ndiff --git a/t/helper/test-parse-options.c b/t/helper/test-parse-options.c\nindex 0d559288d9c..0fcceec56a7 100644\n--- a/t/helper/test-parse-options.c\n+++ b/t/helper/test-parse-options.c\n@@ -120,7 +120,9 @@ int cmd__parse_options(int argc, const char **argv)\n \t};\n \tstruct string_list expect = STRING_LIST_INIT_NODUP;\n \tstruct string_list list = STRING_LIST_INIT_NODUP;\n+\tuint32_t mbounded = 0, ubounded = 0;\n \tuint16_t m16 = 0, u16 = 0;\n+\tint32_t ibounded = 0;\n \tint16_t i16 = 0;\n \n \tstruct option options[] = {\n@@ -142,10 +144,13 @@ int cmd__parse_options(int argc, const char **argv)\n \t\tOPT_GROUP(\"\"),\n \t\tOPT_INTEGER('i', \"integer\", &integer, \"get a integer\"),\n \t\tOPT_INTEGER(0, \"i16\", &i16, \"get a 16 bit integer\"),\n+\t\tOPT_INTEGER_BOUNDED(0, \"ibounded\", &ibounded, -10, 10, \"get a bounded integer between [-10,10]\"),\n \t\tOPT_UNSIGNED(0, \"u16\", &u16, \"get a 16 bit unsigned integer\"),\n+\t\tOPT_UNSIGNED_BOUNDED(0, \"ubounded\", &ubounded, 10, 100, \"get a bounded unsigned integer between [10,100]\"),\n \t\tOPT_INTEGER('j', NULL, &integer, \"get a integer, too\"),\n \t\tOPT_MAGNITUDE('m', \"magnitude\", &magnitude, \"get a magnitude\"),\n \t\tOPT_MAGNITUDE(0, \"m16\", &m16, \"get a 16 bit magnitude\"),\n+\t\tOPT_MAGNITUDE_BOUNDED(0, \"mbounded\", &mbounded, 10, 100, \"get a bounded magnitude between [10,100]\"),\n \t\tOPT_SET_INT(0, \"set23\", &integer, \"set integer to 23\", 23),\n \t\tOPT_CMDMODE(0, \"mode1\", &integer, \"set integer to 1 (cmdmode option)\", 1),\n \t\tOPT_CMDMODE(0, \"mode2\", &integer, \"set integer to 2 (cmdmode option)\", 2),\ndiff --git a/t/t0040-parse-options.sh b/t/t0040-parse-options.sh\nindex 66875ce0586..d76165c2053 100755\n--- a/t/t0040-parse-options.sh\n+++ b/t/t0040-parse-options.sh\n@@ -23,10 +23,13 @@ usage: test-tool parse-options <options>\n     -i, --[no-]integer <n>\n                           get a integer\n     --[no-]i16 <n>        get a 16 bit integer\n+    --[no-]ibounded <n>   get a bounded integer between [-10,10]\n     --[no-]u16 <n>        get a 16 bit unsigned integer\n+    --[no-]ubounded <n>   get a bounded unsigned integer between [10,100]\n     -j <n>                get a integer, too\n     -m, --magnitude <n>   get a magnitude\n     --m16 <n>             get a 16 bit magnitude\n+    --mbounded <n>        get a bounded magnitude between [10,100]\n     --[no-]set23          set integer to 23\n     --mode1               set integer to 1 (cmdmode option)\n     --mode2               set integer to 2 (cmdmode option)\n@@ -848,4 +851,34 @@ test_expect_success 'u16 does not accept negative value' '\n \ttest_must_be_empty out\n '\n \n+test_expect_success 'ibounded does not accept outside range' '\n+\ttest_must_fail test-tool parse-options --ibounded -11 >out 2>err &&\n+\ttest_grep \"value -11 for option .ibounded. not in range \\[-10,10\\]\" err &&\n+\ttest_must_fail test-tool parse-options --ibounded 11 >out 2>err &&\n+\ttest_grep \"value 11 for option .ibounded. not in range \\[-10,10\\]\" err &&\n+\ttest-tool parse-options --ibounded -10 &&\n+\ttest-tool parse-options --ibounded 0 &&\n+\ttest-tool parse-options --ibounded 10\n+'\n+\n+test_expect_success 'ubounded does not accept outside range' '\n+\ttest_must_fail test-tool parse-options --ubounded 9 >out 2>err &&\n+\ttest_grep \"value 9 for option .ubounded. not in range \\[10,100\\]\" err &&\n+\ttest_must_fail test-tool parse-options --ubounded 101 >out 2>err &&\n+\ttest_grep \"value 101 for option .ubounded. not in range \\[10,100\\]\" err &&\n+\ttest-tool parse-options --ubounded 10 &&\n+\ttest-tool parse-options --ubounded 50 &&\n+\ttest-tool parse-options --ubounded 100\n+'\n+\n+test_expect_success 'mbounded does not accept outside range' '\n+\ttest_must_fail test-tool parse-options --mbounded 9 >out 2>err &&\n+\ttest_grep \"value 9 for option .mbounded. not in range \\[10,100\\]\" err &&\n+\ttest_must_fail test-tool parse-options --mbounded 101 >out 2>err &&\n+\ttest_grep \"value 101 for option .mbounded. not in range \\[10,100\\]\" err &&\n+\ttest-tool parse-options --mbounded 10 &&\n+\ttest-tool parse-options --mbounded 50 &&\n+\ttest-tool parse-options --mbounded 100\n+'\n+\n test_done\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516271","messageId":"20250416-b4-pks-parse-options-integers-v3-6-d390746bea79@pks.im","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-0-d390746bea79@pks.im","subject":"[PATCH v3 6/7] parse-options: detect mismatches in integer signedness","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:02:15Z","receivedAt":"2025-04-16T10:02:32Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"It was reported that \"t5620-backfill.sh\" fails on s390x and sparc64 in a\ntest that exercises the \"--min-batch-size\" command line option. The\nsymptom was that the option didn't seem to have an effect: we didn't\nfetch objects with a batch size of 20, but instead fetched all objects\nat once.\n\nAs it turns out, the root cause is that `--min-batch-size` uses\n`OPT_INTEGER()` to parse the command line option. While this macro\nexpects the caller to pass a pointer to an integer, we instead pass a\npointer to a `size_t`. This coincidentally works on most platforms, but\nit breaks apart on the mentioned platforms because they are big endian.\n\nThis issue isn't specific to git-backfill(1): there are a couple of\nother places where we have the same type confusion going on. This\nindicates that the issue really is the interface that the parse-options\nsubsystem provides -- it is simply too easy to get this wrong as there\nisn't any kind of compiler warning, and things just work on the most\ncommon systems.\n\nAddress the systemic issue by introducing two new build asserts\n`BARF_UNLESS_SIGNED()` and `BARF_UNLESS_UNSIGNED()`. As the names\nalready hint at, those macros will cause a compiler error when passed a\nvalue that is not signed or unsigned, respectively.\n\nAdapt `OPT_INTEGER()`, `OPT_UNSIGNED()` as well as `OPT_MAGNITUDE()` to\nuse those asserts. This uncovers a small set of sites where we indeed\nhave the same bug as in git-backfill(1). Adapt all of them to use the\ncorrect option.\n\nReported-by: Todd Zullinger <tmz@pobox.com>\nReported-by: John Paul Adrian Glaubitz <glaubitz@physik.fu-berlin.de>\nHelped-by: SZEDER Gábor <szeder.dev@gmail.com>\nHelped-by: Jeff King <peff@peff.net>\nSigned-off-by: Patrick Steinhardt <ps@pks.im>\n---\n apply.c            | 4 ++--\n builtin/backfill.c | 4 ++--\n builtin/column.c   | 2 +-\n builtin/grep.c     | 4 ++--\n git-compat-util.h  | 7 +++++++\n parse-options.h    | 6 +++---\n 6 files changed, 17 insertions(+), 10 deletions(-)\n\ndiff --git a/apply.c b/apply.c\nindex f274a379487..a850c7d75fe 100644\n--- a/apply.c\n+++ b/apply.c\n@@ -5123,8 +5123,8 @@ int apply_parse_options(int argc, const char **argv,\n \t\t/* Think twice before adding \"--nul\" synonym to this */\n \t\tOPT_SET_INT('z', NULL, &state->line_termination,\n \t\t\tN_(\"paths are separated with NUL character\"), '\\0'),\n-\t\tOPT_INTEGER('C', NULL, &state->p_context,\n-\t\t\t\tN_(\"ensure at least <n> lines of context match\")),\n+\t\tOPT_UNSIGNED('C', NULL, &state->p_context,\n+\t\t\t     N_(\"ensure at least <n> lines of context match\")),\n \t\tOPT_CALLBACK(0, \"whitespace\", state, N_(\"action\"),\n \t\t\tN_(\"detect new or modified lines that have whitespace errors\"),\n \t\t\tapply_option_parse_whitespace),\ndiff --git a/builtin/backfill.c b/builtin/backfill.c\nindex 33e1ea2f84f..d95d7a2d4d6 100644\n--- a/builtin/backfill.c\n+++ b/builtin/backfill.c\n@@ -123,8 +123,8 @@ int cmd_backfill(int argc, const char **argv, const char *prefix, struct reposit\n \t\t.sparse = 0,\n \t};\n \tstruct option options[] = {\n-\t\tOPT_INTEGER(0, \"min-batch-size\", &ctx.min_batch_size,\n-\t\t\t    N_(\"Minimum number of objects to request at a time\")),\n+\t\tOPT_UNSIGNED(0, \"min-batch-size\", &ctx.min_batch_size,\n+\t\t\t     N_(\"Minimum number of objects to request at a time\")),\n \t\tOPT_BOOL(0, \"sparse\", &ctx.sparse,\n \t\t\t N_(\"Restrict the missing objects to the current sparse-checkout\")),\n \t\tOPT_END(),\ndiff --git a/builtin/column.c b/builtin/column.c\nindex 50314cc2559..ce6443d5fac 100644\n--- a/builtin/column.c\n+++ b/builtin/column.c\n@@ -31,7 +31,7 @@ int cmd_column(int argc,\n \tstruct option options[] = {\n \t\tOPT_STRING(0, \"command\", &real_command, N_(\"name\"), N_(\"lookup config vars\")),\n \t\tOPT_COLUMN(0, \"mode\", &colopts, N_(\"layout to use\")),\n-\t\tOPT_INTEGER(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n+\t\tOPT_UNSIGNED(0, \"raw-mode\", &colopts, N_(\"layout to use\")),\n \t\tOPT_INTEGER(0, \"width\", &copts.width, N_(\"maximum width\")),\n \t\tOPT_STRING(0, \"indent\", &copts.indent, N_(\"string\"), N_(\"padding space on left border\")),\n \t\tOPT_STRING(0, \"nl\", &copts.nl, N_(\"string\"), N_(\"padding space on right border\")),\ndiff --git a/builtin/grep.c b/builtin/grep.c\nindex c4869733e1b..f23a6f1dc86 100644\n--- a/builtin/grep.c\n+++ b/builtin/grep.c\n@@ -983,9 +983,9 @@ int cmd_grep(int argc,\n \t\tOPT_CALLBACK('C', \"context\", &opt, N_(\"n\"),\n \t\t\tN_(\"show <n> context lines before and after matches\"),\n \t\t\tcontext_callback),\n-\t\tOPT_INTEGER('B', \"before-context\", &opt.pre_context,\n+\t\tOPT_UNSIGNED('B', \"before-context\", &opt.pre_context,\n \t\t\tN_(\"show <n> context lines before matches\")),\n-\t\tOPT_INTEGER('A', \"after-context\", &opt.post_context,\n+\t\tOPT_UNSIGNED('A', \"after-context\", &opt.post_context,\n \t\t\tN_(\"show <n> context lines after matches\")),\n \t\tOPT_INTEGER(0, \"threads\", &num_threads,\n \t\t\tN_(\"use <n> worker threads\")),\ndiff --git a/git-compat-util.h b/git-compat-util.h\nindex cf733b38acd..1218fcf81a4 100644\n--- a/git-compat-util.h\n+++ b/git-compat-util.h\n@@ -110,12 +110,19 @@ DISABLE_WARNING(-Wsign-compare)\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(__builtin_types_compatible_p(__typeof__(*(dst)), \\\n \t\t\t\t\t\t\t  __typeof__(*(src))))\n+\n+# define BARF_UNLESS_SIGNED(var)   BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) < 0)\n+# define BARF_UNLESS_UNSIGNED(var) BUILD_ASSERT_OR_ZERO(((__typeof__(var)) -1) > 0)\n #else\n # define BARF_UNLESS_AN_ARRAY(arr) 0\n # define BARF_UNLESS_COPYABLE(dst, src) \\\n \tBUILD_ASSERT_OR_ZERO(0 ? ((*(dst) = *(src)), 0) : \\\n \t\t\t\t sizeof(*(dst)) == sizeof(*(src)))\n+\n+# define BARF_UNLESS_SIGNED(var)   0\n+# define BARF_UNLESS_UNSIGNED(var) 0\n #endif\n+\n /*\n  * ARRAY_SIZE - get the number of elements in a visible array\n  * @x: the array whose size you want.\ndiff --git a/parse-options.h b/parse-options.h\nindex aa37134dc72..168df642386 100644\n--- a/parse-options.h\n+++ b/parse-options.h\n@@ -219,7 +219,7 @@ struct option {\n \t.type = OPTION_INTEGER, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_SIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -229,7 +229,7 @@ struct option {\n \t.type = OPTION_UNSIGNED, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n@@ -292,7 +292,7 @@ struct option {\n \t.type = OPTION_MAGNITUDE, \\\n \t.short_name = (s), \\\n \t.long_name = (l), \\\n-\t.value = (v), \\\n+\t.value = (v) + BARF_UNLESS_UNSIGNED(*(v)), \\\n \t.precision = sizeof(*v), \\\n \t.argh = N_(\"n\"), \\\n \t.help = (h), \\\n\n-- \n2.49.0.805.g082f7c87e0.dirty\n\n"},{"id":"516272","messageId":"Z_-GKX8uFSNib5rW@pks.im","threadId":"63234","inReplyTo":"35300426-32cc-4c0e-b0c3-edb2b2ed312f@gmail.com","subject":"Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:27:53Z","receivedAt":"2025-04-16T10:28:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 15, 2025 at 04:52:02PM +0100, Phillip Wood wrote:\n> Hi Patrick\n> \n> On 15/04/2025 13:14, Patrick Steinhardt wrote:\n> > We have two generic ways to parse integers in the \"parse-options\"\n> > subsytem:\n> > \n> >    - `OPTION_INTEGER` parses a signed integer.\n> > \n> >    - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n> >      interprets suffixes like \"k\" or \"g\".\n> > \n> > Notably missing is a middle ground that parses unsigned integers without\n> > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\n> > plug this gap. This option type will be used in subsequent commits.\n> \n> I think this is a useful addition. I wonder about the way it handles\n> negative values though. For types narrower than uintmax_t \"-1\" will be\n> rejected but large negative values that parse as small positive numbers will\n> be accepted. Perhaps we should explicitly reject strings starting with \"-\"\n> as we do in git_parse_ulong() which is used by OPTION_MAGNITUDE.\n\nWait, does it? Why would `strtoul()` or any of its variants ever accept\na string prefixed with a \"-\"?\n\n    If the minus sign was part of the input sequence, the numeric value\n    calculated from the sequence of digits is negated as if by unary\n    minus in the result type, which applies unsigned integer wraparound\n    rules.\n\nOh dear... all these integer conversion functions are really a gift that\nkeeps on giving. Gross.\n\n> This patch also needs the fix from patch 2 to detect overflows for\n> uintmax_t.\n\nYup, will add.\n\nPatrick\n"},{"id":"516273","messageId":"Z_-GMXZmlfQmSuy6@pks.im","threadId":"63234","inReplyTo":"xmqqecxtmkdb.fsf@gitster.g","subject":"Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:28:01Z","receivedAt":"2025-04-16T10:28:06Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 15, 2025 at 09:59:12AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > diff --git a/parse-options.h b/parse-options.h\n> > index 997ffbee805..8d5f9c95f9c 100644\n> > --- a/parse-options.h\n> > +++ b/parse-options.h\n> > @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n> >   * `value`::\n> >   *   stores pointers to the values to be filled.\n> >   *\n> > + * `precision`::\n> > + *   precision of the integer pointed to by `value`. Should typically be its\n> > + *   `sizeof()`.\n> \n> The fact of the integer allowing to store up to 16-bit vs 32-bit, is\n> that \"precision\"?  \"My --size option runs up to 200,000, what value\n> should I set it to?\" is a natural question the readers of this\n> sentence would have in their mind, if we call it \"range\" or\n> something (which might not be a bad thing to have, but that is\n> totally outside the theme of this topic).\n> \n> In any case, include a phrase \"number of bytes\" somewhere in the\n> description to make it clear what unit we are counting.\n\nMakes sense, will dot.\n\n> Are their common use case where this number is *not* its sizeof()\n> already in the codebase?\n\nYeah, this is something that I thought about quite a lot. I think the\n\"precision\" and \"range\" are somewhat orthogonal to one another:\n\n  - The range specifies which values are accepted.\n\n  - The precision specifies the underlying width of the type.\n\nRight now I only handle the latter, but the next step here would be to\nalso start handling the former. But even if we also started to handle\nthe range there is merit in tracking both, as there will be usecases\nwhere the range would fit into a smaller underlying type, but due to\nother reasons one still wants to use the bigger underlying type.\n\nThe next step would thus be to introduce such an optional range so that\noptions can restrict it independent of the precision. \"parse-options.c\"\nwould in that case learn to have a run-time assert that the specified\nrange fits into the given precision (and signedness). And obviously, it\nwould of course also learn to return an error if the value passed by the\nuser exceeds the range.\n\nPatrick\n"},{"id":"516274","messageId":"Z_-GNqY9gZM6fEsg@pks.im","threadId":"63234","inReplyTo":"8e566ea1-5ff4-4854-a1dc-38626510c080@gmail.com","subject":"Re: [PATCH v2 2/5] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:28:06Z","receivedAt":"2025-04-16T10:28:10Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 15, 2025 at 04:51:59PM +0100, Phillip Wood wrote:\n> On 15/04/2025 13:14, Patrick Steinhardt wrote:\n> > Note that we do not yet assert signedness of the passed variable, which\n> > is another source of bugs. This will be handled in a subsequent commit.\n> > \n> > +\t\t} else {\n> > +\t\t\tvalue = strtoimax(arg, (char **)&s, 10);\n> > +\t\t\tif (*s)\n> > +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> > +\t\t\t\t\t     optname(opt, flags));\n> \n> To catch overflow errors for arguments of intimax_t we need to do\n> \n> \terrno = 0\n> \tvalue = strtoimax(arg, (Char **)&s, 10);\n> \tif (errno || *s)\n> \t\treturn error(...)\n> \n> to catch the error when we parse the string as the checks below only work\n> for narrower types.\n\nFair. This issue isn't new -- it already existed before my patch series.\nBut that's not a good enough reason to not fix it while we're at it.\n\nPatrick\n"},{"id":"516275","messageId":"Z_-GOuC79KxWVwJ5@pks.im","threadId":"63234","inReplyTo":"94f4ba9a-81a9-4e3a-932b-faee5aa2d2f4@web.de","subject":"Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-16T10:28:10Z","receivedAt":"2025-04-16T10:28:14Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Tue, Apr 15, 2025 at 07:38:04PM +0200, René Scharfe wrote:\n> Am 15.04.25 um 14:14 schrieb Patrick Steinhardt:\n> > diff --git a/parse-options.c b/parse-options.c\n> > index ae836c384c7..9670e46a679 100644\n> > --- a/parse-options.c\n> > +++ b/parse-options.c\n> > @@ -216,6 +216,49 @@ static enum parse_opt_result do_get_value(struct parse_opt_ctx_t *p,\n> >  \t\t\t    optname(opt, flags));\n> >  \t\t}\n> >  \t}\n> > +\tcase OPTION_UNSIGNED:\n> > +\t{\n> > +\t\tuintmax_t upper_bound = UINTMAX_MAX >> (bitsizeof(uintmax_t) - CHAR_BIT * opt->precision);\n> > +\t\tuintmax_t value;\n> > +\n> > +\t\tif (unset) {\n> > +\t\t\tvalue = 0;\n> > +\t\t} else if (opt->flags & PARSE_OPT_OPTARG && !p->opt) {\n> > +\t\t\tvalue = opt->defval;\n> > +\t\t} else if (get_arg(p, opt, flags, &arg)) {\n> > +\t\t\treturn -1;\n> > +\t\t} else if (!*arg) {\n> > +\t\t\treturn error(_(\"%s expects a numerical value\"),\n> > +\t\t\t\t     optname(opt, flags));\n> > +\t\t} else {\n> > +\t\t\tvalue = strtoumax(arg, (char **)&s, 10);\n> > +\t\t\tif (*s)\n> > +\t\t\t\treturn error(_(\"%s expects a numerical value\"),\n> > +\t\t\t\t\t     optname(opt, flags));\n> > +\t\t}\n> > +\n> > +\t\tif (value > upper_bound)\n> > +\t\t\treturn error(_(\"value %\"PRIuMAX\" for %s exceeds %\"PRIuMAX),\n> > +\t\t\t\t     value, optname(opt, flags), upper_bound);\n> > +\n> > +\t\tswitch (opt->precision) {\n> > +\t\tcase 1:\n> > +\t\t\t*(int8_t *)opt->value = value;\n> \n> uint8_t, surely.  Similarly for the other casts below.\n\nOof, of course.\n\nPatrick\n"},{"id":"516286","messageId":"7c684f2b-ca56-4e8b-b141-0c2f6b64a44a@gmail.com","threadId":"63234","inReplyTo":"Z_-GKX8uFSNib5rW@pks.im","subject":"Re: [PATCH v2 4/5] parse-options: introduce `OPTION_UNSIGNED`","fromName":"","fromEmail":"phillip.wood123@gmail.com","sentAt":"2025-04-16T13:31:44Z","receivedAt":"2025-04-16T13:31:54Z","isPatch":true,"sender":{"key":"phillip.wood@dunelm.org.uk","avatar":null},"body":"On 16/04/2025 11:27, Patrick Steinhardt wrote:\n> On Tue, Apr 15, 2025 at 04:52:02PM +0100, Phillip Wood wrote:\n> \n> Wait, does it? Why would `strtoul()` or any of its variants ever accept\n> a string prefixed with a \"-\"?\n> \n>      If the minus sign was part of the input sequence, the numeric value\n>      calculated from the sequence of digits is negated as if by unary\n>      minus in the result type, which applies unsigned integer wraparound\n>      rules.\n> \n> Oh dear... all these integer conversion functions are really a gift that\n> keeps on giving. Gross.\n\nYes it's horrible behavior for a function that parses an unsigned \ninteger. We should perhaps think about adding 64 bit versions of \nstrtoui_i() and strtoul_ui() so we have a safer option for parsing large \nintegers but I don't think we need to do that as part of this series.\n\nI'll try and take a look at V3 tomorrow\n\nPhillip\n\n>> This patch also needs the fix from patch 2 to detect overflows for\n>> uintmax_t.\n> \n> Yup, will add.\n> \n> Patrick\n\n"},{"id":"516305","messageId":"xmqq1ptshv5k.fsf@gitster.g","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-3-d390746bea79@pks.im","subject":"Re: [PATCH v3 3/7] parse-options: introduce precision handling for `OPTION_INTEGER`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-16T17:29:43Z","receivedAt":"2025-04-16T17:29:46Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> The `OPTION_INTEGER` option type accepts a signed integer. The type of\n> the underlying integer is a simple `int`, which restricts the range of\n> values accepted by such options. But there is a catch: because the\n> caller provides a pointer to the value via the `.value` field, which is\n> a simple void pointer. This has two consequences:\n>\n>   - There is no check whether the passed value is sufficiently long to\n>     store the entire range of `int`. This can lead to integer wraparound\n>     in the best case and out-of-bounds writes in the worst case.\n>\n>   - Even when a caller knows that they want to store a value larger than\n>     `INT_MAX` they don't have a way to do so.\n>\n> Funny enough, even if the caller gets everything correct the parsing\n> logic is still insufficient because we use `strtol()` to parse the\n> argument, which returns a `long`. But as that value is implicitly cast\n> when assigning it to the `int` field we may still get invalid results.\n>\n> In practice this doesn't tend to be a huge issue because users typically\n> don't end up passing huge values to most commands. But the parsing logic\n> is demonstrably broken, and it is too easy to get the calling convention\n> wrong.\n>\n> Improve the situation by introducing a new `precision` field into the\n> structure. This field gets assigned automatically by `OPT_INTEGER_F()`\n> and tracks the size of the passed value. Like this it becomes possible\n> for the caller to pass arbitrarily-sized integers and the underlying\n> logic knows to handle it correctly by doing range checks. Furthermore,\n> convert the code to use `strtoimax()` intstead of `strtol()` so that we\n> can also parse values larger than `LONG_MAX`.\n>\n> Note that we do not yet assert signedness of the passed variable, which\n> is another source of bugs. This will be handled in a subsequent commit.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n> ...\n> +\t{\n> +\t\tintmax_t upper_bound = INTMAX_MAX >> (bitsizeof(intmax_t) - CHAR_BIT * opt->precision);\n> +\t\tintmax_t lower_bound = -upper_bound - 1;\n> +\t\tintmax_t value;\n> +\n>  \t\tif (unset) {\n> ...\n> +\t\tif (value < lower_bound || value > upper_bound)\n>  \t\t\treturn error(_(\"value %s for %s not in range [%\"PRIdMAX\",%\"PRIdMAX\"]\"),\n\nWhen I imagined the case where precision is set to the same size as\nsizeof(intmax_t), this comparison made my head spin, but I think it\nshould safely yield false for any \"value\", in which case it is fine.\n\n> +\t\tswitch (opt->precision) {\n> +\t\tcase 1:\n> +\t\t\t*(int8_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 2:\n> +\t\t\t*(int16_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 4:\n> +\t\t\t*(int32_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tcase 8:\n> +\t\t\t*(int64_t *)opt->value = value;\n> +\t\t\treturn 0;\n> +\t\tdefault:\n\nGood to have this \"default\" arm.  As you cannot take an address of a\nbitfield, you cannot pass a bitfield member in a struct that is set\nto say 24-bit wide with .precision set to 3.  IOW, limiting ourselves\nonly to sizes of naturally occurring integral types is sufficient.\n\n> +\t\t\tBUG(\"invalid precision for option %s\",\n> +\t\t\t    optname(opt, flags));\n> +\t\t}\n> +\t}\n>  \tcase OPTION_MAGNITUDE:\n>  \t\tif (unset) {\n>  \t\t\t*(unsigned long *)opt->value = 0;\n> diff --git a/parse-options.h b/parse-options.h\n> index 997ffbee805..8db96402c4d 100644\n> --- a/parse-options.h\n> +++ b/parse-options.h\n> @@ -92,6 +92,10 @@ typedef int parse_opt_subcommand_fn(int argc, const char **argv,\n>   * `value`::\n>   *   stores pointers to the values to be filled.\n>   *\n> + * `precision`::\n> + *   precision of the integer pointed to by `value` in number of bytes. Should\n> + *   typically be its `sizeof()`.\n> + *\n\nNicer to see \"bytes\" in the description.  I am still puzzled, and\nmore importantly, I suspect readers will be puzzled, when told\n\"Should typically be\", as it implies there may be valid cases when\nit can be different from it, and wanting to know what these cases\nare.\n\n> @@ -214,6 +219,7 @@ struct option {\n>  \t.short_name = (s), \\\n>  \t.long_name = (l), \\\n>  \t.value = (v), \\\n> +\t.precision = sizeof(*v), \\\n>  \t.argh = N_(\"n\"), \\\n>  \t.help = (h), \\\n>  \t.flags = (f), \\\n\nNice.  By doing this in OPT_INTEGER_F(), we cover OPT_INTEGER() as\nwell automatically.\n\nThanks.\n"},{"id":"516306","messageId":"xmqqwmbkgcu5.fsf@gitster.g","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-5-d390746bea79@pks.im","subject":"Re: [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-16T18:50:42Z","receivedAt":"2025-04-16T18:50:45Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> We have two generic ways to parse integers in the \"parse-options\"\n> subsystem:\n>\n>   - `OPTION_INTEGER` parses a signed integer.\n>\n>   - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n>     interprets suffixes like \"k\" or \"g\".\n>\n> Notably missing is a middle ground that parses unsigned integers without\n> interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\n> plug this gap. This option type will be used in subsequent commits.\n\nThis takes a turn in a bit unexpected direction.  Because the way to\nspell a scaled quantity is unambiguous in the sense that anything\nthat used to be parsed with OPT_INTEGER() couldn't have had anything\nbut \"^-?[0-9]+$\", an obvious alternative is to teach OPTION_INTEGER\nto always allow the scaling suffix if the user wants to use one,\nwithout adding a new \"only numbers but this one does not even allow\na sign\" variant.\n\nSeriously, are there good candidates for an option where we want to\nabsolutely refuse to take scaling suffix and insist only on a bare\nnumber?\n"},{"id":"516307","messageId":"xmqqsem7hq2k.fsf@gitster.g","threadId":"63234","inReplyTo":"20250416-b4-pks-parse-options-integers-v3-7-d390746bea79@pks.im","subject":"Re: [PATCH v3 7/7] parse-options: introduce bounded integer options","fromName":"Junio C Hamano","fromEmail":"gitster@pobox.com","sentAt":"2025-04-16T19:19:31Z","receivedAt":"2025-04-16T19:19:35Z","isPatch":true,"sender":{"key":"gitster@pobox.com","avatar":"https://avatars.githubusercontent.com/u/54884?v=4"},"body":"Patrick Steinhardt <ps@pks.im> writes:\n\n> In the preceding commits we have introduced integer precisions. The\n> precision merely tracks bounds of the underlying data types so that we\n> don't try to for example write a `size_t` into an `unsigned`, which\n> could otherwise cause out-of-bounds writes.\n>\n> Some options may have bounds that are stricter than the underlying data\n> type. Right now, users of any such options would have to manually verify\n> that the value passed to such an option is inside the expected bounds.\n> This is rather tedious, and it leads to code duplication across sites\n> that wish to perform such bounds checks.\n>\n> Introduce `OPT_*_BOUNDED()` options that alleviate this issue. Users\n> can optionally specify both a lower and upper bound, and if set we will\n> verify that the value passed by the user is in that range.\n>\n> Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> ---\n>  parse-options.c               | 40 ++++++++++++++++++++++++++++-----\n>  parse-options.h               | 52 +++++++++++++++++++++++++++++++++++++++++++\n>  t/helper/test-parse-options.c |  5 +++++\n>  t/t0040-parse-options.sh      | 33 +++++++++++++++++++++++++++\n>  4 files changed, 125 insertions(+), 5 deletions(-)\n\nIt is certainly cute, but unless there are plenty of existing users\nthat use OPT_INTEGER() and friends and perform bounds checks\nthemselves, I am not sure if this can withstand YAGNI criticism.\nAnd this step being at the end of the series, plus the above\ndiffstat, tells us that there aren't any existing users converted to\nuse this new mechanism.\n\nOPT_INTEGER that wants to track percentage may want to say the value\nis between 0 and 100 (inclusive), but instead we take it bounded not\nto exceed 100, without lower bound.  Without a real callsite, we\ncannot even tell if it is acceptable compromise for the sake of\nsimplicity to forbid 0 as lower or upper bound, for example.\n\nThanks.\n"},{"id":"516333","messageId":"aAC4UwjLBllfeDLV@pks.im","threadId":"63234","inReplyTo":"xmqqsem7hq2k.fsf@gitster.g","subject":"Re: [PATCH v3 7/7] parse-options: introduce bounded integer options","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-17T08:14:33Z","receivedAt":"2025-04-17T08:14:40Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 16, 2025 at 12:19:31PM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > In the preceding commits we have introduced integer precisions. The\n> > precision merely tracks bounds of the underlying data types so that we\n> > don't try to for example write a `size_t` into an `unsigned`, which\n> > could otherwise cause out-of-bounds writes.\n> >\n> > Some options may have bounds that are stricter than the underlying data\n> > type. Right now, users of any such options would have to manually verify\n> > that the value passed to such an option is inside the expected bounds.\n> > This is rather tedious, and it leads to code duplication across sites\n> > that wish to perform such bounds checks.\n> >\n> > Introduce `OPT_*_BOUNDED()` options that alleviate this issue. Users\n> > can optionally specify both a lower and upper bound, and if set we will\n> > verify that the value passed by the user is in that range.\n> >\n> > Signed-off-by: Patrick Steinhardt <ps@pks.im>\n> > ---\n> >  parse-options.c               | 40 ++++++++++++++++++++++++++++-----\n> >  parse-options.h               | 52 +++++++++++++++++++++++++++++++++++++++++++\n> >  t/helper/test-parse-options.c |  5 +++++\n> >  t/t0040-parse-options.sh      | 33 +++++++++++++++++++++++++++\n> >  4 files changed, 125 insertions(+), 5 deletions(-)\n> \n> It is certainly cute, but unless there are plenty of existing users\n> that use OPT_INTEGER() and friends and perform bounds checks\n> themselves, I am not sure if this can withstand YAGNI criticism.\n> And this step being at the end of the series, plus the above\n> diffstat, tells us that there aren't any existing users converted to\n> use this new mechanism.\n\nYeah, that was also a bit of my feeling. I was on the lookout for\ncallsites, but I ultimately didn't find too many. Which is basically the\nreason why I said that this patch is more of a PoC, and that I'm happy\nto drop it again.\n\n> OPT_INTEGER that wants to track percentage may want to say the value\n> is between 0 and 100 (inclusive), but instead we take it bounded not\n> to exceed 100, without lower bound.  Without a real callsite, we\n> cannot even tell if it is acceptable compromise for the sake of\n> simplicity to forbid 0 as lower or upper bound, for example.\n\nYes, `0` meaning \"default\" is restricting us here. But my counter\nargument is that a value that can only be between `0` and `100` should\nuse `OPT_UNSIGNED` in the first place, which allows us to achieve\nexactly that.\n\nLet's just drop this patch for now. It was only a PoC anyway, and we can\nuse it as inpiration if we ever see that this feature is something we\nwant.\n\nPatrick\n"},{"id":"516334","messageId":"aAC4hMDS8gIwi7ku@pks.im","threadId":"63234","inReplyTo":"xmqqwmbkgcu5.fsf@gitster.g","subject":"Re: [PATCH v3 5/7] parse-options: introduce `OPTION_UNSIGNED`","fromName":"Patrick Steinhardt","fromEmail":"ps@pks.im","sentAt":"2025-04-17T08:15:00Z","receivedAt":"2025-04-17T08:15:05Z","isPatch":true,"sender":{"key":"ps@pks.im","avatar":"https://avatars.githubusercontent.com/u/4056630?v=4"},"body":"On Wed, Apr 16, 2025 at 11:50:42AM -0700, Junio C Hamano wrote:\n> Patrick Steinhardt <ps@pks.im> writes:\n> \n> > We have two generic ways to parse integers in the \"parse-options\"\n> > subsystem:\n> >\n> >   - `OPTION_INTEGER` parses a signed integer.\n> >\n> >   - `OPTION_MAGNITUDE` parses an unsigned integer, but it also\n> >     interprets suffixes like \"k\" or \"g\".\n> >\n> > Notably missing is a middle ground that parses unsigned integers without\n> > interpreting suffixes. Introduce a new `OPTION_UNSIGNED` option type to\n> > plug this gap. This option type will be used in subsequent commits.\n> \n> This takes a turn in a bit unexpected direction.  Because the way to\n> spell a scaled quantity is unambiguous in the sense that anything\n> that used to be parsed with OPT_INTEGER() couldn't have had anything\n> but \"^-?[0-9]+$\", an obvious alternative is to teach OPTION_INTEGER\n> to always allow the scaling suffix if the user wants to use one,\n> without adding a new \"only numbers but this one does not even allow\n> a sign\" variant.\n\nYes, that would be the alternative indeed. I wanted to have this patch\nso that I can adapt callsites that accidentally pass unsigned integers\nto the signed option without introducing any kind of incompatibility. So\nI was mostly aiming for 1:1 compatibility.\n\nBut I'm equally fine with extending `OPT_INTEGER()` to support parsing\nmagnitudes. I guess it's even preferable: less overhead on our side, and\nmore consistency for our users.\n\n> Seriously, are there good candidates for an option where we want to\n> absolutely refuse to take scaling suffix and insist only on a bare\n> number?\n\nI cannot think of any specific reason.\n\nPatrick\n"}]}