Commit 2008d037b309902ca4d7fa01692a0db16f5f4a77

Authored by Jay Berkenbilt
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&amp; 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&amp; pdf, Options&amp; 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&amp; pdf, Options&amp; 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 {
... ...