changeset 850:688f28dd3241

irccd: remove short options, closes #1673 While here, use a brand new home made option parsing.
author David Demelier <markand@malikania.fr>
date Tue, 16 Jul 2019 20:49:36 +0200
parents 64f8f82ab110
children 7c5898a79671
files CHANGES.md MIGRATING.md irccd-test/main.cpp irccd/main.cpp irccdctl/cli.cpp irccdctl/main.cpp libirccd-test/irccd/test/cli_fixture.cpp libirccd/CMakeLists.txt libirccd/irccd/options.cpp libirccd/irccd/options.hpp man/irccdctl.1 tests/src/irccdctl/cli-rule-add/main.cpp tests/src/irccdctl/cli-rule-edit/main.cpp
diffstat 13 files changed, 443 insertions(+), 662 deletions(-) [+]
line wrap: on
line diff
--- 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:
 
--- 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
 -------
--- 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);
 }
 
 // }}}
--- 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<bot>(service);
 
-	init(argc, argv);
+	init();
 
 	// 1. Load commands.
 	for (const auto& f : transport_command::registry())
--- 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 <irccd/ctl/controller.hpp>
 
 #include <irccd/daemon/rule_service.hpp>
+#include <irccd/daemon/server.hpp>
 
 #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<std::string> 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<std::string> &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<std::string>& args)
+void rule_add_cli::exec(ctl::controller& ctl, const std::vector<std::string>& 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<std::size_t> 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<std::string>& args)
+void rule_edit_cli::exec(ctl::controller& ctl, const std::vector<std::string>& 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<std::string>& args)
+void server_connect_cli::exec(ctl::controller& ctl, const std::vector<std::string>& argv)
 {
-	std::vector<std::string> 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<std::string>& 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);
--- 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<connector>
+auto parse_connect_ip(const options::pack& result) -> std::unique_ptr<connector>
 {
-	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<ip_connector>(service, hostname, port, ipv4, ipv6);
+	return std::make_unique<ip_connector>(service, hostname->second, port->second, ipv4, ipv6);
 }
 
 /*
@@ -312,19 +344,18 @@
  *
  * -P file
  */
-auto parse_connect_local(const option::result& options) -> std::unique_ptr<connector>
+auto parse_connect_local([[maybe_unused]] const options::pack& options) -> std::unique_ptr<connector>
 {
 #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<local_connector>(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> 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<controller>(std::move(connector));
+	if (hflag)
+		ctl = std::make_unique<controller>(parse_connect_ip(options));
+	else if (pflag)
+		ctl = std::make_unique<controller>(parse_connect_local(options));
 }
 
-auto parse(int& argc, char**& argv) -> option::result
+auto parse(std::vector<std::string>& 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<std::string>& args)
 {
-	std::vector<std::string> 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<std::string> 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;
--- 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;
--- 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
--- 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 <markand@malikania.fr>
- *
- * 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 <cassert>
-
-#include "options.hpp"
-
-namespace irccd {
-
-namespace option {
-
-namespace {
-
-using iterator = std::vector<std::string>::iterator;
-using args = std::vector<std::string>;
-
-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<std::string>& 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<std::string> 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
--- 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 <markand@malikania.fr>
+ * Copyright (c) 2019 David Demelier <markand@malikania.fr>
  *
  * 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 <exception>
-#include <map>
+#include <initializer_list>
+#include <stdexcept>
 #include <string>
-#include <utility>
+#include <string_view>
+#include <tuple>
+#include <unordered_map>
 #include <vector>
 
-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::string>,
+	std::unordered_multimap<char, std::string>
+>;
 
 /**
- * \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 <typename InputIt>
+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<std::string, std::string>;
-
-/**
- * Define the allowed options.
- */
-using options = std::map<std::string, bool>;
-
-/**
- * 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<std::string>& args, const options& definition);
+template <typename String>
+inline auto parse(std::initializer_list<String> 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<std::string_view> 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
--- 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
--- 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);
--- 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);