Commit 2008d037b309902ca4d7fa01692a0db16f5f4a77
1 parent
b3da5a2c
Handle help args using option tables; add json help
Showing
1 changed file
with
203 additions
and
152 deletions
qpdf/qpdf.cc
| ... | ... | @@ -243,6 +243,90 @@ ProgressReporter::reportProgress(int percentage) |
| 243 | 243 | << percentage << "%" << std::endl; |
| 244 | 244 | } |
| 245 | 245 | |
| 246 | +static JSON json_schema() | |
| 247 | +{ | |
| 248 | + // Style: use all lower-case keys with no dashes or underscores. | |
| 249 | + // Choose array or dictionary based on indexing. For example, we | |
| 250 | + // use a dictionary for objects because we want to index by object | |
| 251 | + // ID and an array for pages because we want to index by position. | |
| 252 | + // The pages in the pages array contain references back to the | |
| 253 | + // original object, which can be resolved in the objects | |
| 254 | + // dictionary. When a PDF constract that maps back to an original | |
| 255 | + // object is represented separately, use "object" as the key that | |
| 256 | + // references the original object. | |
| 257 | + | |
| 258 | + // This JSON object doubles as a schema and as documentation for | |
| 259 | + // our JSON output. Any schema mismatch is a bug in qpdf. This | |
| 260 | + // helps to enforce our policy of consistently providing a known | |
| 261 | + // structure where every documented key will always be present, | |
| 262 | + // which makes it easier to consume our JSON. This is discussed in | |
| 263 | + // more depth in the manual. | |
| 264 | + JSON schema = JSON::makeDictionary(); | |
| 265 | + schema.addDictionaryMember( | |
| 266 | + "version", JSON::makeString( | |
| 267 | + "JSON format serial number; increased for non-compatible changes")); | |
| 268 | + JSON j_params = schema.addDictionaryMember( | |
| 269 | + "parameters", JSON::makeDictionary()); | |
| 270 | + j_params.addDictionaryMember( | |
| 271 | + "decodelevel", JSON::makeString( | |
| 272 | + "decode level used to determine stream filterability")); | |
| 273 | + schema.addDictionaryMember( | |
| 274 | + "objects", JSON::makeString( | |
| 275 | + "dictionary of original objects; keys are 'trailer' or 'n n R'")); | |
| 276 | + JSON page = schema.addDictionaryMember("pages", JSON::makeArray()). | |
| 277 | + addArrayElement(JSON::makeDictionary()); | |
| 278 | + page.addDictionaryMember( | |
| 279 | + "object", | |
| 280 | + JSON::makeString("reference to original page object")); | |
| 281 | + JSON image = page.addDictionaryMember("images", JSON::makeArray()). | |
| 282 | + addArrayElement(JSON::makeDictionary()); | |
| 283 | + image.addDictionaryMember( | |
| 284 | + "object", | |
| 285 | + JSON::makeString("reference to image stream")); | |
| 286 | + image.addDictionaryMember( | |
| 287 | + "width", | |
| 288 | + JSON::makeString("image width")); | |
| 289 | + image.addDictionaryMember( | |
| 290 | + "height", | |
| 291 | + JSON::makeString("image height")); | |
| 292 | + image.addDictionaryMember("filter", JSON::makeArray()). | |
| 293 | + addArrayElement( | |
| 294 | + JSON::makeString("filters applied to image data")); | |
| 295 | + image.addDictionaryMember("decodeparms", JSON::makeArray()). | |
| 296 | + addArrayElement( | |
| 297 | + JSON::makeString("decode parameters for image data")); | |
| 298 | + image.addDictionaryMember( | |
| 299 | + "filterable", | |
| 300 | + JSON::makeString("whether image data can be decoded" | |
| 301 | + " using the decode level qpdf was invoked with")); | |
| 302 | + page.addDictionaryMember("contents", JSON::makeArray()). | |
| 303 | + addArrayElement( | |
| 304 | + JSON::makeString("reference to each content stream")); | |
| 305 | + page.addDictionaryMember( | |
| 306 | + "label", | |
| 307 | + JSON::makeString("page label dictionary, or null if none")); | |
| 308 | + JSON labels = schema.addDictionaryMember("pagelabels", JSON::makeArray()). | |
| 309 | + addArrayElement(JSON::makeDictionary()); | |
| 310 | + labels.addDictionaryMember( | |
| 311 | + "index", | |
| 312 | + JSON::makeString("starting page position starting from zero")); | |
| 313 | + labels.addDictionaryMember( | |
| 314 | + "label", | |
| 315 | + JSON::makeString("page label dictionary")); | |
| 316 | + JSON outline = page.addDictionaryMember("outlines", JSON::makeArray()). | |
| 317 | + addArrayElement(JSON::makeDictionary()); | |
| 318 | + outline.addDictionaryMember( | |
| 319 | + "object", | |
| 320 | + JSON::makeString("reference to outline that targets this page")); | |
| 321 | + outline.addDictionaryMember( | |
| 322 | + "title", | |
| 323 | + JSON::makeString("outline title")); | |
| 324 | + outline.addDictionaryMember( | |
| 325 | + "dest", | |
| 326 | + JSON::makeString("outline destination dictionary")); | |
| 327 | + return schema; | |
| 328 | +} | |
| 329 | + | |
| 246 | 330 | // This is not a general-purpose argument parser. It is tightly |
| 247 | 331 | // crafted to work with qpdf. qpdf's command-line syntax is very |
| 248 | 332 | // complex because of its long history, and it doesn't really follow |
| ... | ... | @@ -283,6 +367,11 @@ class ArgParser |
| 283 | 367 | OptionEntry oe_optionalParameter(param_arg_handler_t); |
| 284 | 368 | OptionEntry oe_requiredChoices(param_arg_handler_t, char const** choices); |
| 285 | 369 | |
| 370 | + void argHelp(); | |
| 371 | + void argVersion(); | |
| 372 | + void argCopyright(); | |
| 373 | + void argCompletionBash(); | |
| 374 | + void argJsonHelp(); | |
| 286 | 375 | void argPositional(char* arg); |
| 287 | 376 | void argPassword(char* parameter); |
| 288 | 377 | void argEmpty(); |
| ... | ... | @@ -374,6 +463,7 @@ class ArgParser |
| 374 | 463 | std::set<std::string> completions; |
| 375 | 464 | |
| 376 | 465 | std::map<std::string, OptionEntry>* option_table; |
| 466 | + std::map<std::string, OptionEntry> help_option_table; | |
| 377 | 467 | std::map<std::string, OptionEntry> main_option_table; |
| 378 | 468 | std::map<std::string, OptionEntry> encrypt40_option_table; |
| 379 | 469 | std::map<std::string, OptionEntry> encrypt128_option_table; |
| ... | ... | @@ -447,7 +537,14 @@ ArgParser::oe_requiredChoices(param_arg_handler_t h, char const** choices) |
| 447 | 537 | void |
| 448 | 538 | ArgParser::initOptionTable() |
| 449 | 539 | { |
| 450 | - std::map<std::string, OptionEntry>* t = &this->main_option_table; | |
| 540 | + std::map<std::string, OptionEntry>* t = &this->help_option_table; | |
| 541 | + (*t)["help"] = oe_bare(&ArgParser::argHelp); | |
| 542 | + (*t)["version"] = oe_bare(&ArgParser::argVersion); | |
| 543 | + (*t)["copyright"] = oe_bare(&ArgParser::argCopyright); | |
| 544 | + (*t)["completion-bash"] = oe_bare(&ArgParser::argCompletionBash); | |
| 545 | + (*t)["json-help"] = oe_bare(&ArgParser::argJsonHelp); | |
| 546 | + | |
| 547 | + t = &this->main_option_table; | |
| 451 | 548 | char const* yn[] = {"y", "n", 0}; |
| 452 | 549 | (*t)[""] = oe_positional(&ArgParser::argPositional); |
| 453 | 550 | (*t)["password"] = oe_requiredParameter(&ArgParser::argPassword, "pass"); |
| ... | ... | @@ -568,6 +665,100 @@ ArgParser::argPositional(char* arg) |
| 568 | 665 | } |
| 569 | 666 | |
| 570 | 667 | void |
| 668 | +ArgParser::argVersion() | |
| 669 | +{ | |
| 670 | + std::cout | |
| 671 | + << whoami << " version " << QPDF::QPDFVersion() << std::endl | |
| 672 | + << "Run " << whoami << " --copyright to see copyright and license information." | |
| 673 | + << std::endl; | |
| 674 | +} | |
| 675 | + | |
| 676 | +void | |
| 677 | +ArgParser::argCopyright() | |
| 678 | +{ | |
| 679 | + // Make sure the output looks right on an 80-column display. | |
| 680 | + // 1 2 3 4 5 6 7 8 | |
| 681 | + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 | |
| 682 | + std::cout | |
| 683 | + << whoami << " version " << QPDF::QPDFVersion() << std::endl | |
| 684 | + << std::endl | |
| 685 | + << "Copyright (c) 2005-2018 Jay Berkenbilt" | |
| 686 | + << std::endl | |
| 687 | + << "QPDF is licensed under the Apache License, Version 2.0 (the \"License\");" | |
| 688 | + << std::endl | |
| 689 | + << "not use this file except in compliance with the License." | |
| 690 | + << std::endl | |
| 691 | + << "You may obtain a copy of the License at" | |
| 692 | + << std::endl | |
| 693 | + << std::endl | |
| 694 | + << " http://www.apache.org/licenses/LICENSE-2.0" | |
| 695 | + << std::endl | |
| 696 | + << std::endl | |
| 697 | + << "Unless required by applicable law or agreed to in writing, software" | |
| 698 | + << std::endl | |
| 699 | + << "distributed under the License is distributed on an \"AS IS\" BASIS," | |
| 700 | + << std::endl | |
| 701 | + << "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." | |
| 702 | + << std::endl | |
| 703 | + << "See the License for the specific language governing permissions and" | |
| 704 | + << std::endl | |
| 705 | + << "limitations under the License." | |
| 706 | + << std::endl | |
| 707 | + << std::endl | |
| 708 | + << "Versions of qpdf prior to version 7 were released under the terms" | |
| 709 | + << std::endl | |
| 710 | + << "of version 2.0 of the Artistic License. At your option, you may" | |
| 711 | + << std::endl | |
| 712 | + << "continue to consider qpdf to be licensed under those terms. Please" | |
| 713 | + << std::endl | |
| 714 | + << "see the manual for additional information." | |
| 715 | + << std::endl; | |
| 716 | +} | |
| 717 | + | |
| 718 | +void | |
| 719 | +ArgParser::argHelp() | |
| 720 | +{ | |
| 721 | + std::cout << help; | |
| 722 | +} | |
| 723 | + | |
| 724 | +void | |
| 725 | +ArgParser::argCompletionBash() | |
| 726 | +{ | |
| 727 | + std::string path = argv[0]; | |
| 728 | + size_t slash = path.find('/'); | |
| 729 | + if ((slash != 0) && (slash != std::string::npos)) | |
| 730 | + { | |
| 731 | + std::cerr << "WARNING: qpdf completion enabled" | |
| 732 | + << " using relative path to qpdf" << std::endl; | |
| 733 | + } | |
| 734 | + std::cout << "complete -o bashdefault -o default -o nospace" | |
| 735 | + << " -C " << argv[0] << " " << whoami << std::endl; | |
| 736 | +} | |
| 737 | + | |
| 738 | +void | |
| 739 | +ArgParser::argJsonHelp() | |
| 740 | +{ | |
| 741 | + // Make sure the output looks right on an 80-column display. | |
| 742 | + // 1 2 3 4 5 6 7 8 | |
| 743 | + // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 | |
| 744 | + std::cout | |
| 745 | + << "The json block below contains the same structure with the same keys as the" | |
| 746 | + << std::endl | |
| 747 | + << "json generated by qpdf. In the block below, the values are descriptions of" | |
| 748 | + << std::endl | |
| 749 | + << "the meanings of those entries. The specific contract guaranteed by qpdf in" | |
| 750 | + << std::endl | |
| 751 | + << "its json representation is explained in more detail in the manual. You can" | |
| 752 | + << std::endl | |
| 753 | + << "specify a subset of top-level keys when you invoke qpdf, but the \"version\"" | |
| 754 | + << std::endl | |
| 755 | + << "and \"parameters\" keys will always be present." | |
| 756 | + << std::endl | |
| 757 | + << std::endl | |
| 758 | + << json_schema().serialize(); | |
| 759 | +} | |
| 760 | + | |
| 761 | +void | |
| 571 | 762 | ArgParser::argPassword(char* parameter) |
| 572 | 763 | { |
| 573 | 764 | o.password = parameter; |
| ... | ... | @@ -1510,80 +1701,6 @@ ArgParser::usage(std::string const& message) |
| 1510 | 1701 | } |
| 1511 | 1702 | } |
| 1512 | 1703 | |
| 1513 | -static JSON json_schema(Options& o) | |
| 1514 | -{ | |
| 1515 | - // This JSON object doubles as a schema and as documentation for | |
| 1516 | - // our JSON output. Any schema mismatch is a bug in qpdf. This | |
| 1517 | - // helps to enforce our policy of consistently providing a known | |
| 1518 | - // structure where every documented key will always be present, | |
| 1519 | - // which makes it easier to consume our JSON. This is discussed in | |
| 1520 | - // more depth in the manual. | |
| 1521 | - JSON schema = JSON::makeDictionary(); | |
| 1522 | - schema.addDictionaryMember( | |
| 1523 | - "version", JSON::makeString( | |
| 1524 | - "JSON format serial number; increased for non-compatible changes")); | |
| 1525 | - JSON j_params = schema.addDictionaryMember( | |
| 1526 | - "parameters", JSON::makeDictionary()); | |
| 1527 | - j_params.addDictionaryMember( | |
| 1528 | - "decodeLevel", JSON::makeString( | |
| 1529 | - "decode level used to determine stream filterability")); | |
| 1530 | - schema.addDictionaryMember( | |
| 1531 | - "objects", JSON::makeString( | |
| 1532 | - "Original objects; keys are 'trailer' or 'n n R'")); | |
| 1533 | - JSON page = schema.addDictionaryMember("pages", JSON::makeArray()). | |
| 1534 | - addArrayElement(JSON::makeDictionary()); | |
| 1535 | - page.addDictionaryMember( | |
| 1536 | - "object", | |
| 1537 | - JSON::makeString("reference to original page object")); | |
| 1538 | - JSON image = page.addDictionaryMember("images", JSON::makeArray()). | |
| 1539 | - addArrayElement(JSON::makeDictionary()); | |
| 1540 | - image.addDictionaryMember( | |
| 1541 | - "object", | |
| 1542 | - JSON::makeString("reference to image stream")); | |
| 1543 | - image.addDictionaryMember( | |
| 1544 | - "width", | |
| 1545 | - JSON::makeString("image width")); | |
| 1546 | - image.addDictionaryMember( | |
| 1547 | - "height", | |
| 1548 | - JSON::makeString("image height")); | |
| 1549 | - image.addDictionaryMember("filter", JSON::makeArray()). | |
| 1550 | - addArrayElement( | |
| 1551 | - JSON::makeString("filters applied to image data")); | |
| 1552 | - image.addDictionaryMember("decodeparms", JSON::makeArray()). | |
| 1553 | - addArrayElement( | |
| 1554 | - JSON::makeString("decode parameters for image data")); | |
| 1555 | - image.addDictionaryMember( | |
| 1556 | - "filterable", | |
| 1557 | - JSON::makeString("whether image data can be decoded" | |
| 1558 | - " using the decode level qpdf was invoked with")); | |
| 1559 | - page.addDictionaryMember("contents", JSON::makeArray()). | |
| 1560 | - addArrayElement( | |
| 1561 | - JSON::makeString("reference to each content stream")); | |
| 1562 | - page.addDictionaryMember( | |
| 1563 | - "label", | |
| 1564 | - JSON::makeString("page label dictionary, or null if none")); | |
| 1565 | - JSON labels = schema.addDictionaryMember("pagelabels", JSON::makeArray()). | |
| 1566 | - addArrayElement(JSON::makeDictionary()); | |
| 1567 | - labels.addDictionaryMember( | |
| 1568 | - "index", | |
| 1569 | - JSON::makeString("starting page position starting from zero")); | |
| 1570 | - labels.addDictionaryMember( | |
| 1571 | - "label", | |
| 1572 | - JSON::makeString("page label dictionary")); | |
| 1573 | - JSON outline = page.addDictionaryMember("outlines", JSON::makeArray()). | |
| 1574 | - addArrayElement(JSON::makeDictionary()); | |
| 1575 | - outline.addDictionaryMember( | |
| 1576 | - "object", | |
| 1577 | - JSON::makeString("reference to outline that targets this page")); | |
| 1578 | - outline.addDictionaryMember( | |
| 1579 | - "title", | |
| 1580 | - JSON::makeString("outline title")); | |
| 1581 | - outline.addDictionaryMember( | |
| 1582 | - "dest", | |
| 1583 | - JSON::makeString("outline destination dictionary")); | |
| 1584 | - return schema; | |
| 1585 | -} | |
| 1586 | - | |
| 1587 | 1704 | static std::string show_bool(bool v) |
| 1588 | 1705 | { |
| 1589 | 1706 | return v ? "allowed" : "not allowed"; |
| ... | ... | @@ -1823,9 +1940,6 @@ ArgParser::handleHelpArgs() |
| 1823 | 1940 | // Handle special-case informational options that are only |
| 1824 | 1941 | // available as the sole option. |
| 1825 | 1942 | |
| 1826 | - // The options processed here are also handled as a special case | |
| 1827 | - // in handleCompletion. | |
| 1828 | - | |
| 1829 | 1943 | if (argc != 2) |
| 1830 | 1944 | { |
| 1831 | 1945 | return; |
| ... | ... | @@ -1844,74 +1958,9 @@ ArgParser::handleHelpArgs() |
| 1844 | 1958 | { |
| 1845 | 1959 | return; |
| 1846 | 1960 | } |
| 1847 | - if (strcmp(arg, "version") == 0) | |
| 1848 | - { | |
| 1849 | - std::cout | |
| 1850 | - << whoami << " version " << QPDF::QPDFVersion() << std::endl | |
| 1851 | - << "Run " << whoami << " --copyright to see copyright and license information." | |
| 1852 | - << std::endl; | |
| 1853 | - exit(0); | |
| 1854 | - } | |
| 1855 | - | |
| 1856 | - if (strcmp(arg, "copyright") == 0) | |
| 1857 | - { | |
| 1858 | - // Make sure the output looks right on an 80-column display. | |
| 1859 | - // 1 2 3 4 5 6 7 8 | |
| 1860 | - // 12345678901234567890123456789012345678901234567890123456789012345678901234567890 | |
| 1861 | - std::cout | |
| 1862 | - << whoami << " version " << QPDF::QPDFVersion() << std::endl | |
| 1863 | - << std::endl | |
| 1864 | - << "Copyright (c) 2005-2018 Jay Berkenbilt" | |
| 1865 | - << std::endl | |
| 1866 | - << "QPDF is licensed under the Apache License, Version 2.0 (the \"License\");" | |
| 1867 | - << std::endl | |
| 1868 | - << "not use this file except in compliance with the License." | |
| 1869 | - << std::endl | |
| 1870 | - << "You may obtain a copy of the License at" | |
| 1871 | - << std::endl | |
| 1872 | - << std::endl | |
| 1873 | - << " http://www.apache.org/licenses/LICENSE-2.0" | |
| 1874 | - << std::endl | |
| 1875 | - << std::endl | |
| 1876 | - << "Unless required by applicable law or agreed to in writing, software" | |
| 1877 | - << std::endl | |
| 1878 | - << "distributed under the License is distributed on an \"AS IS\" BASIS," | |
| 1879 | - << std::endl | |
| 1880 | - << "WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied." | |
| 1881 | - << std::endl | |
| 1882 | - << "See the License for the specific language governing permissions and" | |
| 1883 | - << std::endl | |
| 1884 | - << "limitations under the License." | |
| 1885 | - << std::endl | |
| 1886 | - << std::endl | |
| 1887 | - << "Versions of qpdf prior to version 7 were released under the terms" | |
| 1888 | - << std::endl | |
| 1889 | - << "of version 2.0 of the Artistic License. At your option, you may" | |
| 1890 | - << std::endl | |
| 1891 | - << "continue to consider qpdf to be licensed under those terms. Please" | |
| 1892 | - << std::endl | |
| 1893 | - << "see the manual for additional information." | |
| 1894 | - << std::endl; | |
| 1895 | - exit(0); | |
| 1896 | - } | |
| 1897 | - | |
| 1898 | - if (strcmp(arg, "help") == 0) | |
| 1961 | + if (this->help_option_table.count(arg)) | |
| 1899 | 1962 | { |
| 1900 | - std::cout << help; | |
| 1901 | - exit(0); | |
| 1902 | - } | |
| 1903 | - | |
| 1904 | - if (strcmp(arg, "completion-bash") == 0) | |
| 1905 | - { | |
| 1906 | - std::string path = argv[0]; | |
| 1907 | - size_t slash = path.find('/'); | |
| 1908 | - if ((slash != 0) && (slash != std::string::npos)) | |
| 1909 | - { | |
| 1910 | - std::cerr << "WARNING: qpdf completion enabled" | |
| 1911 | - << " using relative path to qpdf" << std::endl; | |
| 1912 | - } | |
| 1913 | - std::cout << "complete -o bashdefault -o default -o nospace" | |
| 1914 | - << " -C " << argv[0] << " " << whoami << std::endl; | |
| 1963 | + (this->*(this->help_option_table[arg].bare_arg_handler))(); | |
| 1915 | 1964 | exit(0); |
| 1916 | 1965 | } |
| 1917 | 1966 | } |
| ... | ... | @@ -2255,11 +2304,13 @@ ArgParser::handleCompletion() |
| 2255 | 2304 | addOptionsToCompletions(); |
| 2256 | 2305 | if (this->argc == 1) |
| 2257 | 2306 | { |
| 2258 | - // Handle options usually handled by handleHelpArgs. | |
| 2259 | - this->completions.insert("--help"); | |
| 2260 | - this->completions.insert("--version"); | |
| 2261 | - this->completions.insert("--copyright"); | |
| 2262 | - this->completions.insert("--completion-bash"); | |
| 2307 | + // Help options are valid only by themselves. | |
| 2308 | + for (std::map<std::string, OptionEntry>::iterator iter = | |
| 2309 | + this->help_option_table.begin(); | |
| 2310 | + iter != this->help_option_table.end(); ++iter) | |
| 2311 | + { | |
| 2312 | + this->completions.insert("--" + (*iter).first); | |
| 2313 | + } | |
| 2263 | 2314 | } |
| 2264 | 2315 | } |
| 2265 | 2316 | } |
| ... | ... | @@ -2651,7 +2702,7 @@ static void do_json(QPDF& pdf, Options& o) |
| 2651 | 2702 | break; |
| 2652 | 2703 | } |
| 2653 | 2704 | j_params.addDictionaryMember( |
| 2654 | - "decodeLevel", JSON::makeString(decode_level_str)); | |
| 2705 | + "decodelevel", JSON::makeString(decode_level_str)); | |
| 2655 | 2706 | |
| 2656 | 2707 | do_json_objects(pdf, o, j); |
| 2657 | 2708 | do_json_pages(pdf, o, j); |
| ... | ... | @@ -2659,7 +2710,7 @@ static void do_json(QPDF& pdf, Options& o) |
| 2659 | 2710 | |
| 2660 | 2711 | // Check against schema |
| 2661 | 2712 | |
| 2662 | - JSON schema = json_schema(o); | |
| 2713 | + JSON schema = json_schema(); | |
| 2663 | 2714 | std::list<std::string> errors; |
| 2664 | 2715 | if (! j.checkSchema(schema, errors)) |
| 2665 | 2716 | { | ... | ... |