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,6 +243,90 @@ ProgressReporter::reportProgress(int percentage)
243 << percentage << "%" << std::endl; 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 // This is not a general-purpose argument parser. It is tightly 330 // This is not a general-purpose argument parser. It is tightly
247 // crafted to work with qpdf. qpdf's command-line syntax is very 331 // crafted to work with qpdf. qpdf's command-line syntax is very
248 // complex because of its long history, and it doesn't really follow 332 // complex because of its long history, and it doesn't really follow
@@ -283,6 +367,11 @@ class ArgParser @@ -283,6 +367,11 @@ class ArgParser
283 OptionEntry oe_optionalParameter(param_arg_handler_t); 367 OptionEntry oe_optionalParameter(param_arg_handler_t);
284 OptionEntry oe_requiredChoices(param_arg_handler_t, char const** choices); 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 void argPositional(char* arg); 375 void argPositional(char* arg);
287 void argPassword(char* parameter); 376 void argPassword(char* parameter);
288 void argEmpty(); 377 void argEmpty();
@@ -374,6 +463,7 @@ class ArgParser @@ -374,6 +463,7 @@ class ArgParser
374 std::set<std::string> completions; 463 std::set<std::string> completions;
375 464
376 std::map<std::string, OptionEntry>* option_table; 465 std::map<std::string, OptionEntry>* option_table;
  466 + std::map<std::string, OptionEntry> help_option_table;
377 std::map<std::string, OptionEntry> main_option_table; 467 std::map<std::string, OptionEntry> main_option_table;
378 std::map<std::string, OptionEntry> encrypt40_option_table; 468 std::map<std::string, OptionEntry> encrypt40_option_table;
379 std::map<std::string, OptionEntry> encrypt128_option_table; 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,7 +537,14 @@ ArgParser::oe_requiredChoices(param_arg_handler_t h, char const** choices)
447 void 537 void
448 ArgParser::initOptionTable() 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 char const* yn[] = {"y", "n", 0}; 548 char const* yn[] = {"y", "n", 0};
452 (*t)[""] = oe_positional(&ArgParser::argPositional); 549 (*t)[""] = oe_positional(&ArgParser::argPositional);
453 (*t)["password"] = oe_requiredParameter(&ArgParser::argPassword, "pass"); 550 (*t)["password"] = oe_requiredParameter(&ArgParser::argPassword, "pass");
@@ -568,6 +665,100 @@ ArgParser::argPositional(char* arg) @@ -568,6 +665,100 @@ ArgParser::argPositional(char* arg)
568 } 665 }
569 666
570 void 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 ArgParser::argPassword(char* parameter) 762 ArgParser::argPassword(char* parameter)
572 { 763 {
573 o.password = parameter; 764 o.password = parameter;
@@ -1510,80 +1701,6 @@ ArgParser::usage(std::string const&amp; message) @@ -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 static std::string show_bool(bool v) 1704 static std::string show_bool(bool v)
1588 { 1705 {
1589 return v ? "allowed" : "not allowed"; 1706 return v ? "allowed" : "not allowed";
@@ -1823,9 +1940,6 @@ ArgParser::handleHelpArgs() @@ -1823,9 +1940,6 @@ ArgParser::handleHelpArgs()
1823 // Handle special-case informational options that are only 1940 // Handle special-case informational options that are only
1824 // available as the sole option. 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 if (argc != 2) 1943 if (argc != 2)
1830 { 1944 {
1831 return; 1945 return;
@@ -1844,74 +1958,9 @@ ArgParser::handleHelpArgs() @@ -1844,74 +1958,9 @@ ArgParser::handleHelpArgs()
1844 { 1958 {
1845 return; 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 exit(0); 1964 exit(0);
1916 } 1965 }
1917 } 1966 }
@@ -2255,11 +2304,13 @@ ArgParser::handleCompletion() @@ -2255,11 +2304,13 @@ ArgParser::handleCompletion()
2255 addOptionsToCompletions(); 2304 addOptionsToCompletions();
2256 if (this->argc == 1) 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,7 +2702,7 @@ static void do_json(QPDF&amp; pdf, Options&amp; o)
2651 break; 2702 break;
2652 } 2703 }
2653 j_params.addDictionaryMember( 2704 j_params.addDictionaryMember(
2654 - "decodeLevel", JSON::makeString(decode_level_str)); 2705 + "decodelevel", JSON::makeString(decode_level_str));
2655 2706
2656 do_json_objects(pdf, o, j); 2707 do_json_objects(pdf, o, j);
2657 do_json_pages(pdf, o, j); 2708 do_json_pages(pdf, o, j);
@@ -2659,7 +2710,7 @@ static void do_json(QPDF&amp; pdf, Options&amp; o) @@ -2659,7 +2710,7 @@ static void do_json(QPDF&amp; pdf, Options&amp; o)
2659 2710
2660 // Check against schema 2711 // Check against schema
2661 2712
2662 - JSON schema = json_schema(o); 2713 + JSON schema = json_schema();
2663 std::list<std::string> errors; 2714 std::list<std::string> errors;
2664 if (! j.checkSchema(schema, errors)) 2715 if (! j.checkSchema(schema, errors))
2665 { 2716 {