# HG changeset patch # User David Demelier # Date 1563302976 -7200 # Node ID 688f28dd32415804d38e898540a757fcdf7d8248 # Parent 64f8f82ab110bfdc8442261e6d12a7341a571043 irccd: remove short options, closes #1673 While here, use a brand new home made option parsing. diff -r 64f8f82ab110 -r 688f28dd3241 CHANGES.md --- a/CHANGES.md Wed Jul 10 20:45:00 2019 +0200 +++ b/CHANGES.md Tue Jul 16 20:49:36 2019 +0200 @@ -8,7 +8,6 @@ - New sections `[paths]` and `[paths.plugin]` have been added to control standard paths for both irccd and plugins (#611), -- If Mercurial is found, the version is bundled in `irccd --version`, - Irccd no longer supports uid, gid, pid and daemon features (#846), - Sections `[identity]` and `[server]` have been merged (#905), - Local transports support SSL (#939), @@ -50,7 +49,8 @@ misc: -- The documentation is in pure manual pages now (#1674). +- The documentation is in pure manual pages now (#1674), +- All command line options are now in short form only (#1673). plugins: diff -r 64f8f82ab110 -r 688f28dd3241 MIGRATING.md --- a/MIGRATING.md Wed Jul 10 20:45:00 2019 +0200 +++ b/MIGRATING.md Tue Jul 16 20:49:36 2019 +0200 @@ -26,6 +26,8 @@ - The option `--host` has been renamed to `--hostname`, - The output style has been unified, - Options `-S` and `--ssl-verify` in server-connect have been removed. +- Connection options are now order dependant and must be set before the command + name. Plugins ------- diff -r 64f8f82ab110 -r 688f28dd3241 irccd-test/main.cpp --- a/irccd-test/main.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/irccd-test/main.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -125,6 +125,7 @@ // {{{ usage +[[noreturn]] void usage() { std::cerr << "usage: irccd-test [-c config] plugin-name" << std::endl; @@ -593,12 +594,15 @@ // {{{ load_plugins -void load_plugins(int argc, char** argv) +void load_plugins(const options::pack& result) { - if (argc <= 0) + const auto& [ args, _ ] = result; + + if (args.size() != 1U) usage(); + // NOTREACHED - daemon->get_plugins().load("test", boost::filesystem::exists(argv[0]) ? argv[0] : ""); + daemon->get_plugins().load("test", boost::filesystem::exists(args[0]) ? args[0] : ""); plugin = daemon->get_plugins().get("test"); } @@ -606,33 +610,28 @@ // {{{ load_config -auto load_config(const option::result& result) -> config +auto load_config(const options::pack& result) -> config { - auto it = result.find("-c"); + const auto& [ _, options ] = result; - if (it != result.end() || (it = result.find("--config")) != result.end()) + if (const auto it = options.find('c'); it != options.end()) return config(it->second); - auto cfg = config::search("irccd.conf"); - - return *cfg; + return config::search("irccd.conf").value_or(config()); } // }}} -// {{{ load_options +// {{{ load_cli -void load_options(int& argc, char**& argv) +auto load_cli(int argc, char** argv) -> options::pack { - static const option::options def{ - { "-c", true }, - { "--config", true } - }; - try { - daemon->set_config(load_config(option::read(argc, argv, def))); + return options::parse(argc, argv, "c:"); } catch (const std::exception& ex) { - throw std::runtime_error(str(format("%1%") % ex.what())); + std::cerr << "abort: " << ex.what() << std::endl; + usage(); + // NOTREACHED } } @@ -654,8 +653,17 @@ daemon->get_plugins().add_loader(std::move(loader)); #endif - load_options(argc, argv); - load_plugins(argc, argv); + const auto pack = load_cli(argc, argv); + + puts("ARGS"); + for (const auto& a : std::get<0>(pack)) + std::cout << a << std::endl; + puts("OPTIONS"); + for (const auto& [k,v] : std::get<1>(pack)) + std::cout << k << " = " << v << std::endl; + + load_config(pack); + load_plugins(pack); } // }}} diff -r 64f8f82ab110 -r 688f28dd3241 irccd/main.cpp --- a/irccd/main.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/irccd/main.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -47,15 +47,12 @@ // {{{ usage +[[noreturn]] void usage() { - std::cerr << "usage: irccd [options...]\n"; + std::cerr << "usage: irccd [-v] [-c config]\n"; std::cerr << " irccd info\n"; - std::cerr << " irccd version\n\n"; - std::cerr << "Available options:\n"; - std::cerr << " -c, --config file specify the configuration file\n"; - std::cerr << " -h, --help show this help\n"; - std::cerr << " -v, --verbose be verbose\n"; + std::cerr << " irccd version\n"; std::exit(1); } @@ -96,45 +93,29 @@ // {{{ init -void init(int& argc, char**& argv) +void init() { // Needed for some components. sys::set_program_name("irccd"); // Default logging to console. instance->get_log().set_verbose(false); - - -- argc; - ++ argv; } // }}} // {{{ parse -auto parse(int& argc, char**& argv) -> option::result +auto parse(int argc, char** argv) -> options::pack { - option::result result; + options::pack result; try { - option::options options{ - { "-c", true }, - { "--config", true }, - { "-h", false }, - { "--help", false }, - { "-v", false }, - { "--verbose", false }, - }; + result = options::parse(argc, argv, "c:v"); - result = option::read(argc, argv, options); - - for (const auto& pair : result) { - if (pair.first == "-h" || pair.first == "--help") - usage(); - // NOTREACHED - if (pair.first == "-v" || pair.first == "--verbose") + for (const auto& [ opt, _ ] : std::get<1>(result)) + if (opt == 'v') instance->get_log().set_verbose(true); - } } catch (const std::exception& ex) { instance->get_log().warning("irccd", "") << "abort: " << ex.what() << std::endl; usage(); @@ -147,11 +128,11 @@ // {{{ open -auto open(const option::result& result) -> config +auto open(const options::pack& result) -> config { - auto it = result.find("-c"); + const auto& [ _, options ] = result; - if (it != result.end() || (it = result.find("--config")) != result.end()) + if (const auto it = options.find('c'); it != options.end()) return config(it->second); const auto cfg = config::search("irccd.conf"); @@ -170,6 +151,9 @@ int main(int argc, char** argv) { + --argc; + ++argv; + using namespace irccd; using namespace irccd::daemon; @@ -178,7 +162,7 @@ instance = std::make_unique(service); - init(argc, argv); + init(); // 1. Load commands. for (const auto& f : transport_command::registry()) diff -r 64f8f82ab110 -r 688f28dd3241 irccdctl/cli.cpp --- a/irccdctl/cli.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/irccdctl/cli.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -27,6 +27,7 @@ #include #include +#include #include "cli.hpp" @@ -34,6 +35,7 @@ using irccd::json_util::pretty; using irccd::daemon::rule_error; +using irccd::daemon::server_error; namespace irccd::ctl { @@ -58,21 +60,6 @@ }; } -auto format(std::vector args) -> std::string -{ - auto result = option::read(args, { - { "-f", true }, - { "--format", true } - }); - - if (result.count("-f") > 0) - return result.find("-f")->second; - if (result.count("--format") > 0) - return result.find("--format")->second; - - return "native"; -} - auto align(std::string_view topic) -> std::ostream& { assert(topic.size() <= 16); @@ -230,26 +217,6 @@ }); } -auto parse(std::vector &args) -> option::result -{ - option::options options{ - { "-4", false }, - { "-6", false }, - { "-c", true }, - { "--command", true }, - { "-n", true }, - { "--nickname", true }, - { "-r", true }, - { "--realname", true }, - { "-s", false }, - { "--ssl", false }, - { "-u", true }, - { "--username", true } - }; - - return option::read(args, options); -} - } // !namespace // }}} @@ -493,25 +460,11 @@ return "rule-add"; } -void rule_add_cli::exec(ctl::controller& ctl, const std::vector& args) +void rule_add_cli::exec(ctl::controller& ctl, const std::vector& argv) { - static const option::options options{ - { "-c", true }, - { "--add-channel", true }, - { "-e", true }, - { "--add-event", true }, - { "-i", true }, - { "--index", true }, - { "-p", true }, - { "--add-plugin", true }, - { "-s", true }, - { "--add-server", true } - }; + const auto [ args, options ] = options::parse(argv.begin(), argv.end(), "c:e:i:p:s:"); - auto copy = args; - auto result = option::read(copy, options); - - if (copy.size() < 1) + if (args.size() < 1U) throw std::invalid_argument("rule-add requires at least 1 argument"); auto json = nlohmann::json::object({ @@ -523,29 +476,31 @@ }); // All sets. - for (const auto& pair : result) { - if (pair.first == "-c" || pair.first == "--add-channel") - json["channels"].push_back(pair.second); - if (pair.first == "-e" || pair.first == "--add-event") - json["events"].push_back(pair.second); - if (pair.first == "-p" || pair.first == "--add-plugin") - json["plugins"].push_back(pair.second); - if (pair.first == "-s" || pair.first == "--add-server") - json["servers"].push_back(pair.second); + for (const auto& [ option, value ] : options) { + switch (option) { + case 'c': + json["channels"].push_back(value); + break; + case 'e': + json["events"].push_back(value); + break; + case 'p': + json["plugins"].push_back(value); + break; + case 's': + json["servers"].push_back(value); + break; + case 'i': + if (const auto index = string_util::to_uint(value); index) + json["index"] = *index; + else + throw std::invalid_argument("invalid index argument"); + default: + break; + } } - // Index. - std::optional index; - - if (result.count("-i") > 0 && !(index = string_util::to_uint(result.find("-i")->second))) - throw std::invalid_argument("invalid index argument"); - if (result.count("--index") > 0 && !(index = string_util::to_uint(result.find("--index")->second))) - throw std::invalid_argument("invalid index argument"); - - if (index) - json["index"] = *index; - - json["action"] = copy[0]; + json["action"] = args[0]; request(ctl, json); } @@ -559,33 +514,11 @@ return "rule-edit"; } -void rule_edit_cli::exec(ctl::controller& ctl, const std::vector& args) +void rule_edit_cli::exec(ctl::controller& ctl, const std::vector& argv) { - static const option::options options{ - { "-a", true }, - { "--action", true }, - { "-c", true }, - { "--add-channel", true }, - { "-C", true }, - { "--remove-channel", true }, - { "-e", true }, - { "--add-event", true }, - { "-E", true }, - { "--remove-event", true }, - { "-p", true }, - { "--add-plugin", true }, - { "-P", true }, - { "--remove-plugin", true }, - { "-s", true }, - { "--add-server", true }, - { "-S", true }, - { "--remove-server", true }, - }; + const auto [ args, options ] = options::parse(argv.begin(), argv.end(), "a:c:C:e:E:p:P:s:S:"); - auto copy = args; - auto result = option::read(copy, options); - - if (copy.size() < 1) + if (args.size() < 1U) throw std::invalid_argument("rule-edit requires at least 1 argument"); auto json = nlohmann::json::object({ @@ -596,40 +529,44 @@ { "servers", nlohmann::json::array() } }); - for (const auto& pair : result) { - // Action. - if (pair.first == "-a" || pair.first == "--action") - json["action"] = pair.second; - - // Additions. - if (pair.first == "-c" || pair.first == "--add-channel") - json["add-channels"].push_back(pair.second); - if (pair.first == "-e" || pair.first == "--add-event") - json["add-events"].push_back(pair.second); - if (pair.first == "-p" || pair.first == "--add-plugin") - json["add-plugins"].push_back(pair.second); - if (pair.first == "-s" || pair.first == "--add-server") - json["add-servers"].push_back(pair.second); - - // Removals. - if (pair.first == "-C" || pair.first == "--remove-channel") - json["remove-channels"].push_back(pair.second); - if (pair.first == "-E" || pair.first == "--remove-event") - json["remove-events"].push_back(pair.second); - if (pair.first == "-P" || pair.first == "--remove-plugin") - json["remove-plugins"].push_back(pair.second); - if (pair.first == "-S" || pair.first == "--remove-server") - json["remove-servers"].push_back(pair.second); + for (const auto& [ option, value ] : options) { + switch (option) { + case 'a': + json["action"] = value; + break; + case 'c': + json["add-channels"].push_back(value); + break; + case 'e': + json["add-events"].push_back(value); + break; + case 'p': + json["add-plugins"].push_back(value); + break; + case 's': + json["add-servers"].push_back(value); + break; + case 'C': + json["remove-channels"].push_back(value); + break; + case 'E': + json["remove-events"].push_back(value); + break; + case 'P': + json["remove-plugins"].push_back(value); + break; + case 'S': + json["remove-servers"].push_back(value); + default: + break; + } } - // Index. - const auto index = string_util::to_uint(copy[0]); - - if (!index) + if (const auto index = string_util::to_uint(args[0]); index) + json["index"] = *index; + else throw rule_error(rule_error::invalid_index); - json["index"] = *index; - request(ctl, json); } @@ -783,45 +720,49 @@ return "server-connect"; } -void server_connect_cli::exec(ctl::controller& ctl, const std::vector& args) +void server_connect_cli::exec(ctl::controller& ctl, const std::vector& argv) { - std::vector copy(args); + const auto [ args, options ] = options::parse(argv.begin(), argv.end(), "46c:n:r:su:p:"); - option::result result = parse(copy); - option::result::const_iterator it; - - if (copy.size() < 2) + if (args.size() < 2) throw std::invalid_argument("server-connect requires at least 2 arguments"); auto object = nlohmann::json::object({ { "command", "server-connect" }, - { "name", copy[0] }, - { "hostname", copy[1] }, + { "name", args[0] }, + { "hostname", args[1] }, }); - if (copy.size() == 3) { - const auto port = string_util::to_int(copy[2]); - - if (!port) - throw std::invalid_argument("invalid port given"); - - object["port"] = *port; - } + for (const auto& [ option, value ] : options) { + switch (option) { + case 'p': + if (const auto port = string_util::to_int(value); port) + object["port"] = *port; + else + throw server_error(server_error::invalid_port); - if (result.count("-s") > 0 || result.count("--ssl") > 0) - object["ssl"] = true; - if ((it = result.find("-n")) != result.end() || (it = result.find("--nickname")) != result.end()) - object["nickname"] = it->second; - if ((it = result.find("-r")) != result.end() || (it = result.find("--realname")) != result.end()) - object["realname"] = it->second; - if ((it = result.find("-u")) != result.end() || (it = result.find("--username")) != result.end()) - object["username"] = it->second; - - // Mutually exclusive. - if ((it = result.find("-4")) != result.end()) - object["family"] = nlohmann::json::array({ "ipv4" }); - if ((it = result.find("-6")) != result.end()) - object["family"] = nlohmann::json::array({ "ipv6" }); + break; + case 's': + object["ssl"] = true; + break; + case 'n': + object["nickname"] = value; + break; + case 'r': + object["realname"] = value; + break; + case 'u': + object["username"] = value; + break; + case '4': + object["ipv4"] = true; + break; + case '6': + object["ipv6"] = true; + default: + break; + } + } request(ctl, object); } @@ -1179,7 +1120,12 @@ void watch_cli::exec(ctl::controller& ctl, const std::vector& args) { - const auto fmt = format(args); + std::string fmt = "native"; + + const auto [ _, options ] = options::parse(args.begin(), args.end(), "f:"); + + if (const auto it = options.find('f'); it != options.end()) + fmt = it->second; if (fmt != "native" && fmt != "json") throw std::invalid_argument("invalid format given: " + fmt); diff -r 64f8f82ab110 -r 688f28dd3241 irccdctl/main.cpp --- a/irccdctl/main.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/irccdctl/main.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -69,8 +69,36 @@ * ------------------------------------------------------------------- */ +[[noreturn]] void usage() { + std::cerr << "usage: irccdctl plugin-config id [variable] [value]\n"; + std::cerr << " irccdctl plugin-info id\n"; + std::cerr << " irccdctl plugin-list\n"; + std::cerr << " irccdctl plugin-load name\n"; + std::cerr << " irccdctl plugin-reload plugin plugin-unload plugin\n"; + std::cerr << " irccdctl rule-add [-c channel] [-e event] [-i index] [-o origin] [-s server] accept|drop\n"; + std::cerr << " irccdctl rule-edit [-a accept|drop] [-c|C channel] [-e|E event] [-o|O origin] [-s|S server] index\n"; + std::cerr << " irccdctl rule-info index\n"; + std::cerr << " irccdctl rule-list\n"; + std::cerr << " irccdctl rule-move from to\n"; + std::cerr << " irccdctl rule-remove index\n"; + std::cerr << " irccdctl server-connect [-46s] [-n nickname] [-r realname] [-u username] [-p port] id hostname\n"; + std::cerr << " irccdctl server-disconnect [server]\n"; + std::cerr << " irccdctl server-info server\n"; + std::cerr << " irccdctl server-invite server target channel\n"; + std::cerr << " irccdctl server-join server channel [password]\n"; + std::cerr << " irccdctl server-kick server target channel [reason]\n"; + std::cerr << " irccdctl server-list\n"; + std::cerr << " irccdctl server-me server target message\n"; + std::cerr << " irccdctl server-message server target message\n"; + std::cerr << " irccdctl server-mode server target mode [limit] [user] [mask]\n"; + std::cerr << " irccdctl server-nick server nickname\n"; + std::cerr << " irccdctl server-notice server target message\n"; + std::cerr << " irccdctl server-part server channel [reason]\n"; + std::cerr << " irccdctl server-reconnect [server]\n"; + std::cerr << " irccdctl server-topic server channel topic\n"; + std::cerr << " irccdctl watch [-f native|json]\n"; std::exit(1); } @@ -277,31 +305,35 @@ * * Parse internet connection from command line. * - * -t ip | ipv6 - * -h hostname or ip - * -p port + * -h hostname or ip address + * -p port (can be a string) + * -4 enable IPv4 (default) + * -6 enable IPv6 (default) */ -auto parse_connect_ip(std::string_view type, const option::result& options) -> std::unique_ptr +auto parse_connect_ip(const options::pack& result) -> std::unique_ptr { - option::result::const_iterator it; + const auto& [ _, options ] = result; + const auto hostname = options.find('h'); + const auto port = options.find('p'); - // Host (-h or --host). - if ((it = options.find("-h")) == options.end() && (it = options.find("--hostname")) == options.end()) - throw transport_error(transport_error::invalid_hostname); + /* + * Both are to true by default, setting one disable the second unless + * it is also specified. + */ + bool ipv4 = true; + bool ipv6 = true; - const auto hostname = it->second; + if (options.count('4')) + ipv6 = options.count('6'); + else if (options.count('6')) + ipv4 = options.count('4'); - // Port (-p or --port). - if ((it = options.find("-p")) == options.end() && (it = options.find("--port")) == options.end()) + if (hostname == options.end() || hostname->second.empty()) + throw transport_error(transport_error::invalid_hostname); + if (port == options.end() || port->second.empty()) throw transport_error(transport_error::invalid_port); - const auto port = it->second; - - // Type (-t or --type). - const auto ipv4 = type == "ip"; - const auto ipv6 = type == "ipv6"; - - return std::make_unique(service, hostname, port, ipv4, ipv6); + return std::make_unique(service, hostname->second, port->second, ipv4, ipv6); } /* @@ -312,19 +344,18 @@ * * -P file */ -auto parse_connect_local(const option::result& options) -> std::unique_ptr +auto parse_connect_local([[maybe_unused]] const options::pack& options) -> std::unique_ptr { #if !BOOST_OS_WINDOWS - option::result::const_iterator it; + const auto& [ _, options ] = result; + const auto path = options.find('P'); - if ((it = options.find("-P")) == options.end() && (it = options.find("--path")) == options.end()) - throw std::invalid_argument("missing path parameter (-P or --path)"); + if (path == options.end() || path->second.empty()) + throw transport_error(transport_error::invalid_path); return std::make_unique(service, it->second); #else - (void)options; - - throw std::invalid_argument("unix connection not supported on Windows"); + throw transport_error(transport_error::not_supported); #endif } @@ -334,60 +365,38 @@ * * Generic parsing of command line option for connection. */ -void parse_connect(const option::result& options) +void parse_connect(const options::pack& options) { - assert(options.count("-t") > 0 || options.count("--type") > 0); - - auto it = options.find("-t"); - - if (it == options.end()) - it = options.find("--type"); + const auto hflag = std::get<1>(options).count('h') > 0; + const auto pflag = std::get<1>(options).count('P') > 0; - std::unique_ptr connector; + if (hflag && pflag) + throw std::invalid_argument("-h and -P are mutually exclusive"); - if (it->second == "ip" || it->second == "ipv6") - connector = parse_connect_ip(it->second, options); - else if (it->second == "unix") - connector = parse_connect_local(options); - else - throw std::invalid_argument(str(format("invalid type given: %1%") % it->second)); - - if (connector) - ctl = std::make_unique(std::move(connector)); + if (hflag) + ctl = std::make_unique(parse_connect_ip(options)); + else if (pflag) + ctl = std::make_unique(parse_connect_local(options)); } -auto parse(int& argc, char**& argv) -> option::result +auto parse(std::vector& args) -> options::pack { - // 1. Parse command line options. - option::options def{ - { "-c", true }, - { "--config", true }, - { "-h", true }, - { "--help", false }, - { "--hostname", true }, - { "-p", true }, - { "--port", true }, - { "-P", true }, - { "--path", true }, - { "-t", true }, - { "--type", true }, - { "-v", false }, - { "--verbose", false } - }; - - option::result result; + options::pack result; try { - result = option::read(argc, argv, def); + // 1. Collect the options before the command name. + auto begin = args.begin(); + auto end = args.end(); - if (result.count("--help") > 0 || result.count("-h") > 0) - usage(); - // NOTREACHED + result = options::parse(begin, end, "c:h:p:P:v!"); + + for (const auto& [ opt, _ ] : std::get<1>(result)) + if (opt == 'v') + verbose = true; - if (result.count("-v") != 0 || result.count("--verbose") != 0) - verbose = true; + args.erase(args.begin(), begin); } catch (const std::exception& ex) { - std::cerr << "irccdctl: " << ex.what() << std::endl; + std::cerr << "abort: " << ex.what() << std::endl; usage(); } @@ -458,13 +467,10 @@ throw std::invalid_argument("no alias or command named " + name); } -void init(int &argc, char **&argv) +void init() { sys::set_program_name("irccdctl"); - -- argc; - ++ argv; - for (const auto& f : cli::registry) { auto c = f(); @@ -498,13 +504,8 @@ service.reset(); } -void do_exec(int argc, char** argv) +void do_exec(const std::vector& args) { - std::vector args; - - for (int i = 0; i < argc; ++i) - args.push_back(argv[i]); - enqueue(args); for (const auto& req : requests) { @@ -520,10 +521,20 @@ int main(int argc, char** argv) { - irccd::ctl::init(argc, argv); + --argc; + ++argv; + + // 0. Keep track of parsed arguments. + std::vector cli(argc); + + for (int i = 0; i < argc; ++i) + cli[i] = argv[i]; + + irccd::ctl::init(); // 1. Read command line arguments. - const auto result = irccd::ctl::parse(argc, argv); + const auto result = irccd::ctl::parse(cli); + const auto& [ args, options ] = result; /* * 2. Open optional config by command line or by searching it @@ -535,23 +546,18 @@ * 3. From the configuration file searched through directories */ try { - if (result.count("-t") > 0 || result.count("--type") > 0) - irccd::ctl::parse_connect(result); - - auto it = result.find("-c"); + irccd::ctl::parse_connect(result); - if (it != result.end() || (it = result.find("--config")) != result.end()) + if (const auto it = options.find('c'); it != options.end()) irccd::ctl::read(it->second); - else { - if (auto conf = irccd::config::search("irccdctl.conf")) - irccd::ctl::read(*conf); - } + else if (const auto conf = irccd::config::search("irccdctl.conf")) + irccd::ctl::read(*conf); } catch (const std::exception& ex) { std::cerr << "abort: " << ex.what() << std::endl; return 1; } - if (argc <= 0) + if (cli.size() <= 0) irccd::ctl::usage(); // NOTREACHED @@ -562,7 +568,7 @@ try { irccd::ctl::do_connect(); - irccd::ctl::do_exec(argc, argv); + irccd::ctl::do_exec(cli); } catch (const std::system_error& ex) { std::cerr << "abort: " << ex.code().message() << std::endl; return 1; diff -r 64f8f82ab110 -r 688f28dd3241 libirccd-test/irccd/test/cli_fixture.cpp --- a/libirccd-test/irccd/test/cli_fixture.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/libirccd-test/irccd/test/cli_fixture.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -87,7 +87,7 @@ { std::ostringstream oss; - oss << irccdctl_ << " -t ip --hostname 127.0.0.1 -p " << port_ << " "; + oss << irccdctl_ << " -h 127.0.0.1 -p " << port_ << " "; oss << string_util::join(args, " "); proc::ipstream stream_out, stream_err; diff -r 64f8f82ab110 -r 688f28dd3241 libirccd/CMakeLists.txt --- a/libirccd/CMakeLists.txt Wed Jul 10 20:45:00 2019 +0200 +++ b/libirccd/CMakeLists.txt Tue Jul 16 20:49:36 2019 +0200 @@ -34,7 +34,6 @@ ${libirccd_SOURCE_DIR}/irccd/ini_util.hpp ${libirccd_SOURCE_DIR}/irccd/json_util.cpp ${libirccd_SOURCE_DIR}/irccd/json_util.hpp - ${libirccd_SOURCE_DIR}/irccd/options.cpp ${libirccd_SOURCE_DIR}/irccd/options.hpp ${libirccd_SOURCE_DIR}/irccd/stream.hpp ${libirccd_SOURCE_DIR}/irccd/string_util.cpp diff -r 64f8f82ab110 -r 688f28dd3241 libirccd/irccd/options.cpp --- a/libirccd/irccd/options.cpp Wed Jul 10 20:45:00 2019 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,193 +0,0 @@ -/* - * options.cpp -- parse Unix command line options - * - * Copyright (c) 2015-2019 David Demelier - * - * Permission to use, copy, modify, and/or distribute this software for any - * purpose with or without fee is hereby granted, provided that the above - * copyright notice and this permission notice appear in all copies. - * - * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES - * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF - * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR - * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES - * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN - * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF - * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. - */ - -#include "sysconfig.hpp" - -#include - -#include "options.hpp" - -namespace irccd { - -namespace option { - -namespace { - -using iterator = std::vector::iterator; -using args = std::vector; - -inline bool is_option(const std::string& arg) noexcept -{ - return arg.size() >= 2 && arg[0] == '-'; -} - -inline bool is_long_option(const std::string& arg) noexcept -{ - assert(is_option(arg)); - - return arg.size() >= 3 && arg[1] == '-'; -} - -inline bool is_short_simple(const std::string& arg) noexcept -{ - assert(is_option(arg) && !is_long_option(arg)); - - return arg.size() == 2; -} - -void parse_long_option(result& result, args& args, iterator& it, iterator& end, const options& definition) -{ - auto arg = *it++; - auto opt = definition.find(arg); - - if (opt == definition.end()) - throw invalid_option(arg); - - // Need argument? - if (opt->second) { - if (it == end || is_option(*it)) - throw missing_value(arg); - - result.insert(std::make_pair(arg, *it++)); - it = args.erase(args.begin(), it); - end = args.end(); - } else { - result.insert(std::make_pair(arg, "")); - it = args.erase(args.begin()); - end = args.end(); - } -} - -void parse_short_option_simple(result& result, args& args, iterator& it, iterator &end, const options& definition) -{ - /* - * Here two cases: - * - * -v (no option) - * -c value - */ - auto arg = *it++; - auto opt = definition.find(arg); - - if (opt == definition.end()) - throw invalid_option(arg); - - // Need argument? - if (opt->second) { - if (it == end || is_option(*it)) - throw missing_value(arg); - - result.insert(std::make_pair(arg, *it++)); - it = args.erase(args.begin(), it); - end = args.end(); - } else { - result.insert(std::make_pair(arg, "")); - it = args.erase(args.begin()); - end = args.end(); - } -} - -void parse_short_option_compressed(result& result, args& args, iterator& it, iterator &end, const options& definition) -{ - /* - * Here multiple scenarios: - * - * 1. -abc (-a -b -c if all are simple boolean arguments) - * 2. -vc foo.conf (-v -c foo.conf if -c is argument dependant) - * 3. -vcfoo.conf (-v -c foo.conf also) - */ - auto value = it->substr(1); - auto len = value.length(); - int toremove = 1; - - for (std::size_t i = 0; i < len; ++i) { - auto arg = std::string{'-'} + value[i]; - auto opt = definition.find(arg); - - if (opt == definition.end()) - throw invalid_option(arg); - - if (opt->second) { - if (i == (len - 1)) { - // End of string, get the next argument (see 2.). - if (++it == end || is_option(*it)) - throw missing_value(arg); - - result.insert(std::make_pair(arg, *it)); - toremove += 1; - } else { - result.insert(std::make_pair(arg, value.substr(i + 1))); - i = len; - } - } else - result.insert(std::make_pair(arg, "")); - } - - it = args.erase(args.begin(), args.begin() + toremove); - end = args.end(); -} - -void parse_short_option(result& result, args& args, iterator& it, iterator &end, const options& definition) -{ - if (is_short_simple(*it)) - parse_short_option_simple(result, args, it, end, definition); - else - parse_short_option_compressed(result, args, it, end, definition); -} - -} // !namespace - -result read(std::vector& args, const options& definition) -{ - result result; - - auto it = args.begin(); - auto end = args.end(); - - while (it != end) { - if (!is_option(*it)) - break; - - if (is_long_option(*it)) - parse_long_option(result, args, it, end, definition); - else - parse_short_option(result, args, it, end, definition); - } - - return result; -} - -result read(int& argc, char**& argv, const options& definition) -{ - std::vector args; - - for (int i = 0; i < argc; ++i) - args.push_back(argv[i]); - - auto before = args.size(); - auto result = read(args, definition); - - argc -= before - args.size(); - argv += before - args.size(); - - return result; -} - -} // !option - -} // !irccd diff -r 64f8f82ab110 -r 688f28dd3241 libirccd/irccd/options.hpp --- a/libirccd/irccd/options.hpp Wed Jul 10 20:45:00 2019 +0200 +++ b/libirccd/irccd/options.hpp Tue Jul 16 20:49:36 2019 +0200 @@ -1,7 +1,7 @@ /* - * options.hpp -- parse Unix command line options + * options.hpp -- C++ similar interface to getopt(3) * - * Copyright (c) 2015-2019 David Demelier + * Copyright (c) 2019 David Demelier * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted, provided that the above @@ -21,145 +21,165 @@ /** * \file options.hpp - * \brief Basic Unix options parser. + * \brief C++ similar interface to getopt(3). */ -#include "sysconfig.hpp" - -#include -#include +#include +#include #include -#include +#include +#include +#include #include -namespace irccd { - -/** - * Namespace for options parsing. - */ -namespace option { - /** - * \brief This exception is thrown when an invalid option has been found. + * \brief C++ similar interface to getopt(3). */ -class invalid_option : public std::exception { -private: - std::string message_; - std::string name_; - -public: - /** - * Construct the exception. - * - * \param name the argument missing - */ - inline invalid_option(std::string name) - : name_(std::move(name)) - { - message_ = std::string("invalid option: ") + name_; - } +namespace irccd::options { - /** - * Get the option name. - * - * \return the name - */ - inline const std::string& name() const noexcept - { - return name_; - } - - /** - * Get the error message. - * - * \return the error message - */ - const char* what() const noexcept override - { - return message_.c_str(); - } -}; +/** + * Store the positional arguments and options. + */ +using pack = std::tuple< + std::vector, + std::unordered_multimap +>; /** - * \brief This exception is thrown when an option requires a value and no value - * has been given. + * Parse a collection of options and arguments. + * + * This function uses the same format as getopt(3) function, you need specify + * each option in the fmt string and add a colon after the option character if + * it requires a value. + * + * If a -- option appears in the argument list, it stops option parsing and all + * next tokens are considered arguments even if they start with an hyphen. + * + * If the exlamation mark appears in the fmt argument, the function will stop + * parsing tokens immediately when one argument is not an option. + * + * This function explicitly takes references to it and end parameters to allow + * the user to determine the number of tokens actually parsed. + * + * Example of format strings: + * + * - "abc": are all three boolean options, + * - "c:v": v is a boolean option c requires a value. + * + * Example of invocation: + * + * - `mycli -v -a`: is similar to `-va` if both 'v' and 'a' are boolean options, + * - `mycli -v -- -c`: -c will be a positional argument rather than an option + * but '-v' is still an option. + * + * \tparam InputIt must dereference a string type (literal, std::string_view or + * std::string) + * \param it the first item + * \param end the next item + * \param fmt the format string + * \return the result */ -class missing_value : public std::exception { -private: - std::string message_; - std::string name_; +template +inline auto parse(InputIt&& it, InputIt&& end, std::string_view fmt) -> pack +{ + pack result; -public: - /** - * Construct the exception. - * - * \param name the option that requires a value - */ - inline missing_value(std::string name) - : name_(std::move(name)) - { - message_ = std::string("missing argument for: ") + name_; - } + for (; it != end; ++it) { + const std::string_view token(*it); - /** - * Get the option name. - * - * \return the name - */ - inline const std::string& name() const noexcept - { - return name_; - } + /* + * Special token that stops parsing options, all next tokens + * will be considered as positional arguments. + */ + if (token == "--") { + for (++it; it != end; ++it) + std::get<0>(result).push_back(std::string(*it)); + break; + } + + // Is this a positional argument? + if (token.compare(0U, 1U, "-") != 0) { + // Stop parsing in case of '!' in format string. + if (fmt.find('!') != std::string_view::npos) + break; + + std::get<0>(result).push_back(std::string(token)); + continue; + } + + const auto sub = token.substr(1); + + for (std::size_t i = 0U; i < sub.size(); ++i) { + const auto idx = fmt.find(sub[i]); - /** - * Get the error message. - * - * \return the error message - */ - const char* what() const noexcept override - { - return message_.c_str(); - } -}; + if (idx == std::string_view::npos) + throw std::runtime_error("invalid option"); + + // This is a boolean value. + if (fmt.compare(idx + 1U, 1U, ":") != 0) { + std::get<1>(result).emplace(sub[i], ""); + continue; + } + + /* + * The value is adjacent to the option (e.g. + * -csuper.conf). + */ + if (idx + 1U < sub.size()) { + std::get<1>(result).emplace(sub[i], std::string(sub.substr(i + 1))); + break; + } + + // Option is the next token (e.g. -c super.conf). + if (++it == end || std::string_view(*it).compare(0U, 1U, "-") == 0) + throw std::runtime_error("option require a value"); + + std::get<1>(result).emplace(sub[i], std::string(*it)); + } + } + + return result; +} /** - * Packed multimap of options. - */ -using result = std::multimap; - -/** - * Define the allowed options. - */ -using options = std::map; - -/** - * Extract the command line options and return a result. + * Convenient overload with an initializer_list. * + * \tparam StringType must be either a std::string or std::string_view * \param args the arguments - * \param definition - * \warning the arguments vector is modified in place to remove parsed options - * \throw missing_value - * \throw invalid_option + * \param fmt the format string * \return the result */ -result read(std::vector& args, const options& definition); +template +inline auto parse(std::initializer_list args, std::string_view fmt) -> pack +{ + auto begin = args.begin(); + auto end = args.end(); + + return parse(begin, end, fmt); +} /** - * Overloaded function for usage with main() arguments. + * Convenient overload for main() arguments. * * \param argc the number of arguments - * \param argv the argument vector - * \param definition - * \note don't forget to remove the first argv[0] argument - * \warning the argc and argv are modified in place to remove parsed options - * \throw missing_value - * \throw invalid_option + * \param argv the arguments + * \param fmt the format string * \return the result */ -result read(int& argc, char**& argv, const options& definition); +inline auto parse(int argc, char** argv, std::string_view fmt) -> pack +{ + std::vector args(argc); + + for (int i = 0; i < argc; ++i) + args[i] = argv[i]; -} // !option + auto begin = args.begin(); + auto end = args.end(); -} // !irccd + return parse(begin, end, fmt); +} + + +} // !irccd::options #endif // !IRCCD_OPTIONS_HPP diff -r 64f8f82ab110 -r 688f28dd3241 man/irccdctl.1 --- a/man/irccdctl.1 Wed Jul 10 20:45:00 2019 +0200 +++ b/man/irccdctl.1 Tue Jul 16 20:49:36 2019 +0200 @@ -87,9 +87,9 @@ .Op Fl n Ar nickname .Op Fl r Ar realname .Op Fl u Ar username +.Op Fl p Ar port .Ar id .Ar hostname -.Op Ar port .\" server-disconnect .Nm .Cm server-disconnect @@ -183,8 +183,43 @@ .Pp The general syntax for running an irccdctl command is: .Bd -literal -offset Ds -irccdctl command options arg1 arg2 arg3 ... argn +irccdctl global-options command command-options command-arguments .Ed +.Pp +The following +.Ar global-options +are available +.Em before +the +.Ar command +name: +.Bl -tag -width 12n +.It Fl 4 +Try to connect using IPv4. Specifying this option +unset +.Fl 6 +option, set it explicitly to enable both families. +.It Fl 6 +Try to connect using IPv6. Specifying this option will +unset +.Fl 4 +option, set it explicitly to enable both families. +.It Fl h Ar hostname +Connect to the IP address or hostname. +.It Fl p Ar port +Use the +.Ar port +number or service name. +.It Fl P Ar path +Connect to the UNIX local socket specified by +.Ar path . +.El +.Pp +Note: options +.Fl h +and +.Fl P +are mutually exclusive. .\" COMMANDS .Sh COMMANDS .Bl -tag -width xxxxxxxx-yyyyyyyyy @@ -304,9 +339,7 @@ .Ar id must not be already present. The argument .Ar hostname -can be a hostname or IP address. If the -.Ar port -number is not specified, the default value of 6667 is used instead. +can be a hostname or IP address. .Pp Available options: .Bl -tag -width 12n @@ -318,6 +351,8 @@ specify a real name. .It Fl u Ar username specify a user name. +.It Fl p Ar port +use the specific port, otherwise 6667 is used. .El .\" server-disconnect .It Cm server-disconnect diff -r 64f8f82ab110 -r 688f28dd3241 tests/src/irccdctl/cli-rule-add/main.cpp --- a/tests/src/irccdctl/cli-rule-add/main.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/tests/src/irccdctl/cli-rule-add/main.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -43,10 +43,10 @@ { const auto [code, out, err] = exec({ "rule-add", - "-c c1", "--add-channel c2", - "-e onMessage", "--add-event onCommand", - "-p p1", "--add-plugin p2", - "-s s1", "--add-server s2", + "-c c1", "-c c2", + "-e onMessage", "-e onCommand", + "-p p1", "-p p2", + "-s s1", "-s s2", "drop" }); @@ -75,7 +75,7 @@ start(); { - const auto [code, out, err] = exec({ "rule-add", "-s s1", "--add-server s2", "drop" }); + const auto [code, out, err] = exec({ "rule-add", "-s s1", "-s s2", "drop" }); BOOST_TEST(!code); BOOST_TEST(out.size() == 0U); @@ -102,7 +102,7 @@ start(); { - const auto [code, out, err] = exec({ "rule-add", "-c c1", "--add-channel c2", "drop" }); + const auto [code, out, err] = exec({ "rule-add", "-c c1", "-c c2", "drop" }); BOOST_TEST(!code); BOOST_TEST(out.size() == 0U); @@ -129,7 +129,7 @@ start(); { - const auto [code, out, err] = exec({ "rule-add", "-p p1", "--add-plugin p2", "drop" }); + const auto [code, out, err] = exec({ "rule-add", "-p p1", "-p p2", "drop" }); BOOST_TEST(!code); BOOST_TEST(out.size() == 0U); @@ -156,7 +156,7 @@ start(); { - const auto [code, out, err] = exec({ "rule-add", "-e onMessage", "--add-event onCommand", "drop" }); + const auto [code, out, err] = exec({ "rule-add", "-e onMessage", "-e onCommand", "drop" }); BOOST_TEST(!code); BOOST_TEST(out.size() == 0U); @@ -184,7 +184,7 @@ { start(); - const auto [code, out, err] = exec({ "rule-add", "-p p1", "--add-plugin p2", "break" }); + const auto [code, out, err] = exec({ "rule-add", "-p p1", "-p p2", "break" }); BOOST_TEST(code); BOOST_TEST(out.size() == 0U); diff -r 64f8f82ab110 -r 688f28dd3241 tests/src/irccdctl/cli-rule-edit/main.cpp --- a/tests/src/irccdctl/cli-rule-edit/main.cpp Wed Jul 10 20:45:00 2019 +0200 +++ b/tests/src/irccdctl/cli-rule-edit/main.cpp Tue Jul 16 20:49:36 2019 +0200 @@ -53,8 +53,8 @@ { const auto [code, out, err] = exec({ "rule-edit", - "-s ts1", "--add-server ts2", - "-S s1", "--remove-server s2", + "-s ts1", "-s ts2", + "-S s1", "-S s2", "0" }); @@ -84,8 +84,8 @@ { const auto [code, out, err] = exec({ "rule-edit", - "-c tc1", "--add-channel tc2", - "-C c1", "--remove-channel c2", + "-c tc1", "-c tc2", + "-C c1", "-C c2", "0" }); @@ -115,8 +115,8 @@ { const auto [code, out, err] = exec({ "rule-edit", - "-p tp1", "--add-plugin tp2", - "-P p1", "--remove-plugin p2", + "-p tp1", "-p tp2", + "-P p1", "-P p2", "0" }); @@ -146,8 +146,8 @@ { const auto [code, out, err] = exec({ "rule-edit", - "-e onKick", "--add-event onNickname", - "-E onMessage", "--remove-event onCommand", + "-e onKick", "-e onNickname", + "-E onMessage", "-E onCommand", "0" }); @@ -198,32 +198,6 @@ } } -BOOST_AUTO_TEST_CASE(action_2) -{ - start(); - - { - const auto [code, out, err] = exec({ "rule-edit", "--action accept", "0" }); - - BOOST_TEST(out.size() == 0U); - BOOST_TEST(err.size() == 0U); - } - - { - const auto [code, out, err] = exec({ "rule-list" }); - - BOOST_TEST(!code); - BOOST_TEST(out.size() == 6U); - BOOST_TEST(err.size() == 0U); - BOOST_TEST(out[0] == "rule: 0"); - BOOST_TEST(out[1] == "servers: s1 s2 "); - BOOST_TEST(out[2] == "channels: c1 c2 "); - BOOST_TEST(out[3] == "plugins: p1 p2 "); - BOOST_TEST(out[4] == "events: onCommand onMessage "); - BOOST_TEST(out[5] == "action: accept"); - } -} - BOOST_AUTO_TEST_SUITE(errors) BOOST_AUTO_TEST_CASE(invalid_index_1) @@ -254,7 +228,7 @@ { start(); - const auto [code, out, err] = exec({ "rule-edit", "--action break", "0" }); + const auto [code, out, err] = exec({ "rule-edit", "-a break", "0" }); BOOST_TEST(code); BOOST_TEST(out.size() == 0U);