/************************************************************************* ** CLCommandLine.cpp ** ** ** ** This file is part of dvisvgm -- a fast DVI to SVG converter ** ** Copyright (C) 2005-2024 Martin Gieseking ** ** ** ** This program is free software; you can redistribute it and/or ** ** modify it under the terms of the GNU General Public License as ** ** published by the Free Software Foundation; either version 3 of ** ** the License, or (at your option) any later version. ** ** ** ** This program is distributed in the hope that it will be useful, but ** ** WITHOUT ANY WARRANTY; without even the implied warranty of ** ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ** ** GNU General Public License for more details. ** ** ** ** You should have received a copy of the GNU General Public License ** ** along with this program; if not, see . ** *************************************************************************/ #include #include #include #include "CLCommandLine.hpp" #include "version.hpp" using namespace CL; using namespace std; CommandLine::CommandLine (const char *summary, const char *usage, const char *copyright) : _summary(summary), _usage(usage), _copyright(copyright) { } void CommandLine::parse (int argc, char **argv) { bool filesOnly = false; for (int i=1; i < argc; i++) { istringstream iss(argv[i]); if (filesOnly || iss.peek() != '-') _files.emplace_back(argv[i]); else { iss.get(); // skip dash if (iss.peek() < 0) _singleDashParsed = true; else if (iss.peek() != '-') parseShortOption(iss, argc, argv, i); else { iss.get(); // skip dash if (iss.peek() == EOF) // "--" only, no following option name? filesOnly = true; // treat all following options as filenames else parseLongOption(iss); } } } } static void error (const Option &option, bool shortname, const string &msg) { ostringstream oss; oss << "option "; if (shortname) oss << '-' << option.shortName(); else oss << "--" << option.longName(); oss << ": " << msg; throw CommandLineException(oss.str()); } static void type_error (const Option &option, bool shortname) { ostringstream oss; switch (option.argMode()) { case Option::ArgMode::NONE: error(option, shortname, "no argument expected"); break; case Option::ArgMode::OPTIONAL: oss << "optional argument "; if (!option.argName().empty()) oss << "'" << option.argName() << "' "; oss << "must be of "+option.typeString()+" type"; error(option, shortname, oss.str()); break; default: oss << option.typeString()+" argument "; if (!option.argName().empty()) oss << "'" << option.argName() << "' "; oss << "expected"; error(option, shortname, oss.str()); } } void CommandLine::parseShortOption (istringstream &iss, int argc, char **argv, int &argn) { bool combined = false; do { char shortname = static_cast(iss.get()); if (!isalnum(shortname)) throw CommandLineException(string("syntax error: -")+shortname); if (Option *option = lookupOption(shortname)) { if (!combined || option->argMode() == Option::ArgMode::NONE) { if (option->argMode() == Option::ArgMode::REQUIRED && strlen(argv[argn]) == 2) { // required argument separated by whitespace? if (argn+1 < argc && argv[argn+1][0] != '-') { iss.clear(); // reset error flags iss.str(argv[++argn]); // continue parsing with next command-line field } } if (!option->parse(iss, false)) type_error(*option, true); if (option->argMode() == Option::ArgMode::NONE) combined = true; } else throw CommandLineException(string("option -")+ shortname + " must be given separately"); } else throw CommandLineException(string("unknown option -")+shortname); } while (combined && !iss.eof()); } void CommandLine::parseLongOption (istream &is) { string optname; while (isalnum(is.peek()) || (!optname.empty() && is.peek() == '-')) optname += char(is.get()); if (optname.empty()) throw CommandLineException("unexpected character '" + string(1, is.peek()) + "' after '--'"); vector options = lookupOption(optname); if (options.empty()) throw CommandLineException("unknown option --"+optname); else if (options.size() == 1) { if (!options[0]->parse(is, true)) type_error(*options[0], false); } else { // is partially given option ambiguous? sort(options.begin(), options.end(), [](const Option *opt1, const Option *opt2) { return opt1->longName() < opt2->longName(); }); string msg = "option --" + optname + " is ambiguous ("; for (const Option *opt : options) { if (opt != options[0]) msg += ", "; msg += opt->longName(); } msg += ')'; throw CommandLineException(msg); } } /** Returns all options that match the given long name. */ vector CommandLine::lookupOption (const string &optname) const { vector matches; auto len = optname.length(); for (OptSectPair optsect : options()) { if (optsect.first->longName() == optname) { // exact match? matches.clear(); matches.push_back(optsect.first); break; } if (optsect.first->longName().substr(0, len) == optname) // partial match? matches.push_back(optsect.first); } return matches; } /** Returns the option that match the given short name. */ Option* CommandLine::lookupOption (char optchar) const { auto it = find_if(options().begin(), options().end(), [=](const OptSectPair &optsect) { return optsect.first->shortName() == optchar; }); return (it != options().end()) ? it->first : nullptr; } /** Prints help text including summary of options. * @param[in] os output stream the help text is printed to * @param[in] mode output mode (0=section, 1=sorted by short options, 2=sorted by long options) */ void CommandLine::help (ostream &os, int mode) const { os << PROGRAM_NAME << ' '<< PROGRAM_VERSION << "\n\n"; os << _summary << "\n\n"; // print usage info string usage = _usage; int count=0; while (!usage.empty()) { auto pos = usage.find('\n'); os << (count++ == 0 ? "Usage: " : " ") << PROGRAM_NAME << ' ' << usage.substr(0, pos) << '\n'; if (pos != string::npos) usage = usage.substr(pos+1); else usage.clear(); } if (mode > 0) os << '\n'; // compute width of first column of help output unordered_map> linecols; size_t col1width=0; for (const OptSectPair &ospair : options()) { string::size_type pos; string line = ospair.first->helpline(); if ((pos = line.find('\t')) != string::npos) { linecols.emplace(ospair.first, pair(line.substr(0, pos), line.substr(pos+1))); col1width = max(col1width, pos); } } if (mode > 0) { bool (*isless)(const OptSectPair&, const OptSectPair&) = [](const OptSectPair &p1, const OptSectPair &p2) { return p1.first->longName() < p2.first->longName(); }; if (mode == 1) { isless = [](const OptSectPair &p1, const OptSectPair &p2) { char c1 = p1.first->shortName(), c2 = p2.first->shortName(); if (c1 == c2) return p1.first->longName() < p2.first->longName(); if (tolower(c1) == tolower(c2)) return c1 > c2; // lower case before upper case letters return tolower(c1) < tolower(c2); }; } sort(options().begin(), options().end(), isless); } // print summary of options col1width += 2; int sectno=-1; for (const OptSectPair &ospair : options()) { if (mode == 0 && ospair.second != sectno) { // first option in current section? sectno = ospair.second; if (section(sectno)) os << '\n' << section(sectno) << ":\n"; } Option *opt = ospair.first; os << " " << setw(col1width) << left << linecols[opt].first; os << setw(0) << linecols[opt].second << '\n'; } os << '\n' << _copyright << '\n'; }