From e6e1ec3263916dfbb6247129fd9fd24f3257db9a Mon Sep 17 00:00:00 2001 From: Erica Fischer Date: Thu, 6 Aug 2026 16:19:03 -0700 Subject: [PATCH] Generate the usage message of each tool from its long_options (#409) * Generate the usage message of each tool from its long_options The usage messages of tile-join, tippecanoe-overzoom, tippecanoe-json-tool, tippecanoe-decode, and tippecanoe-enumerate were hand-written lists of options that had drifted years out of date, since nothing tied them to the options that are really accepted. Move the option-list printing that tippecanoe already does into a shared print_usage(), and use it in all the tools, so that the message is derived from the same long_options table that getopt_long() gets and can't fall behind it again. The tables now carry section headings, as tippecanoe's does, and the options that were only reachable by their short names (tile-join's -O, -b, -R, and -r among them) are listed for the first time. Also state the non-option arguments the way each tool really treats them: tile-join takes source tilesets unless --read-from names a file to read them from, tippecanoe-decode takes a tileset either alone or with a zoom/x/y, tippecanoe-json-tool reads standard input when no files are named, and tippecanoe-overzoom's two forms are the ones its argument parsing recognizes. tippecanoe-overzoom now reports the missing -o instead of passing NULL to fopen(), and tippecanoe-enumerate goes through getopt_long() so that it will pick up any options added later. The shared getopt_string() replaces the identical loop that four of the tools each had for building the short option string, and strip_usage_headings() the one for dropping the headings before getopt_long() sees them. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016frkRY1xXtiWjxYuCJ8vZY * Print the usage message when tippecanoe is run with no arguments Running `tippecanoe` with nothing at all reported the missing output file, which is true but is not what someone who typed the bare command needs to know. Check for the empty command line before parsing and print the general usage message instead, and leave the specific complaint for the case where an input file was named but an output file wasn't. To make the message reachable from there, the options table and the usage printing move out of main() into a usage() function, as in the other tools. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016frkRY1xXtiWjxYuCJ8vZY * Address review: alternation, the dead tile-join option, and --version Four fixes from review of the generated usage messages: * `--output` and `--output-to-directory` are one-of, not one required and one optional, in both tippecanoe and tile-join. A `usage_required_option` can now name an alternation that it belongs to, and the options in one are listed together as `(--output=... | --output-to-directory=...)`, which is what the runtime check enforces. * tile-join's `--use-attribute-for-id` has had no implementation since 533e000 removed it; only the table entry was left behind, so the option parsed and then exited with "Unrecognized option". Generating the usage message from the table turned that into a documented option that doesn't work, so remove the leftover entry too. * `--version` was grouped under "Progress indicator", in the options table and in the README both. Give it a heading of its own now that the headings are something users see. * print_usage() left `width` holding the length of the last synopsis line, and only got away with it because every table so far begins with a heading, which resets it. Start the option list on a line of its own instead of depending on that. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_016frkRY1xXtiWjxYuCJ8vZY --------- Co-authored-by: Claude --- Makefile | 12 +- README.md | 3 + decode.cpp | 66 ++++---- enumerate.cpp | 21 ++- jsontool.cpp | 57 ++++--- main.cpp | 425 ++++++++++++++++++++++++-------------------------- overzoom.cpp | 97 +++++++----- tile-join.cpp | 135 +++++++++------- usage.cpp | 133 ++++++++++++++++ usage.hpp | 52 ++++++ 10 files changed, 628 insertions(+), 373 deletions(-) create mode 100644 usage.cpp create mode 100644 usage.hpp diff --git a/Makefile b/Makefile index a7ee214e..5e6c69ae 100644 --- a/Makefile +++ b/Makefile @@ -59,25 +59,25 @@ C = $(wildcard *.c) $(wildcard *.cpp) INCLUDES = -I/usr/local/include -I. -Iclipper2/include LIBS = -L/usr/local/lib -tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o clipper2/src/clipper.engine.o +tippecanoe: geojson.o jsonpull/jsonpull.o tile.o pool.o mbtiles.o geometry.o projection.o memfile.o mvt.o serial.o main.o platform.o text.o dirtiles.o pmtiles_file.o plugin.o read_json.o write_json.o geobuf.o flatgeobuf.o evaluator.o geocsv.o csv.o geojson-loop.o json_logger.o visvalingam.o compression.o clip.o sort.o attribute.o thread.o shared_borders.o usage.o clipper2/src/clipper.engine.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -tippecanoe-enumerate: enumerate.o +tippecanoe-enumerate: enumerate.o usage.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lsqlite3 -tippecanoe-decode: decode.o projection.o mvt.o write_json.o text.o jsonpull/jsonpull.o dirtiles.o pmtiles_file.o +tippecanoe-decode: decode.o projection.o mvt.o write_json.o text.o jsonpull/jsonpull.o dirtiles.o pmtiles_file.o usage.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -tile-join: tile-join.o platform.o projection.o mbtiles.o mvt.o memfile.o dirtiles.o jsonpull/jsonpull.o text.o evaluator.o csv.o write_json.o pmtiles_file.o clip.o attribute.o thread.o read_json.o clipper2/src/clipper.engine.o +tile-join: tile-join.o platform.o projection.o mbtiles.o mvt.o memfile.o dirtiles.o jsonpull/jsonpull.o text.o evaluator.o csv.o write_json.o pmtiles_file.o clip.o attribute.o thread.o read_json.o usage.o clipper2/src/clipper.engine.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -tippecanoe-json-tool: jsontool.o jsonpull/jsonpull.o csv.o text.o geojson-loop.o +tippecanoe-json-tool: jsontool.o jsonpull/jsonpull.o csv.o text.o geojson-loop.o usage.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread unit: unit.o text.o sort.o mvt.o projection.o clip.o attribute.o jsonpull/jsonpull.o evaluator.o read_json.o clipper2/src/clipper.engine.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -tippecanoe-overzoom: overzoom.o mvt.o clip.o evaluator.o jsonpull/jsonpull.o text.o attribute.o read_json.o projection.o read_json.o clipper2/src/clipper.engine.o +tippecanoe-overzoom: overzoom.o mvt.o clip.o evaluator.o jsonpull/jsonpull.o text.o attribute.o read_json.o projection.o read_json.o usage.o clipper2/src/clipper.engine.o $(CXX) $(PG) $(LIBS) $(FINAL_FLAGS) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) -lm -lz -lsqlite3 -lpthread -include $(wildcard *.d) diff --git a/README.md b/README.md index 5c6ffec5..0788e95c 100644 --- a/README.md +++ b/README.md @@ -569,6 +569,9 @@ the same layer, enclose them in an `all` expression so they will all be evaluate * `-Q` or `--no-progress-indicator`: Don't report progress, but still give warnings * `-U` _seconds_ or `--progress-interval=`_seconds_: Don't report progress more often than the specified number of _seconds_. * `-u` or `--json-progress`: like `-quiet` but logs progress as a JSON object. Use in combination with `-U`. + +### Version + * `-v` or `--version`: Report Tippecanoe's version number ### Filters diff --git a/decode.cpp b/decode.cpp index 534e6219..6bee8876 100644 --- a/decode.cpp +++ b/decode.cpp @@ -24,6 +24,7 @@ #include "dirtiles.hpp" #include "pmtiles_file.hpp" #include "errors.hpp" +#include "usage.hpp" int minzoom = 0; int maxzoom = 32; @@ -549,8 +550,42 @@ void decode(char *fname, int z, unsigned x, unsigned y, std::set co } } +static const struct option long_options[] = { + {"Tiles to decode", 0, 0, 0}, + {"minimum-zoom", required_argument, 0, 'Z'}, + {"maximum-zoom", required_argument, 0, 'z'}, + {"layer", required_argument, 0, 'l'}, + + {"Output format", 0, 0, 0}, + {"projection", required_argument, 0, 's'}, + {"fractional-coordinates", no_argument, 0, 'F'}, + {"integer-coordinates", no_argument, 0, 'I'}, + {"tag-layer-and-zoom", no_argument, 0, 'c'}, + {"stats", no_argument, 0, 'S'}, + + {"Filtering the output", 0, 0, 0}, + {"include", required_argument, 0, 'y'}, + {"exclude-metadata-row", required_argument, 0, 'x'}, + + {"Ignoring errors in the input", 0, 0, 0}, + {"force", no_argument, 0, 'f'}, + + {0, 0, 0, 0}, +}; + +// the options above, with the usage message headings removed +static struct option real_long_options[sizeof(long_options) / sizeof(long_options[0])]; + void usage(char **argv) { - fprintf(stderr, "Usage: %s [-s projection] [-Z minzoom] [-z maxzoom] [-l layer ...] file.mbtiles [zoom x y]\n", argv[0]); + static const char *const forms[] = { + "[options] tileset", + "[options] tileset zoom x y", + NULL, + }; + + print_usage(stderr, argv[0], forms, long_options, NULL); + fprintf(stderr, "\nThe tileset can be an .mbtiles or .pmtiles file or a directory of tiles,\n"); + fprintf(stderr, "or, if zoom/x/y is specified, a single .pbf tile.\n"); exit(EXIT_ARGS); } @@ -564,33 +599,10 @@ int main(int argc, char **argv) { std::set exclude_meta; int coordinate_mode = 0; - struct option long_options[] = { - {"projection", required_argument, 0, 's'}, - {"fractional-coordinates", no_argument, 0, 'F'}, - {"integer-coordinates", no_argument, 0, 'I'}, - {"maximum-zoom", required_argument, 0, 'z'}, - {"minimum-zoom", required_argument, 0, 'Z'}, - {"layer", required_argument, 0, 'l'}, - {"tag-layer-and-zoom", no_argument, 0, 'c'}, - {"stats", no_argument, 0, 'S'}, - {"force", no_argument, 0, 'f'}, - {"exclude-metadata-row", required_argument, 0, 'x'}, - {"include", required_argument, 0, 'y'}, - {0, 0, 0, 0}, - }; + strip_usage_headings(long_options, real_long_options); + std::string getopt_str = getopt_string(real_long_options); - std::string getopt_str; - for (size_t lo = 0; long_options[lo].name != NULL; lo++) { - if (long_options[lo].val > ' ') { - getopt_str.push_back(long_options[lo].val); - - if (long_options[lo].has_arg == required_argument) { - getopt_str.push_back(':'); - } - } - } - - while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, NULL)) != -1) { + while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, NULL)) != -1) { switch (i) { case 0: break; diff --git a/enumerate.cpp b/enumerate.cpp index 691e06ce..40ff506d 100644 --- a/enumerate.cpp +++ b/enumerate.cpp @@ -1,8 +1,11 @@ #include #include #include +#include +#include #include #include "errors.hpp" +#include "usage.hpp" void enumerate(char *fname) { sqlite3 *db; @@ -48,8 +51,20 @@ void enumerate(char *fname) { } } +// there are no options, but the table is still what the usage message +// and the getopt string are derived from, so that they will keep up +// with any options that are added later +static const struct option long_options[] = { + {0, 0, 0, 0}, +}; + void usage(char **argv) { - fprintf(stderr, "Usage: %s file.mbtiles ...\n", argv[0]); + static const char *const forms[] = { + "file.mbtiles ...", + NULL, + }; + + print_usage(stderr, argv[0], forms, long_options, NULL); exit(EXIT_ARGS); } @@ -58,7 +73,9 @@ int main(int argc, char **argv) { // extern char *optarg; int i; - while ((i = getopt(argc, argv, "")) != -1) { + std::string getopt_str = getopt_string(long_options); + + while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, NULL)) != -1) { usage(argv); } diff --git a/jsontool.cpp b/jsontool.cpp index bb5341b8..5c4e0abe 100644 --- a/jsontool.cpp +++ b/jsontool.cpp @@ -13,6 +13,7 @@ #include "geojson-loop.hpp" #include "milo/dtoa_milo.h" #include "errors.hpp" +#include "usage.hpp" int fail = EXIT_SUCCESS; bool wrap = false; @@ -407,34 +408,45 @@ void process(FILE *fp, const char *fname) { json_end(jp); } +static const struct option long_options[] = { + {"Wrapping the output", 0, 0, 0}, + {"wrap", no_argument, 0, 'w'}, + + {"Sorting and joining", 0, 0, 0}, + {"extract", required_argument, 0, 'e'}, + {"csv", required_argument, 0, 'c'}, + {"empty-csv-columns-are-null", no_argument, &pe, 1}, + + {"", 0, 0, 0}, + {"prevent", required_argument, 0, 'p'}, + + {0, 0, 0, 0}, +}; + +// the options above, with the usage message headings removed +static struct option real_long_options[sizeof(long_options) / sizeof(long_options[0])]; + +void usage(char **argv) { + static const char *const forms[] = { + "[options] [file.json ...]", + NULL, + }; + + print_usage(stderr, argv[0], forms, long_options, NULL); + fprintf(stderr, "\nIf no files are named, the JSON is read from the standard input.\n"); + exit(EXIT_ARGS); +} + int main(int argc, char **argv) { const char *csv = NULL; - struct option long_options[] = { - {"wrap", no_argument, 0, 'w'}, - {"extract", required_argument, 0, 'e'}, - {"csv", required_argument, 0, 'c'}, - {"empty-csv-columns-are-null", no_argument, &pe, 1}, - {"prevent", required_argument, 0, 'p'}, - - {0, 0, 0, 0}, - }; - - std::string getopt_str; - for (size_t lo = 0; long_options[lo].name != NULL; lo++) { - if (long_options[lo].val > ' ') { - getopt_str.push_back(long_options[lo].val); - - if (long_options[lo].has_arg == required_argument) { - getopt_str.push_back(':'); - } - } - } + strip_usage_headings(long_options, real_long_options); + std::string getopt_str = getopt_string(real_long_options); extern int optind; int i; - while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, NULL)) != -1) { + while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, NULL)) != -1) { switch (i) { case 0: break; @@ -461,8 +473,7 @@ int main(int argc, char **argv) { break; default: - fprintf(stderr, "Unexpected option -%c\n", i); - exit(EXIT_ARGS); + usage(argv); } } diff --git a/main.cpp b/main.cpp index 1f8b8f83..3fb5467a 100644 --- a/main.cpp +++ b/main.cpp @@ -57,6 +57,7 @@ #include "geometry.hpp" #include "serial.hpp" #include "options.hpp" +#include "usage.hpp" #include "mvt.hpp" #include "dirtiles.hpp" #include "evaluator.hpp" @@ -2965,11 +2966,206 @@ void parse_json_source(const char *arg, struct source &src) { json_end(jp); } +static const struct option long_options_orig[] = { + {"Output tileset", 0, 0, 0}, + {"output", required_argument, 0, 'o'}, + {"output-to-directory", required_argument, 0, 'e'}, + {"force", no_argument, 0, 'f'}, + {"allow-existing", no_argument, 0, 'F'}, + + {"Tileset description and attribution", 0, 0, 0}, + {"name", required_argument, 0, 'n'}, + {"attribution", required_argument, 0, 'A'}, + {"description", required_argument, 0, 'N'}, + + {"Input files and layer names", 0, 0, 0}, + {"layer", required_argument, 0, 'l'}, + {"named-layer", required_argument, 0, 'L'}, + + {"Parallel processing of input", 0, 0, 0}, + {"read-parallel", no_argument, 0, 'P'}, + + {"Projection of input", 0, 0, 0}, + {"projection", required_argument, 0, 's'}, + + {"Zoom levels", 0, 0, 0}, + {"maximum-zoom", required_argument, 0, 'z'}, + {"minimum-zoom", required_argument, 0, 'Z'}, + {"smallest-maximum-zoom-guess", required_argument, 0, '~'}, + {"extend-zooms-if-still-dropping", no_argument, &additional[A_EXTEND_ZOOMS], 1}, + {"extend-zooms-if-still-dropping-maximum", required_argument, 0, '~'}, + {"generate-variable-depth-tile-pyramid", no_argument, &additional[A_VARIABLE_DEPTH_PYRAMID], 1}, + {"one-tile", required_argument, 0, 'R'}, + + {"Tile resolution", 0, 0, 0}, + {"full-detail", required_argument, 0, 'd'}, + {"low-detail", required_argument, 0, 'D'}, + {"minimum-detail", required_argument, 0, 'm'}, + {"extra-detail", required_argument, 0, '~'}, + + {"Filtering feature attributes", 0, 0, 0}, + {"exclude", required_argument, 0, 'x'}, + {"include", required_argument, 0, 'y'}, + {"exclude-all", no_argument, 0, 'X'}, + + {"Modifying feature attributes", 0, 0, 0}, + {"attribute-type", required_argument, 0, 'T'}, + {"attribute-description", required_argument, 0, 'Y'}, + {"accumulate-attribute", required_argument, 0, 'E'}, + {"empty-csv-columns-are-null", no_argument, &prevent[P_EMPTY_CSV_COLUMNS], 1}, + {"convert-stringified-ids-to-numbers", no_argument, &additional[A_CONVERT_NUMERIC_IDS], 1}, + {"use-attribute-for-id", required_argument, 0, '~'}, + {"single-precision", no_argument, &prevent[P_SINGLE_PRECISION], 1}, + {"set-attribute", required_argument, 0, '~'}, + {"maximum-string-attribute-length", required_argument, 0, '~'}, + + {"Filtering features by attributes", 0, 0, 0}, + {"feature-filter-file", required_argument, 0, 'J'}, + {"feature-filter", required_argument, 0, 'j'}, + {"unidecode-data", required_argument, 0, '~'}, + + {"Dropping a fixed fraction of features by zoom level", 0, 0, 0}, + {"drop-rate", required_argument, 0, 'r'}, + {"retain-points-multiplier", required_argument, 0, '~'}, + {"base-zoom", required_argument, 0, 'B'}, + {"drop-denser", required_argument, 0, '~'}, + {"limit-base-zoom-to-maximum-zoom", no_argument, &prevent[P_BASEZOOM_ABOVE_MAXZOOM], 1}, + {"drop-lines", no_argument, &additional[A_LINE_DROP], 1}, + {"drop-polygons", no_argument, &additional[A_POLYGON_DROP], 1}, + {"cluster-distance", required_argument, 0, 'K'}, + {"cluster-maxzoom", required_argument, 0, 'k'}, + {"preserve-point-density-threshold", required_argument, 0, '~'}, + {"preserve-multiplier-density-threshold", required_argument, 0, '~'}, + + {"Dropping or merging a fraction of features to keep under tile size limits", 0, 0, 0}, + {"drop-densest-as-needed", no_argument, &additional[A_DROP_DENSEST_AS_NEEDED], 1}, + {"drop-fraction-as-needed", no_argument, &additional[A_DROP_FRACTION_AS_NEEDED], 1}, + {"drop-smallest-as-needed", no_argument, &additional[A_DROP_SMALLEST_AS_NEEDED], 1}, + {"drop-by-attribute-as-needed", required_argument, 0, '~'}, + {"drop-by-attribute-order", required_argument, 0, '~'}, + {"coalesce-densest-as-needed", no_argument, &additional[A_COALESCE_DENSEST_AS_NEEDED], 1}, + {"coalesce-fraction-as-needed", no_argument, &additional[A_COALESCE_FRACTION_AS_NEEDED], 1}, + {"coalesce-smallest-as-needed", no_argument, &additional[A_COALESCE_SMALLEST_AS_NEEDED], 1}, + {"force-feature-limit", no_argument, &prevent[P_DYNAMIC_DROP], 1}, + {"cluster-densest-as-needed", no_argument, &additional[A_CLUSTER_DENSEST_AS_NEEDED], 1}, + {"keep-point-cluster-position", no_argument, &additional[A_KEEP_POINT_CLUSTER_POSITION], 1}, + + {"Dropping tightly overlapping features", 0, 0, 0}, + {"gamma", required_argument, 0, 'g'}, + {"increase-gamma-as-needed", no_argument, &additional[A_INCREASE_GAMMA_AS_NEEDED], 1}, + + {"Line and polygon simplification", 0, 0, 0}, + {"simplification", required_argument, 0, 'S'}, + {"no-line-simplification", no_argument, &prevent[P_SIMPLIFY], 1}, + {"simplify-only-low-zooms", no_argument, &prevent[P_SIMPLIFY_LOW], 1}, + {"simplification-at-maximum-zoom", required_argument, 0, '~'}, + {"no-tiny-polygon-reduction", no_argument, &prevent[P_TINY_POLYGON_REDUCTION], 1}, + {"no-tiny-polygon-reduction-at-maximum-zoom", no_argument, &prevent[P_TINY_POLYGON_REDUCTION_AT_MAXZOOM], 1}, + {"tiny-polygon-size", required_argument, 0, '~'}, + {"no-simplification-of-shared-nodes", no_argument, &prevent[P_SIMPLIFY_SHARED_NODES], 1}, + {"visvalingam", no_argument, &additional[A_VISVALINGAM], 1}, + + {"Attempts to improve shared polygon boundaries", 0, 0, 0}, + {"detect-shared-borders", no_argument, &additional[A_DETECT_SHARED_BORDERS], 1}, + {"grid-low-zooms", no_argument, &additional[A_GRID_LOW_ZOOMS], 1}, + + {"Controlling clipping to tile boundaries", 0, 0, 0}, + {"buffer", required_argument, 0, 'b'}, + {"no-clipping", no_argument, &prevent[P_CLIPPING], 1}, + {"no-duplication", no_argument, &prevent[P_DUPLICATION], 1}, + + {"Reordering features within each tile", 0, 0, 0}, + {"preserve-input-order", no_argument, &prevent[P_INPUT_ORDER], 1}, + {"reorder", no_argument, &additional[A_REORDER], 1}, + {"coalesce", no_argument, &additional[A_COALESCE], 1}, + {"reverse", no_argument, &additional[A_REVERSE], 1}, + {"hilbert", no_argument, &additional[A_HILBERT], 1}, + {"order-by", required_argument, 0, '~'}, + {"order-descending-by", required_argument, 0, '~'}, + {"order-smallest-first", no_argument, 0, '~'}, + {"order-largest-first", no_argument, 0, '~'}, + + {"Adding calculated attributes", 0, 0, 0}, + {"calculate-feature-density", no_argument, &additional[A_CALCULATE_FEATURE_DENSITY], 1}, + {"generate-ids", no_argument, &additional[A_GENERATE_IDS], 1}, + {"calculate-feature-index", no_argument, &additional[A_CALCULATE_INDEX], 1}, + + {"Trying to correct bad source geometry", 0, 0, 0}, + {"detect-longitude-wraparound", no_argument, &additional[A_DETECT_WRAPAROUND], 1}, + {"use-source-polygon-winding", no_argument, &prevent[P_USE_SOURCE_POLYGON_WINDING], 1}, + {"reverse-source-polygon-winding", no_argument, &prevent[P_REVERSE_SOURCE_POLYGON_WINDING], 1}, + {"clip-bounding-box", required_argument, 0, '~'}, + {"convert-polygons-to-label-points", no_argument, &additional[A_GENERATE_POLYGON_LABEL_POINTS], 1}, + + {"Filtering tile contents", 0, 0, 0}, + {"prefilter", required_argument, 0, 'C'}, + {"postfilter", required_argument, 0, 'c'}, + + {"Setting or disabling tile size limits", 0, 0, 0}, + {"maximum-tile-bytes", required_argument, 0, 'M'}, + {"maximum-tile-features", required_argument, 0, 'O'}, + {"limit-tile-feature-count", required_argument, 0, '~'}, + {"limit-tile-feature-count-at-maximum-zoom", required_argument, 0, '~'}, + {"no-feature-limit", no_argument, &prevent[P_FEATURE_LIMIT], 1}, + {"no-tile-size-limit", no_argument, &prevent[P_KILOBYTE_LIMIT], 1}, + {"no-tile-compression", no_argument, &prevent[P_TILE_COMPRESSION], 1}, + {"no-tile-stats", no_argument, &prevent[P_TILE_STATS], 1}, + {"tile-stats-attributes-limit", required_argument, 0, '~'}, + {"tile-stats-sample-values-limit", required_argument, 0, '~'}, + {"tile-stats-values-limit", required_argument, 0, '~'}, + + {"Temporary storage", 0, 0, 0}, + {"temporary-directory", required_argument, 0, 't'}, + + {"Progress indicator", 0, 0, 0}, + {"quiet", no_argument, 0, 'q'}, + {"no-progress-indicator", no_argument, 0, 'Q'}, + {"progress-interval", required_argument, 0, 'U'}, + {"json-progress", no_argument, 0, 'u'}, + + {"Version", 0, 0, 0}, + {"version", no_argument, 0, 'v'}, + + {"", 0, 0, 0}, + {"prevent", required_argument, 0, 'p'}, + {"additional", required_argument, 0, 'a'}, + {"check-polygons", no_argument, &additional[A_DEBUG_POLYGON], 1}, + {"no-polygon-splitting", no_argument, &prevent[P_POLYGON_SPLIT], 1}, + {"prefer-radix-sort", no_argument, &additional[A_PREFER_RADIX_SORT], 1}, + {"help", no_argument, 0, 'H'}, + + {0, 0, 0, 0}, +}; + +// the options above, with the usage message headings removed +static struct option long_options[sizeof(long_options_orig) / sizeof(long_options_orig[0])]; + +void usage(char **argv, int status) { + static const char *const forms[] = { + "[options] [file.json ...]", + NULL, + }; + static const struct usage_required_option required[] = { + {"output", "output.mbtiles", 1}, + {"output-to-directory", "directory", 1}, + {NULL, NULL, 0}, + }; + + print_usage(stderr, argv[0], forms, long_options_orig, required); + exit(status); +} + int main(int argc, char **argv) { #ifdef MTRACE mtrace(); #endif + if (argc == 1) { + // with no arguments at all, there is nothing to complain about + // specifically, so say in general what the arguments should be + usage(argv, EXIT_ARGS); + } + av = argv; init_cpus(); @@ -3017,197 +3213,9 @@ int main(int argc, char **argv) { additional[i] = 0; } - static struct option long_options_orig[] = { - {"Output tileset", 0, 0, 0}, - {"output", required_argument, 0, 'o'}, - {"output-to-directory", required_argument, 0, 'e'}, - {"force", no_argument, 0, 'f'}, - {"allow-existing", no_argument, 0, 'F'}, - - {"Tileset description and attribution", 0, 0, 0}, - {"name", required_argument, 0, 'n'}, - {"attribution", required_argument, 0, 'A'}, - {"description", required_argument, 0, 'N'}, - - {"Input files and layer names", 0, 0, 0}, - {"layer", required_argument, 0, 'l'}, - {"named-layer", required_argument, 0, 'L'}, - - {"Parallel processing of input", 0, 0, 0}, - {"read-parallel", no_argument, 0, 'P'}, - - {"Projection of input", 0, 0, 0}, - {"projection", required_argument, 0, 's'}, - - {"Zoom levels", 0, 0, 0}, - {"maximum-zoom", required_argument, 0, 'z'}, - {"minimum-zoom", required_argument, 0, 'Z'}, - {"smallest-maximum-zoom-guess", required_argument, 0, '~'}, - {"extend-zooms-if-still-dropping", no_argument, &additional[A_EXTEND_ZOOMS], 1}, - {"extend-zooms-if-still-dropping-maximum", required_argument, 0, '~'}, - {"generate-variable-depth-tile-pyramid", no_argument, &additional[A_VARIABLE_DEPTH_PYRAMID], 1}, - {"one-tile", required_argument, 0, 'R'}, - - {"Tile resolution", 0, 0, 0}, - {"full-detail", required_argument, 0, 'd'}, - {"low-detail", required_argument, 0, 'D'}, - {"minimum-detail", required_argument, 0, 'm'}, - {"extra-detail", required_argument, 0, '~'}, - - {"Filtering feature attributes", 0, 0, 0}, - {"exclude", required_argument, 0, 'x'}, - {"include", required_argument, 0, 'y'}, - {"exclude-all", no_argument, 0, 'X'}, - - {"Modifying feature attributes", 0, 0, 0}, - {"attribute-type", required_argument, 0, 'T'}, - {"attribute-description", required_argument, 0, 'Y'}, - {"accumulate-attribute", required_argument, 0, 'E'}, - {"empty-csv-columns-are-null", no_argument, &prevent[P_EMPTY_CSV_COLUMNS], 1}, - {"convert-stringified-ids-to-numbers", no_argument, &additional[A_CONVERT_NUMERIC_IDS], 1}, - {"use-attribute-for-id", required_argument, 0, '~'}, - {"single-precision", no_argument, &prevent[P_SINGLE_PRECISION], 1}, - {"set-attribute", required_argument, 0, '~'}, - {"maximum-string-attribute-length", required_argument, 0, '~'}, - - {"Filtering features by attributes", 0, 0, 0}, - {"feature-filter-file", required_argument, 0, 'J'}, - {"feature-filter", required_argument, 0, 'j'}, - {"unidecode-data", required_argument, 0, '~'}, - - {"Dropping a fixed fraction of features by zoom level", 0, 0, 0}, - {"drop-rate", required_argument, 0, 'r'}, - {"retain-points-multiplier", required_argument, 0, '~'}, - {"base-zoom", required_argument, 0, 'B'}, - {"drop-denser", required_argument, 0, '~'}, - {"limit-base-zoom-to-maximum-zoom", no_argument, &prevent[P_BASEZOOM_ABOVE_MAXZOOM], 1}, - {"drop-lines", no_argument, &additional[A_LINE_DROP], 1}, - {"drop-polygons", no_argument, &additional[A_POLYGON_DROP], 1}, - {"cluster-distance", required_argument, 0, 'K'}, - {"cluster-maxzoom", required_argument, 0, 'k'}, - {"preserve-point-density-threshold", required_argument, 0, '~'}, - {"preserve-multiplier-density-threshold", required_argument, 0, '~'}, - - {"Dropping or merging a fraction of features to keep under tile size limits", 0, 0, 0}, - {"drop-densest-as-needed", no_argument, &additional[A_DROP_DENSEST_AS_NEEDED], 1}, - {"drop-fraction-as-needed", no_argument, &additional[A_DROP_FRACTION_AS_NEEDED], 1}, - {"drop-smallest-as-needed", no_argument, &additional[A_DROP_SMALLEST_AS_NEEDED], 1}, - {"drop-by-attribute-as-needed", required_argument, 0, '~'}, - {"drop-by-attribute-order", required_argument, 0, '~'}, - {"coalesce-densest-as-needed", no_argument, &additional[A_COALESCE_DENSEST_AS_NEEDED], 1}, - {"coalesce-fraction-as-needed", no_argument, &additional[A_COALESCE_FRACTION_AS_NEEDED], 1}, - {"coalesce-smallest-as-needed", no_argument, &additional[A_COALESCE_SMALLEST_AS_NEEDED], 1}, - {"force-feature-limit", no_argument, &prevent[P_DYNAMIC_DROP], 1}, - {"cluster-densest-as-needed", no_argument, &additional[A_CLUSTER_DENSEST_AS_NEEDED], 1}, - {"keep-point-cluster-position", no_argument, &additional[A_KEEP_POINT_CLUSTER_POSITION], 1}, - - {"Dropping tightly overlapping features", 0, 0, 0}, - {"gamma", required_argument, 0, 'g'}, - {"increase-gamma-as-needed", no_argument, &additional[A_INCREASE_GAMMA_AS_NEEDED], 1}, - - {"Line and polygon simplification", 0, 0, 0}, - {"simplification", required_argument, 0, 'S'}, - {"no-line-simplification", no_argument, &prevent[P_SIMPLIFY], 1}, - {"simplify-only-low-zooms", no_argument, &prevent[P_SIMPLIFY_LOW], 1}, - {"simplification-at-maximum-zoom", required_argument, 0, '~'}, - {"no-tiny-polygon-reduction", no_argument, &prevent[P_TINY_POLYGON_REDUCTION], 1}, - {"no-tiny-polygon-reduction-at-maximum-zoom", no_argument, &prevent[P_TINY_POLYGON_REDUCTION_AT_MAXZOOM], 1}, - {"tiny-polygon-size", required_argument, 0, '~'}, - {"no-simplification-of-shared-nodes", no_argument, &prevent[P_SIMPLIFY_SHARED_NODES], 1}, - {"visvalingam", no_argument, &additional[A_VISVALINGAM], 1}, - - {"Attempts to improve shared polygon boundaries", 0, 0, 0}, - {"detect-shared-borders", no_argument, &additional[A_DETECT_SHARED_BORDERS], 1}, - {"grid-low-zooms", no_argument, &additional[A_GRID_LOW_ZOOMS], 1}, - - {"Controlling clipping to tile boundaries", 0, 0, 0}, - {"buffer", required_argument, 0, 'b'}, - {"no-clipping", no_argument, &prevent[P_CLIPPING], 1}, - {"no-duplication", no_argument, &prevent[P_DUPLICATION], 1}, - - {"Reordering features within each tile", 0, 0, 0}, - {"preserve-input-order", no_argument, &prevent[P_INPUT_ORDER], 1}, - {"reorder", no_argument, &additional[A_REORDER], 1}, - {"coalesce", no_argument, &additional[A_COALESCE], 1}, - {"reverse", no_argument, &additional[A_REVERSE], 1}, - {"hilbert", no_argument, &additional[A_HILBERT], 1}, - {"order-by", required_argument, 0, '~'}, - {"order-descending-by", required_argument, 0, '~'}, - {"order-smallest-first", no_argument, 0, '~'}, - {"order-largest-first", no_argument, 0, '~'}, - - {"Adding calculated attributes", 0, 0, 0}, - {"calculate-feature-density", no_argument, &additional[A_CALCULATE_FEATURE_DENSITY], 1}, - {"generate-ids", no_argument, &additional[A_GENERATE_IDS], 1}, - {"calculate-feature-index", no_argument, &additional[A_CALCULATE_INDEX], 1}, - - {"Trying to correct bad source geometry", 0, 0, 0}, - {"detect-longitude-wraparound", no_argument, &additional[A_DETECT_WRAPAROUND], 1}, - {"use-source-polygon-winding", no_argument, &prevent[P_USE_SOURCE_POLYGON_WINDING], 1}, - {"reverse-source-polygon-winding", no_argument, &prevent[P_REVERSE_SOURCE_POLYGON_WINDING], 1}, - {"clip-bounding-box", required_argument, 0, '~'}, - {"convert-polygons-to-label-points", no_argument, &additional[A_GENERATE_POLYGON_LABEL_POINTS], 1}, - - {"Filtering tile contents", 0, 0, 0}, - {"prefilter", required_argument, 0, 'C'}, - {"postfilter", required_argument, 0, 'c'}, - - {"Setting or disabling tile size limits", 0, 0, 0}, - {"maximum-tile-bytes", required_argument, 0, 'M'}, - {"maximum-tile-features", required_argument, 0, 'O'}, - {"limit-tile-feature-count", required_argument, 0, '~'}, - {"limit-tile-feature-count-at-maximum-zoom", required_argument, 0, '~'}, - {"no-feature-limit", no_argument, &prevent[P_FEATURE_LIMIT], 1}, - {"no-tile-size-limit", no_argument, &prevent[P_KILOBYTE_LIMIT], 1}, - {"no-tile-compression", no_argument, &prevent[P_TILE_COMPRESSION], 1}, - {"no-tile-stats", no_argument, &prevent[P_TILE_STATS], 1}, - {"tile-stats-attributes-limit", required_argument, 0, '~'}, - {"tile-stats-sample-values-limit", required_argument, 0, '~'}, - {"tile-stats-values-limit", required_argument, 0, '~'}, - - {"Temporary storage", 0, 0, 0}, - {"temporary-directory", required_argument, 0, 't'}, - - {"Progress indicator", 0, 0, 0}, - {"quiet", no_argument, 0, 'q'}, - {"no-progress-indicator", no_argument, 0, 'Q'}, - {"progress-interval", required_argument, 0, 'U'}, - {"json-progress", no_argument, 0, 'u'}, - {"version", no_argument, 0, 'v'}, - - {"", 0, 0, 0}, - {"prevent", required_argument, 0, 'p'}, - {"additional", required_argument, 0, 'a'}, - {"check-polygons", no_argument, &additional[A_DEBUG_POLYGON], 1}, - {"no-polygon-splitting", no_argument, &prevent[P_POLYGON_SPLIT], 1}, - {"prefer-radix-sort", no_argument, &additional[A_PREFER_RADIX_SORT], 1}, - {"help", no_argument, 0, 'H'}, - - {0, 0, 0, 0}, - }; - - static struct option long_options[sizeof(long_options_orig) / sizeof(long_options_orig[0])]; - static char getopt_str[sizeof(long_options_orig) / sizeof(long_options_orig[0]) * 2 + 1]; + strip_usage_headings(long_options_orig, long_options); { - size_t out = 0; - size_t cout = 0; - for (size_t lo = 0; long_options_orig[lo].name != NULL; lo++) { - if (long_options_orig[lo].val != 0) { - long_options[out++] = long_options_orig[lo]; - - if (long_options_orig[lo].val > ' ') { - getopt_str[cout++] = long_options_orig[lo].val; - - if (long_options_orig[lo].has_arg == required_argument) { - getopt_str[cout++] = ':'; - } - } - } - } - long_options[out] = {0, 0, 0, 0}; - getopt_str[cout] = '\0'; - for (size_t lo = 0; long_options[lo].name != NULL; lo++) { if (long_options[lo].flag != NULL) { if (*long_options[lo].flag != 0) { @@ -3226,9 +3234,10 @@ int main(int argc, char **argv) { } std::string commandline = format_commandline(argc, argv); + std::string getopt_str = getopt_string(long_options); int option_index = 0; - while ((i = getopt_long(argc, argv, getopt_str, long_options, &option_index)) != -1) { + while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, &option_index)) != -1) { switch (i) { case 0: break; @@ -3652,43 +3661,11 @@ int main(int argc, char **argv) { set_attribute_accum(attribute_accum, optarg, argv); break; - default: { + default: if (i != 'H' && i != '?') { fprintf(stderr, "Unknown option -%c\n", i); } - int width = 7 + strlen(argv[0]); - fprintf(stderr, "Usage: %s [options] [file.json ...]", argv[0]); - for (size_t lo = 0; long_options_orig[lo].name != NULL && strlen(long_options_orig[lo].name) > 0; lo++) { - if (long_options_orig[lo].val == 0) { - fprintf(stderr, "\n %s\n ", long_options_orig[lo].name); - width = 8; - continue; - } - if (width + strlen(long_options_orig[lo].name) + 9 >= 80) { - fprintf(stderr, "\n "); - width = 8; - } - width += strlen(long_options_orig[lo].name) + 9; - if (strcmp(long_options_orig[lo].name, "output") == 0) { - fprintf(stderr, " --%s=output.mbtiles", long_options_orig[lo].name); - width += 9; - } else if (long_options_orig[lo].has_arg) { - fprintf(stderr, " [--%s=...]", long_options_orig[lo].name); - } else { - fprintf(stderr, " [--%s]", long_options_orig[lo].name); - } - } - if (width + 16 >= 80) { - fprintf(stderr, "\n "); - width = 8; - } - fprintf(stderr, "\n"); - if (i == 'H') { - exit(EXIT_SUCCESS); - } else { - exit(EXIT_ARGS); - } - } + usage(argv, i == 'H' ? EXIT_SUCCESS : EXIT_ARGS); } } diff --git a/overzoom.cpp b/overzoom.cpp index cdef1dd8..4347bd4f 100644 --- a/overzoom.cpp +++ b/overzoom.cpp @@ -11,6 +11,7 @@ #include "text.hpp" #include "read_json.hpp" #include "projection.hpp" +#include "usage.hpp" extern char *optarg; extern int optind; @@ -30,11 +31,58 @@ std::set keep; std::set exclude; std::vector exclude_prefix; +static const struct option long_options[] = { + {"Output tile", 0, 0, 0}, + {"output", required_argument, 0, 'o'}, + {"source-tile", required_argument, 0, 't'}, + {"no-tile-compression", no_argument, 0, 'd' & 0x1F}, + + {"Tile resolution", 0, 0, 0}, + {"full-detail", required_argument, 0, 'd'}, + {"buffer", required_argument, 0, 'b'}, + + {"Filtering feature attributes", 0, 0, 0}, + {"include", required_argument, 0, 'y'}, + {"exclude", required_argument, 0, 'x'}, + {"exclude-prefix", required_argument, 0, 'x' & 0x1F}, + + {"Modifying feature attributes", 0, 0, 0}, + {"accumulate-attribute", required_argument, 0, 'E'}, + {"unidecode-data", required_argument, 0, 'u' & 0x1F}, + + {"Filtering features", 0, 0, 0}, + {"feature-filter", required_argument, 0, 'j'}, + {"feature-filter-file", required_argument, 0, 'J'}, + {"filter-points-multiplier", no_argument, 0, 'm'}, + {"deduplicate-by-id", no_argument, 0, 'i' & 0x1F}, + + {"Line and polygon simplification", 0, 0, 0}, + {"line-simplification", required_argument, 0, 'S'}, + {"tiny-polygon-size", required_argument, 0, 's' & 0x1F}, + + {"Reordering features within the tile", 0, 0, 0}, + {"preserve-input-order", no_argument, 0, 'o' & 0x1F}, + + {0, 0, 0, 0}, +}; + +// the options above, with the usage message headings removed +static struct option real_long_options[sizeof(long_options) / sizeof(long_options[0])]; + void usage(char **argv) { - fprintf(stderr, "Usage: %s -o newtile.pbf.gz tile.pbf.gz oz/ox/oy nz/nx/ny\n", argv[0]); - fprintf(stderr, "to create tile nz/nx/ny from tile oz/ox/oy\n"); - fprintf(stderr, "Usage: %s -o newtile.pbf.gz -t nz/nx/ny tile.pbf.gz oz/ox/oy tile2.pbf.gz oz2/ox2/oy2\n", argv[0]); - fprintf(stderr, "to create tile nz/nx/ny from tiles oz/ox/oy and oz2/ox2/oy2\n"); + static const char *const forms[] = { + "[options] tile.pbf.gz oz/ox/oy nz/nx/ny", + "[options] --source-tile=nz/nx/ny tile.pbf.gz oz/ox/oy ...", + NULL, + }; + static const struct usage_required_option required[] = { + {"output", "newtile.pbf.gz", 0}, + {NULL, NULL, 0}, + }; + + print_usage(stderr, argv[0], forms, long_options, required); + fprintf(stderr, "\nThe tile nz/nx/ny is created from the tile or tiles oz/ox/oy that contain it.\n"); + fprintf(stderr, "In the second form, each source tile is named by a file name and a z/x/y pair.\n"); exit(EXIT_FAILURE); } @@ -67,41 +115,11 @@ int main(int argc, char **argv) { std::vector sources; - struct option long_options[] = { - {"include", required_argument, 0, 'y'}, - {"exclude", required_argument, 0, 'x'}, - {"exclude-prefix", required_argument, 0, 'x' & 0x1F}, - {"full-detail", required_argument, 0, 'd'}, - {"buffer", required_argument, 0, 'b'}, - {"output", required_argument, 0, 'o'}, - {"filter-points-multiplier", no_argument, 0, 'm'}, - {"feature-filter", required_argument, 0, 'j'}, - {"feature-filter-file", required_argument, 0, 'J'}, - {"preserve-input-order", no_argument, 0, 'o' & 0x1F}, - {"accumulate-attribute", required_argument, 0, 'E'}, - {"unidecode-data", required_argument, 0, 'u' & 0x1F}, - {"line-simplification", required_argument, 0, 'S'}, - {"tiny-polygon-size", required_argument, 0, 's' & 0x1F}, - {"source-tile", required_argument, 0, 't'}, - {"no-tile-compression", no_argument, 0, 'd' & 0x1F}, - {"deduplicate-by-id", no_argument, 0, 'i' & 0x1F}, - - {0, 0, 0, 0}, - }; - - std::string getopt_str; - for (size_t lo = 0; long_options[lo].name != NULL; lo++) { - if (long_options[lo].val > ' ') { - getopt_str.push_back(long_options[lo].val); - - if (long_options[lo].has_arg == required_argument) { - getopt_str.push_back(':'); - } - } - } + strip_usage_headings(long_options, real_long_options); + std::string getopt_str = getopt_string(real_long_options); int option_index = 0; - while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, &option_index)) != -1) { + while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, &option_index)) != -1) { switch (i) { case 'y': keep.insert(optarg); @@ -181,6 +199,11 @@ int main(int argc, char **argv) { std::vector its; int nz, nx, ny; + if (outfile == NULL) { + fprintf(stderr, "%s: must specify -o newtile.pbf.gz\n", argv[0]); + usage(argv); + } + if (outtile == NULL) { // single input if (argc - optind != 3) { fprintf(stderr, "Wrong number of arguments\n"); diff --git a/tile-join.cpp b/tile-join.cpp index 1b057577..581a1574 100644 --- a/tile-join.cpp +++ b/tile-join.cpp @@ -44,6 +44,7 @@ #include "geometry.hpp" #include "thread.hpp" #include "platform.hpp" +#include "usage.hpp" int pk = false; int pC = false; @@ -1253,8 +1254,83 @@ void decode(struct tileset_reader *readers, std::map ' ') { - getopt_str.push_back(long_options[lo].val); - - if (long_options[lo].has_arg == required_argument) { - getopt_str.push_back(':'); - } - } - } + strip_usage_headings(long_options, real_long_options); + std::string getopt_str = getopt_string(real_long_options); extern int optind; extern char *optarg; @@ -1357,7 +1384,7 @@ int main(int argc, char **argv) { std::string commandline = format_commandline(argc, argv); int option_index = 0; - while ((i = getopt_long(argc, argv, getopt_str.c_str(), long_options, &option_index)) != -1) { + while ((i = getopt_long(argc, argv, getopt_str.c_str(), real_long_options, &option_index)) != -1) { switch (i) { case 0: break; @@ -1502,7 +1529,7 @@ int main(int argc, char **argv) { break; case '~': { - const char *opt = long_options[option_index].name; + const char *opt = real_long_options[option_index].name; if (strcmp(opt, "tile-stats-attributes-limit") == 0) { max_tilestats_attributes = atoi(optarg); } else if (strcmp(opt, "tile-stats-sample-values-limit") == 0) { diff --git a/usage.cpp b/usage.cpp new file mode 100644 index 00000000..0f36f282 --- /dev/null +++ b/usage.cpp @@ -0,0 +1,133 @@ +#include +#include +#include +#include "usage.hpp" + +// Options are wrapped to fit within this many columns +#define USAGE_WIDTH 80 + +// The indentation of the continuation lines of the option list +#define USAGE_INDENT 8 + +std::string getopt_string(const struct option *long_options) { + std::string getopt_str; + + for (size_t lo = 0; long_options[lo].name != NULL; lo++) { + if (long_options[lo].val > ' ') { + getopt_str.push_back(long_options[lo].val); + + if (long_options[lo].has_arg == required_argument) { + getopt_str.push_back(':'); + } + } + } + + return getopt_str; +} + +void strip_usage_headings(const struct option *long_options, struct option *real_long_options) { + size_t out = 0; + + for (size_t lo = 0; long_options[lo].name != NULL; lo++) { + if (long_options[lo].val != 0) { + real_long_options[out++] = long_options[lo]; + } + } + + real_long_options[out] = {0, 0, 0, 0}; +} + +// The entry for `name` in the list of options that must be specified, +// or NULL if it is an optional option +static const struct usage_required_option *required_for(const char *name, const struct usage_required_option *required) { + for (size_t i = 0; required != NULL && required[i].name != NULL; i++) { + if (strcmp(required[i].name, name) == 0) { + return &required[i]; + } + } + + return NULL; +} + +// "--option", or "--option=placeholder" if the option takes an argument +static std::string option_text(const struct option *opt, const struct usage_required_option *req) { + std::string text = std::string("--") + opt->name; + + if (opt->has_arg != no_argument) { + text += "="; + text += (req != NULL && req->placeholder != NULL) ? req->placeholder : "..."; + } + + return text; +} + +// The alternatives that `req` belongs to, as "(--this=... | --that=...)" +static std::string alternation_text(const struct option *long_options, const struct usage_required_option *required, int alternation) { + std::string text; + size_t found = 0; + + for (size_t lo = 0; long_options[lo].name != NULL && long_options[lo].name[0] != '\0'; lo++) { + const struct usage_required_option *req = required_for(long_options[lo].name, required); + + if (req != NULL && req->alternation == alternation) { + if (found++ > 0) { + text += " | "; + } + + text += option_text(&long_options[lo], req); + } + } + + if (found > 1) { + text = "(" + text + ")"; + } + + return text; +} + +void print_usage(FILE *out, const char *program, const char *const *forms, + const struct option *long_options, + const struct usage_required_option *required) { + for (size_t f = 0; forms[f] != NULL; f++) { + const char *lead = (f == 0) ? "Usage: " : "\n or: "; + fprintf(out, "%s%s %s", lead, program, forms[f]); + } + + // whatever the forms took up, the option list starts on a line of its own + size_t width = USAGE_WIDTH; + std::set alternations_listed; + + for (size_t lo = 0; long_options[lo].name != NULL && long_options[lo].name[0] != '\0'; lo++) { + if (long_options[lo].val == 0) { + fprintf(out, "\n %s\n%*s", long_options[lo].name, USAGE_INDENT, ""); + width = USAGE_INDENT; + continue; + } + + const struct usage_required_option *req = required_for(long_options[lo].name, required); + std::string text; + + if (req == NULL) { + text = "[" + option_text(&long_options[lo], NULL) + "]"; + } else if (req->alternation == 0) { + text = option_text(&long_options[lo], req); + } else { + if (alternations_listed.count(req->alternation) != 0) { + continue; // already listed with the first of its alternatives + } + alternations_listed.insert(req->alternation); + + text = alternation_text(long_options, required, req->alternation); + } + + if (width + 1 + text.size() >= USAGE_WIDTH) { + fprintf(out, "\n%*s", USAGE_INDENT, ""); + width = USAGE_INDENT; + } + + fprintf(out, " %s", text.c_str()); + width += 1 + text.size(); + } + + fprintf(out, "\n"); +} diff --git a/usage.hpp b/usage.hpp new file mode 100644 index 00000000..d8e8d68d --- /dev/null +++ b/usage.hpp @@ -0,0 +1,52 @@ +#ifndef USAGE_HPP +#define USAGE_HPP + +#include +#include +#include + +// An option that must be specified rather than being optional, and the +// placeholder to show for its argument in the usage message. +// +// Options that share the same non-zero `alternation` are alternatives to +// each other: one of them must be specified, but not more than one, and +// they are listed together as `(--this=... | --that=...)`. +struct usage_required_option { + const char *name; + const char *placeholder; + int alternation; +}; + +// Returns the short option string to pass to getopt_long() for the +// options in `long_options`, so that the two can't disagree about +// which short options exist or take arguments. +std::string getopt_string(const struct option *long_options); + +// Copies `long_options` to `real_long_options`, leaving out the headings +// of the usage message, which are not real options and so must not be +// passed on to getopt_long(). The destination must be at least as large +// as the source. +void strip_usage_headings(const struct option *long_options, struct option *real_long_options); + +// Prints a usage message for `program` to `out`: +// +// Usage: program forms[0] +// or: program forms[1] +// [--some-option] [--another-option=...] ... +// +// where `forms` is a NULL-terminated list of the ways the non-option +// arguments can be given, and the list of options is derived from +// `long_options`, the same table that is passed to getopt_long(), so that +// the message stays in sync with the options that are really accepted. +// +// Options named in `required` (a list terminated by a NULL name, or NULL +// if there are none) are shown without brackets, using the placeholder +// given there for their argument, and grouped with any alternatives to +// them. An entry in `long_options` with no `val` is printed as a heading +// for the options that follow it, and an entry with an empty name ends +// the listing, hiding any options after it. +void print_usage(FILE *out, const char *program, const char *const *forms, + const struct option *long_options, + const struct usage_required_option *required); + +#endif