feat: allow optional arguments in tr_getopt() (#7510)

* feat: allow optional arguments in `tr_getopt()`

* test: new tests for optional arg

* code review: `using Arg = tr_option::Arg`

* refactor: static assert option array size

* test: add new tests for missing arguments in the middle

* test: static auto constexpr
This commit is contained in:
Yat Ho
2025-10-16 01:08:11 +08:00
committed by GitHub
parent a7a5bc38ad
commit 9c14fa58d8
10 changed files with 536 additions and 402 deletions

View File

@@ -48,36 +48,41 @@ sig_atomic_t manualUpdate = false;
char const* torrentPath = nullptr; char const* torrentPath = nullptr;
auto constexpr Options = std::array<tr_option, 20>{ using Arg = tr_option::Arg;
{ { 'b', "blocklist", "Enable peer blocklists", "b", false, nullptr }, auto constexpr Options = std::array<tr_option, 20>{ {
{ 'B', "no-blocklist", "Disable peer blocklists", "B", false, nullptr }, { 'b', "blocklist", "Enable peer blocklists", "b", Arg::None, nullptr },
{ 'd', "downlimit", "Set max download speed in " SPEED_K_STR, "d", true, "<speed>" }, { 'B', "no-blocklist", "Disable peer blocklists", "B", Arg::None, nullptr },
{ 'D', "no-downlimit", "Don't limit the download speed", "D", false, nullptr }, { 'd', "downlimit", "Set max download speed in " SPEED_K_STR, "d", Arg::Required, "<speed>" },
{ 910, "encryption-required", "Encrypt all peer connections", "er", false, nullptr }, { 'D', "no-downlimit", "Don't limit the download speed", "D", Arg::None, nullptr },
{ 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", false, nullptr }, { 910, "encryption-required", "Encrypt all peer connections", "er", Arg::None, nullptr },
{ 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", false, nullptr }, { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", Arg::None, nullptr },
{ 'f', "finish", "Run a script when the torrent finishes", "f", true, "<script>" }, { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", Arg::None, nullptr },
{ 'g', "config-dir", "Where to find configuration files", "g", true, "<path>" }, { 'f', "finish", "Run a script when the torrent finishes", "f", Arg::Required, "<script>" },
{ 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, nullptr }, { 'g', "config-dir", "Where to find configuration files", "g", Arg::Required, "<path>" },
{ 'M', "no-portmap", "Disable portmapping", "M", false, nullptr }, { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", Arg::None, nullptr },
{ 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", true, "<port>" }, { 'M', "no-portmap", "Disable portmapping", "M", Arg::None, nullptr },
{ 't', { 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", Arg::Required, "<port>" },
"tos", { 't',
"Peer socket DSCP / ToS setting (number, or a DSCP string, e.g. 'af11' or 'cs0', default=" TR_DEFAULT_PEER_SOCKET_TOS_STR "tos",
")", "Peer socket DSCP / ToS setting (number, or a DSCP string, e.g. 'af11' or 'cs0', default=" TR_DEFAULT_PEER_SOCKET_TOS_STR
"t", ")",
true, "t",
"<dscp-or-tos>" }, Arg::Required,
{ 'u', "uplimit", "Set max upload speed in " SPEED_K_STR, "u", true, "<speed>" }, "<dscp-or-tos>" },
{ 'U', "no-uplimit", "Don't limit the upload speed", "U", false, nullptr }, { 'u', "uplimit", "Set max upload speed in " SPEED_K_STR, "u", Arg::Required, "<speed>" },
{ 'v', "verify", "Verify the specified torrent", "v", false, nullptr }, { 'U', "no-uplimit", "Don't limit the upload speed", "U", Arg::None, nullptr },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 'v', "verify", "Verify the specified torrent", "v", Arg::None, nullptr },
{ 'w', "download-dir", "Where to save downloaded data", "w", true, "<path>" }, { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
{ 500, "sequential-download", "Download pieces sequentially", "seq", false, nullptr }, { 'w', "download-dir", "Where to save downloaded data", "w", Arg::Required, "<path>" },
{ 500, "sequential-download", "Download pieces sequentially", "seq", Arg::None, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } } { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
}; } };
static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
int parseCommandLine(tr_variant*, int argc, char const** argv); int parseCommandLine(tr_variant*, int argc, char const** argv);
void sigHandler(int signal); void sigHandler(int signal);

View File

@@ -75,79 +75,86 @@ char constexpr Usage[] = "Transmission " LONG_VERSION_STRING
"\n" "\n"
"Usage: transmission-daemon [options]"; "Usage: transmission-daemon [options]";
// --- Config File using Arg = tr_option::Arg;
auto constexpr Options = std::array<tr_option, 47>{ {
auto constexpr Options = std::array<tr_option, 47>{ { 'a', "allowed", "Allowed IP addresses. (Default: " TR_DEFAULT_RPC_WHITELIST ")", "a", Arg::Required, "<list>" },
{ { 'a', "allowed", "Allowed IP addresses. (Default: " TR_DEFAULT_RPC_WHITELIST ")", "a", true, "<list>" }, { 'b', "blocklist", "Enable peer blocklists", "b", Arg::None, nullptr },
{ 'b', "blocklist", "Enable peer blocklists", "b", false, nullptr }, { 'B', "no-blocklist", "Disable peer blocklists", "B", Arg::None, nullptr },
{ 'B', "no-blocklist", "Disable peer blocklists", "B", false, nullptr }, { 'c', "watch-dir", "Where to watch for new torrent files", "c", Arg::Required, "<directory>" },
{ 'c', "watch-dir", "Where to watch for new torrent files", "c", true, "<directory>" }, { 'C', "no-watch-dir", "Disable the watch-dir", "C", Arg::None, nullptr },
{ 'C', "no-watch-dir", "Disable the watch-dir", "C", false, nullptr }, { 941, "incomplete-dir", "Where to store new torrents until they're complete", nullptr, Arg::Required, "<directory>" },
{ 941, "incomplete-dir", "Where to store new torrents until they're complete", nullptr, true, "<directory>" }, { 942, "no-incomplete-dir", "Don't store incomplete torrents in a different location", nullptr, Arg::None, nullptr },
{ 942, "no-incomplete-dir", "Don't store incomplete torrents in a different location", nullptr, false, nullptr }, { 'd', "dump-settings", "Dump the settings and exit", "d", Arg::None, nullptr },
{ 'd', "dump-settings", "Dump the settings and exit", "d", false, nullptr }, { 943, "default-trackers", "Trackers for public torrents to use automatically", nullptr, Arg::Required, "<list>" },
{ 943, "default-trackers", "Trackers for public torrents to use automatically", nullptr, true, "<list>" }, { 'e', "logfile", "Dump the log messages to this filename", "e", Arg::Required, "<filename>" },
{ 'e', "logfile", "Dump the log messages to this filename", "e", true, "<filename>" }, { 'f', "foreground", "Run in the foreground instead of daemonizing", "f", Arg::None, nullptr },
{ 'f', "foreground", "Run in the foreground instead of daemonizing", "f", false, nullptr }, { 'g', "config-dir", "Where to look for configuration files", "g", Arg::Required, "<path>" },
{ 'g', "config-dir", "Where to look for configuration files", "g", true, "<path>" }, { 'p', "port", "RPC port (Default: " TR_DEFAULT_RPC_PORT_STR ")", "p", Arg::Required, "<port>" },
{ 'p', "port", "RPC port (Default: " TR_DEFAULT_RPC_PORT_STR ")", "p", true, "<port>" }, { 't', "auth", "Require authentication", "t", Arg::None, nullptr },
{ 't', "auth", "Require authentication", "t", false, nullptr }, { 'T', "no-auth", "Don't require authentication", "T", Arg::None, nullptr },
{ 'T', "no-auth", "Don't require authentication", "T", false, nullptr }, { 'u', "username", "Set username for authentication", "u", Arg::Required, "<username>" },
{ 'u', "username", "Set username for authentication", "u", true, "<username>" }, { 'v', "password", "Set password for authentication", "v", Arg::Required, "<password>" },
{ 'v', "password", "Set password for authentication", "v", true, "<password>" }, { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 810,
{ 810, "log-level", "Must be 'critical', 'error', 'warn', 'info', 'debug', or 'trace'.", nullptr, true, "<level>" }, "log-level",
{ 811, "log-error", "Deprecated. Use --log-level=error", nullptr, false, nullptr }, "Must be 'critical', 'error', 'warn', 'info', 'debug', or 'trace'.",
{ 812, "log-info", "Deprecated. Use --log-level=info", nullptr, false, nullptr }, nullptr,
{ 813, "log-debug", "Deprecated. Use --log-level=debug", nullptr, false, nullptr }, Arg::Required,
{ 'w', "download-dir", "Where to save downloaded data", "w", true, "<path>" }, "<level>" },
{ 800, "paused", "Pause all torrents on startup", nullptr, false, nullptr }, { 811, "log-error", "Deprecated. Use --log-level=error", nullptr, Arg::None, nullptr },
{ 'o', "dht", "Enable distributed hash tables (DHT)", "o", false, nullptr }, { 812, "log-info", "Deprecated. Use --log-level=info", nullptr, Arg::None, nullptr },
{ 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", false, nullptr }, { 813, "log-debug", "Deprecated. Use --log-level=debug", nullptr, Arg::None, nullptr },
{ 'y', "lpd", "Enable local peer discovery (LPD)", "y", false, nullptr }, { 'w', "download-dir", "Where to save downloaded data", "w", Arg::Required, "<path>" },
{ 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", false, nullptr }, { 800, "paused", "Pause all torrents on startup", nullptr, Arg::None, nullptr },
{ 830, "utp", "Enable µTP for peer connections", nullptr, false, nullptr }, { 'o', "dht", "Enable distributed hash tables (DHT)", "o", Arg::None, nullptr },
{ 831, "no-utp", "Disable µTP for peer connections", nullptr, false, nullptr }, { 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", Arg::None, nullptr },
{ 'P', "peerport", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "P", true, "<port>" }, { 'y', "lpd", "Enable local peer discovery (LPD)", "y", Arg::None, nullptr },
{ 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, nullptr }, { 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", Arg::None, nullptr },
{ 'M', "no-portmap", "Disable portmapping", "M", false, nullptr }, { 830, "utp", "Enable µTP for peer connections", nullptr, Arg::None, nullptr },
{ 'L', { 831, "no-utp", "Disable µTP for peer connections", nullptr, Arg::None, nullptr },
"peerlimit-global", { 'P', "peerport", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "P", Arg::Required, "<port>" },
"Maximum overall number of peers (Default: " TR_DEFAULT_PEER_LIMIT_GLOBAL_STR ")", { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", Arg::None, nullptr },
"L", { 'M', "no-portmap", "Disable portmapping", "M", Arg::None, nullptr },
true, { 'L',
"<limit>" }, "peerlimit-global",
{ 'l', "Maximum overall number of peers (Default: " TR_DEFAULT_PEER_LIMIT_GLOBAL_STR ")",
"peerlimit-torrent", "L",
"Maximum number of peers per torrent (Default: " TR_DEFAULT_PEER_LIMIT_TORRENT_STR ")", Arg::Required,
"l", "<limit>" },
true, { 'l',
"<limit>" }, "peerlimit-torrent",
{ 910, "encryption-required", "Encrypt all peer connections", "er", false, nullptr }, "Maximum number of peers per torrent (Default: " TR_DEFAULT_PEER_LIMIT_TORRENT_STR ")",
{ 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", false, nullptr }, "l",
{ 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", false, nullptr }, Arg::Required,
{ 'i', "bind-address-ipv4", "Where to listen for peer connections", "i", true, "<ipv4 addr>" }, "<limit>" },
{ 'I', "bind-address-ipv6", "Where to listen for peer connections", "I", true, "<ipv6 addr>" }, { 910, "encryption-required", "Encrypt all peer connections", "er", Arg::None, nullptr },
{ 'r', "rpc-bind-address", "Where to listen for RPC connections", "r", true, "<ip addr>" }, { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", Arg::None, nullptr },
{ 953, { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", Arg::None, nullptr },
"global-seedratio", { 'i', "bind-address-ipv4", "Where to listen for peer connections", "i", Arg::Required, "<ipv4 addr>" },
"All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio", { 'I', "bind-address-ipv6", "Where to listen for peer connections", "I", Arg::Required, "<ipv6 addr>" },
"gsr", { 'r', "rpc-bind-address", "Where to listen for RPC connections", "r", Arg::Required, "<ip addr>" },
true, { 953,
"ratio" }, "global-seedratio",
{ 954, "All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio",
"no-global-seedratio", "gsr",
"All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio", Arg::Required,
"GSR", "ratio" },
false, { 954,
nullptr }, "no-global-seedratio",
{ 994, "sequential-download", "Enable sequential download by default", "seq", false, nullptr }, "All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio",
{ 995, "no-sequential-download", "Disable sequential download by default", "SEQ", false, nullptr }, "GSR",
{ 'x', "pid-file", "Enable PID file", "x", true, "<pid-file>" }, Arg::None,
{ 0, nullptr, nullptr, nullptr, false, nullptr } } nullptr },
}; { 994, "sequential-download", "Enable sequential download by default", "seq", Arg::None, nullptr },
{ 995, "no-sequential-download", "Disable sequential download by default", "SEQ", Arg::None, nullptr },
{ 'x', "pid-file", "Enable PID file", "x", Arg::Required, "<pid-file>" },
{ 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0); static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
[[nodiscard]] std::string getConfigDir(int argc, char const* const* argv) [[nodiscard]] std::string getConfigDir(int argc, char const* const* argv)
{ {
int c; int c;

View File

@@ -21,19 +21,18 @@ int tr_optind = 1;
namespace namespace
{ {
[[nodiscard]] constexpr std::string_view getArgName(tr_option const* opt) [[nodiscard]] std::string getArgName(tr_option const* opt)
{ {
if (!opt->has_arg) auto const* const arg_name = opt->argName != nullptr ? opt->argName : "<args>";
switch (opt->arg)
{ {
return ""sv; case tr_option::Arg::None:
return ""s;
case tr_option::Arg::Optional:
return fmt::format("[{}]", arg_name);
default: // tr_option::Arg::Required
return arg_name;
} }
if (opt->argName != nullptr)
{
return opt->argName;
}
return "<args>"sv;
} }
[[nodiscard]] constexpr size_t get_next_line_len(std::string_view description, size_t maxlen) [[nodiscard]] constexpr size_t get_next_line_len(std::string_view description, size_t maxlen)
@@ -113,7 +112,7 @@ tr_option const* findOption(tr_option const* opts, char const* str, char const**
size_t len = o->longName != nullptr ? strlen(o->longName) : 0; size_t len = o->longName != nullptr ? strlen(o->longName) : 0;
if (matchlen < len && str[0] == '-' && str[1] == '-' && strncmp(str + 2, o->longName, len) == 0 && if (matchlen < len && str[0] == '-' && str[1] == '-' && strncmp(str + 2, o->longName, len) == 0 &&
(str[len + 2] == '\0' || (o->has_arg && str[len + 2] == '='))) (str[len + 2] == '\0' || (o->has_arg() && str[len + 2] == '=')))
{ {
matchlen = len; matchlen = len;
match = o; match = o;
@@ -122,7 +121,8 @@ tr_option const* findOption(tr_option const* opts, char const* str, char const**
len = o->shortName != nullptr ? strlen(o->shortName) : 0; len = o->shortName != nullptr ? strlen(o->shortName) : 0;
if (matchlen < len && str[0] == '-' && strncmp(str + 1, o->shortName, len) == 0 && (str[len + 1] == '\0' || o->has_arg)) if (matchlen < len && str[0] == '-' && strncmp(str + 1, o->shortName, len) == 0 &&
(str[len + 1] == '\0' || o->has_arg()))
{ {
matchlen = len; matchlen = len;
match = o; match = o;
@@ -165,7 +165,7 @@ void tr_getopt_usage(char const* app_name, char const* description, struct tr_op
maxWidth(o, long_width, short_width, arg_width); maxWidth(o, long_width, short_width, arg_width);
} }
auto const help = tr_option{ -1, "help", "Display this help page and exit", "h", false, nullptr }; auto const help = tr_option{ -1, "help", "Display this help page and exit", "h", tr_option::Arg::None, nullptr };
maxWidth(&help, long_width, short_width, arg_width); maxWidth(&help, long_width, short_width, arg_width);
if (description == nullptr) if (description == nullptr)
@@ -215,7 +215,7 @@ int tr_getopt(char const* usage, int argc, char const* const* argv, tr_option co
return TR_OPT_UNK; return TR_OPT_UNK;
} }
if (!o->has_arg) if (!o->has_arg())
{ {
/* no argument needed for this option, so we're done */ /* no argument needed for this option, so we're done */
if (arg != nullptr) if (arg != nullptr)
@@ -223,28 +223,28 @@ int tr_getopt(char const* usage, int argc, char const* const* argv, tr_option co
return TR_OPT_ERR; return TR_OPT_ERR;
} }
*setme_optarg = nullptr;
++tr_optind; ++tr_optind;
return o->val; return o->val;
} }
/* option needed an argument, and it was embedded in this string */ ++tr_optind;
/* option allows an argument, and it was embedded in this string */
if (arg != nullptr) if (arg != nullptr)
{ {
*setme_optarg = arg; *setme_optarg = arg;
++tr_optind;
return o->val; return o->val;
} }
/* throw an error if the option needed an argument but didn't get one */ if (tr_optind >= argc || findOption(opts, argv[tr_optind], nullptr) != nullptr)
if (++tr_optind >= argc)
{ {
return TR_OPT_ERR; /* throw an error if the option needed an argument but didn't get one */
} if (o->arg == tr_option::Arg::Required)
{
return TR_OPT_ERR;
}
if (findOption(opts, argv[tr_optind], nullptr) != nullptr) return o->val;
{
return TR_OPT_ERR;
} }
*setme_optarg = argv[tr_optind++]; *setme_optarg = argv[tr_optind++];

View File

@@ -17,11 +17,23 @@ extern int tr_optind;
struct tr_option struct tr_option
{ {
enum class Arg : uint8_t
{
None,
Optional,
Required
};
[[nodiscard]] constexpr bool has_arg() const noexcept
{
return arg >= Arg::Optional;
}
int val; /* the value to return from tr_getopt() */ int val; /* the value to return from tr_getopt() */
char const* longName; /* --long-form */ char const* longName; /* --long-form */
char const* description; /* option's description for tr_getopt_usage() */ char const* description; /* option's description for tr_getopt_usage() */
char const* shortName; /* short form */ char const* shortName; /* short form */
bool has_arg; /* 0 for no argument, 1 for argument */ Arg arg; /* See enum class Arg */
char const* argName; /* argument's description for tr_getopt_usage() */ char const* argName; /* argument's description for tr_getopt_usage() */
}; };

View File

@@ -29,17 +29,22 @@ char const* const DisplayName = "transmission-qt";
auto constexpr FileArgsSeparator = "--"sv; auto constexpr FileArgsSeparator = "--"sv;
auto constexpr QtArgsSeparator = "---"sv; auto constexpr QtArgsSeparator = "---"sv;
std::array<tr_option, 8> const Opts = { using Arg = tr_option::Arg;
tr_option{ 'g', "config-dir", "Where to look for configuration files", "g", true, "<path>" }, auto constexpr Opts = std::array<tr_option, 8>{ {
{ 'm', "minimized", "Start minimized in system tray", "m", false, nullptr }, { 'g', "config-dir", "Where to look for configuration files", "g", Arg::Required, "<path>" },
{ 'p', "port", "Port to use when connecting to an existing session", "p", true, "<port>" }, { 'm', "minimized", "Start minimized in system tray", "m", Arg::None, nullptr },
{ 'r', "remote", "Connect to an existing session at the specified hostname", "r", true, "<host>" }, { 'p', "port", "Port to use when connecting to an existing session", "p", Arg::Required, "<port>" },
{ 'u', "username", "Username to use when connecting to an existing session", "u", true, "<username>" }, { 'r', "remote", "Connect to an existing session at the specified hostname", "r", Arg::Required, "<host>" },
{ 'v', "version", "Show version number and exit", "v", false, nullptr }, { 'u', "username", "Username to use when connecting to an existing session", "u", Arg::Required, "<username>" },
{ 'w', "password", "Password to use when connecting to an existing session", "w", true, "<password>" }, { 'v', "version", "Show version number and exit", "v", Arg::None, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } { 'w', "password", "Password to use when connecting to an existing session", "w", Arg::Required, "<password>" },
}; { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Opts[std::size(Opts) - 2].val != 0);
} // namespace
namespace
{
char const* getUsage() char const* getUsage()
{ {
return "Usage:\n" return "Usage:\n"

View File

@@ -11,18 +11,24 @@
namespace namespace
{ {
using Arg = tr_option::Arg;
auto const Options = std::array<tr_option, 8>{ auto constexpr Options = std::array<tr_option, 9>{ {
tr_option{ 'p', "private", "Allow this torrent to only be used with the specified tracker(s)", "p", false, nullptr }, { 'p', "private", "Allow this torrent to only be used with the specified tracker(s)", "p", Arg::None, nullptr },
{ 'o', "outfile", "Save the generated .torrent to this filename", "o", true, "<file>" }, { 'o', "outfile", "Save the generated .torrent to this filename", "o", Arg::Required, "<file>" },
{ 's', "piecesize", "Set how many KiB each piece should be, overriding the preferred default", "s", true, "<size in KiB>" }, { 's',
{ 'c', "comment", "Add a comment", "c", true, "<comment>" }, "piecesize",
{ 't', "tracker", "Add a tracker's announce URL", "t", true, "<url>" }, "Set how many KiB each piece should be, overriding the preferred default",
{ 'q', "pooka", "Pooka", "pk", false, nullptr }, "s",
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, Arg::Required,
{ 0, nullptr, nullptr, nullptr, false, nullptr } "<size in KiB>" },
}; { 'c', "comment", "Add a comment", "c", Arg::Required, "<comment>" },
{ 't', "tracker", "Add a tracker's announce URL", "t", Arg::Required, "<url>" },
{ 'q', "pooka", "Pooka", "pk", Arg::None, nullptr },
{ 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
{ 994, "sequential-download", "Download the torrent sequentially", "seq", Arg::Optional, "<piece>" },
{ 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace } // namespace
class GetoptTest : public ::testing::Test class GetoptTest : public ::testing::Test
@@ -54,102 +60,183 @@ protected:
TEST_F(GetoptTest, noOptions) TEST_F(GetoptTest, noOptions)
{ {
auto const args = std::array<char const*, 1>{ "/some/path/tr-getopt-test" }; static auto constexpr Args = std::array<char const*, 1>{ "/some/path/tr-getopt-test" };
auto constexpr ExpectedN = 0; static auto constexpr ExpectedN = 0;
auto const expected_c = std::array<int, ExpectedN>{}; static auto constexpr ExpectedC = std::array<int, ExpectedN>{};
auto const expected_opt_arg = std::array<char const*, ExpectedN>{}; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{};
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, shortNoarg) TEST_F(GetoptTest, shortNoarg)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-p" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-p" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'p' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'p' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ nullptr }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, longNoarg) TEST_F(GetoptTest, longNoarg)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--private" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--private" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'p' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'p' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ nullptr }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, shortWithArg) TEST_F(GetoptTest, shortWithRequiredArg)
{ {
auto const args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "-o", "/tmp/outfile" }; static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "-o", "/tmp/outfile" };
auto constexpr ExpectedN = 1; auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'o' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'o' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "/tmp/outfile" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "/tmp/outfile" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, longWithArg) TEST_F(GetoptTest, longWithRequiredArg)
{ {
auto const args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "--outfile", "/tmp/outfile" }; static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "--outfile", "/tmp/outfile" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'o' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'o' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "/tmp/outfile" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "/tmp/outfile" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, shortWithArgAfterEq) TEST_F(GetoptTest, shortWithRequiredArgAfterEq)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-o=/tmp/outfile" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-o=/tmp/outfile" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'o' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'o' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "/tmp/outfile" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "/tmp/outfile" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, longWithArgAfterEq) TEST_F(GetoptTest, longWithRequiredArgAfterEq)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--outfile=/tmp/outfile" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--outfile=/tmp/outfile" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'o' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'o' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "/tmp/outfile" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "/tmp/outfile" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, unknownOption) TEST_F(GetoptTest, unknownOption)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-z" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-z" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ TR_OPT_UNK }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ TR_OPT_UNK };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "-z" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "-z" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, missingArg) TEST_F(GetoptTest, missingArgEnd)
{ {
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-o" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-o" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ TR_OPT_ERR }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ TR_OPT_ERR };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ nullptr }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, missingArgMiddle)
{
static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "-o", "-p" };
static auto constexpr ExpectedN = 2;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ TR_OPT_ERR, 'p' };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr, nullptr };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, lotsOfOptions) TEST_F(GetoptTest, lotsOfOptions)
{ {
auto const args = std::array<char const*, 6>{ static auto constexpr Args = std::array<char const*, 6>{
"/some/path/tr-getopt-test", "--piecesize=4", "-c", "hello world", "-p", "--tracker=foo" "/some/path/tr-getopt-test", "--piecesize=4", "-c", "hello world", "-p", "--tracker=foo"
}; };
auto constexpr ExpectedN = 4; static auto constexpr ExpectedN = 4;
auto const expected_c = std::array<int, ExpectedN>{ 's', 'c', 'p', 't' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 's', 'c', 'p', 't' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ "4", "hello world", nullptr, "foo" }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "4", "hello world", nullptr, "foo" };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }
TEST_F(GetoptTest, matchLongerKey) TEST_F(GetoptTest, matchLongerKey)
{ {
// confirm that this resolves to 'q' and not 'p' // confirm that this resolves to 'q' and not 'p'
auto const args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-pk" }; static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-pk" };
auto constexpr ExpectedN = 1; static auto constexpr ExpectedN = 1;
auto const expected_c = std::array<int, ExpectedN>{ 'q' }; static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 'q' };
auto const expected_opt_arg = std::array<char const*, ExpectedN>{ nullptr }; static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(args.size(), args.data(), ExpectedN, expected_c.data(), expected_opt_arg.data()); runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, shortWithOptionalArg)
{
static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "-seq", "12" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "12" };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, longWithOptionalArg)
{
static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "--sequential-download", "12" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "12" };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, shortWithOptionalArgAfterEq)
{
static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-seq=12" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "12" };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, longWithOptionalArgAfterEq)
{
static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--sequential-download=12" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ "12" };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, shortWithoutOptionalArgEnd)
{
static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "-seq" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, longWithoutOptionalArgEnd)
{
static auto constexpr Args = std::array<char const*, 2>{ "/some/path/tr-getopt-test", "--sequential-download" };
static auto constexpr ExpectedN = 1;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994 };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, shortWithoutOptionalArgMiddle)
{
static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "-seq", "-p" };
static auto constexpr ExpectedN = 2;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994, 'p' };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr, nullptr };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
}
TEST_F(GetoptTest, longWithoutOptionalArgMiddle)
{
static auto constexpr Args = std::array<char const*, 3>{ "/some/path/tr-getopt-test", "--sequential-download", "-p" };
static auto constexpr ExpectedN = 2;
static auto constexpr ExpectedC = std::array<int, ExpectedN>{ 994, 'p' };
static auto constexpr ExpectedOptArg = std::array<char const*, ExpectedN>{ nullptr, nullptr };
runTest(Args.size(), Args.data(), ExpectedN, ExpectedC.data(), ExpectedOptArg.data());
} }

View File

@@ -42,19 +42,24 @@ char constexpr Usage[] = "Usage: transmission-create [options] <file|directory>"
uint32_t constexpr KiB = 1024; uint32_t constexpr KiB = 1024;
auto constexpr Options = std::array<tr_option, 10>{ using Arg = tr_option::Arg;
{ { 'p', "private", "Allow this torrent to only be used with the specified tracker(s)", "p", false, nullptr }, auto constexpr Options = std::array<tr_option, 10>{ {
{ 'r', "source", "Set the source for private trackers", "r", true, "<source>" }, { 'p', "private", "Allow this torrent to only be used with the specified tracker(s)", "p", Arg::None, nullptr },
{ 'o', "outfile", "Save the generated .torrent to this filename", "o", true, "<file>" }, { 'r', "source", "Set the source for private trackers", "r", Arg::Required, "<source>" },
{ 's', "piecesize", "Set the piece size in KiB, overriding the preferred default", "s", true, "<KiB>" }, { 'o', "outfile", "Save the generated .torrent to this filename", "o", Arg::Required, "<file>" },
{ 'c', "comment", "Add a comment", "c", true, "<comment>" }, { 's', "piecesize", "Set the piece size in KiB, overriding the preferred default", "s", Arg::Required, "<KiB>" },
{ 't', "tracker", "Add a tracker's announce URL", "t", true, "<url>" }, { 'c', "comment", "Add a comment", "c", Arg::Required, "<comment>" },
{ 'w', "webseed", "Add a webseed URL", "w", true, "<url>" }, { 't', "tracker", "Add a tracker's announce URL", "t", Arg::Required, "<url>" },
{ 'x', "anonymize", R"(Omit "Creation date" and "Created by" info)", nullptr, false, nullptr }, { 'w', "webseed", "Add a webseed URL", "w", Arg::Required, "<url>" },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 'x', "anonymize", R"(Omit "Creation date" and "Created by" info)", nullptr, Arg::None, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } } { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
}; { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
struct app_options struct app_options
{ {
tr_announce_list trackers; tr_announce_list trackers;

View File

@@ -35,15 +35,20 @@ struct app_options
bool show_version = false; bool show_version = false;
}; };
auto constexpr Options = std::array<tr_option, 6>{ using Arg = tr_option::Arg;
{ { 'a', "add", "Add a tracker's announce URL", "a", true, "<url>" }, auto constexpr Options = std::array<tr_option, 6>{ {
{ 'd', "delete", "Delete a tracker's announce URL", "d", true, "<url>" }, { 'a', "add", "Add a tracker's announce URL", "a", Arg::Required, "<url>" },
{ 'r', "replace", "Search and replace a substring in the announce URLs", "r", true, "<old> <new>" }, { 'd', "delete", "Delete a tracker's announce URL", "d", Arg::Required, "<url>" },
{ 's', "source", "Set the source", "s", true, "<source>" }, { 'r', "replace", "Search and replace a substring in the announce URLs", "r", Arg::Required, "<old> <new>" },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 's', "source", "Set the source", "s", Arg::Required, "<source>" },
{ 0, nullptr, nullptr, nullptr, false, nullptr } } { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
}; { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
int parseCommandLine(app_options& opts, int argc, char const* const* argv) int parseCommandLine(app_options& opts, int argc, char const* const* argv)
{ {
int c; int c;

View File

@@ -211,166 +211,170 @@ enum
// --- Command-Line Arguments // --- Command-Line Arguments
auto constexpr Options = std::array<tr_option, 105>{ using Arg = tr_option::Arg;
{ { 'a', "add", "Add torrent files by filename or URL", "a", false, nullptr }, auto constexpr Options = std::array<tr_option, 105>{ {
{ 970, "alt-speed", "Use the alternate Limits", "as", false, nullptr }, { 'a', "add", "Add torrent files by filename or URL", "a", Arg::None, nullptr },
{ 971, "no-alt-speed", "Don't use the alternate Limits", "AS", false, nullptr }, { 970, "alt-speed", "Use the alternate Limits", "as", Arg::None, nullptr },
{ 972, "alt-speed-downlimit", "max alternate download speed (in " SPEED_K_STR ")", "asd", true, "<speed>" }, { 971, "no-alt-speed", "Don't use the alternate Limits", "AS", Arg::None, nullptr },
{ 973, "alt-speed-uplimit", "max alternate upload speed (in " SPEED_K_STR ")", "asu", true, "<speed>" }, { 972, "alt-speed-downlimit", "max alternate download speed (in " SPEED_K_STR ")", "asd", Arg::Required, "<speed>" },
{ 974, "alt-speed-scheduler", "Use the scheduled on/off times", "asc", false, nullptr }, { 973, "alt-speed-uplimit", "max alternate upload speed (in " SPEED_K_STR ")", "asu", Arg::Required, "<speed>" },
{ 975, "no-alt-speed-scheduler", "Don't use the scheduled on/off times", "ASC", false, nullptr }, { 974, "alt-speed-scheduler", "Use the scheduled on/off times", "asc", Arg::None, nullptr },
{ 976, "alt-speed-time-begin", "Time to start using the alt speed limits (in hhmm)", nullptr, true, "<time>" }, { 975, "no-alt-speed-scheduler", "Don't use the scheduled on/off times", "ASC", Arg::None, nullptr },
{ 977, "alt-speed-time-end", "Time to stop using the alt speed limits (in hhmm)", nullptr, true, "<time>" }, { 976, "alt-speed-time-begin", "Time to start using the alt speed limits (in hhmm)", nullptr, Arg::Required, "<time>" },
{ 978, "alt-speed-days", "Numbers for any/all days of the week - eg. \"1-7\"", nullptr, true, "<days>" }, { 977, "alt-speed-time-end", "Time to stop using the alt speed limits (in hhmm)", nullptr, Arg::Required, "<time>" },
{ 963, "blocklist-update", "Blocklist update", nullptr, false, nullptr }, { 978, "alt-speed-days", "Numbers for any/all days of the week - eg. \"1-7\"", nullptr, Arg::Required, "<days>" },
{ 'c', "incomplete-dir", "Where to store new torrents until they're complete", "c", true, "<dir>" }, { 963, "blocklist-update", "Blocklist update", nullptr, Arg::None, nullptr },
{ 'C', "no-incomplete-dir", "Don't store incomplete torrents in a different location", "C", false, nullptr }, { 'c', "incomplete-dir", "Where to store new torrents until they're complete", "c", Arg::Required, "<dir>" },
{ 'b', "debug", "Print debugging information", "b", false, nullptr }, { 'C', "no-incomplete-dir", "Don't store incomplete torrents in a different location", "C", Arg::None, nullptr },
{ 730, "bandwidth-group", "Set the current torrents' bandwidth group", "bwg", true, "<group>" }, { 'b', "debug", "Print debugging information", "b", Arg::None, nullptr },
{ 731, "no-bandwidth-group", "Reset the current torrents' bandwidth group", "nwg", false, nullptr }, { 730, "bandwidth-group", "Set the current torrents' bandwidth group", "bwg", Arg::Required, "<group>" },
{ 732, "list-groups", "Show bandwidth groups with their parameters", "lg", false, nullptr }, { 731, "no-bandwidth-group", "Reset the current torrents' bandwidth group", "nwg", Arg::None, nullptr },
{ 'd', { 732, "list-groups", "Show bandwidth groups with their parameters", "lg", Arg::None, nullptr },
"downlimit", { 'd',
"Set the max download speed in " SPEED_K_STR " for the current torrent(s) or globally", "downlimit",
"d", "Set the max download speed in " SPEED_K_STR " for the current torrent(s) or globally",
true, "d",
"<speed>" }, Arg::Required,
{ 'D', "no-downlimit", "Disable max download speed for the current torrent(s) or globally", "D", false, nullptr }, "<speed>" },
{ 'e', "cache", "Set the maximum size of the session's memory cache (in " MEM_M_STR ")", "e", true, "<size>" }, { 'D', "no-downlimit", "Disable max download speed for the current torrent(s) or globally", "D", Arg::None, nullptr },
{ 910, "encryption-required", "Encrypt all peer connections", "er", false, nullptr }, { 'e', "cache", "Set the maximum size of the session's memory cache (in " MEM_M_STR ")", "e", Arg::Required, "<size>" },
{ 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", false, nullptr }, { 910, "encryption-required", "Encrypt all peer connections", "er", Arg::None, nullptr },
{ 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", false, nullptr }, { 911, "encryption-preferred", "Prefer encrypted peer connections", "ep", Arg::None, nullptr },
{ 850, "exit", "Tell the transmission session to shut down", nullptr, false, nullptr }, { 912, "encryption-tolerated", "Prefer unencrypted peer connections", "et", Arg::None, nullptr },
{ 940, "files", "List the current torrent(s)' files", "f", false, nullptr }, { 850, "exit", "Tell the transmission session to shut down", nullptr, Arg::None, nullptr },
{ 'F', "filter", "Filter the current torrent(s)", "F", true, "criterion" }, { 940, "files", "List the current torrent(s)' files", "f", Arg::None, nullptr },
{ 'g', "get", "Mark files for download", "g", true, "<files>" }, { 'F', "filter", "Filter the current torrent(s)", "F", Arg::Required, "criterion" },
{ 'G', "no-get", "Mark files for not downloading", "G", true, "<files>" }, { 'g', "get", "Mark files for download", "g", Arg::Required, "<files>" },
{ 'i', "info", "Show the current torrent(s)' details", "i", false, nullptr }, { 'G', "no-get", "Mark files for not downloading", "G", Arg::Required, "<files>" },
{ 944, "print-ids", "Print the current torrent(s)' ids", "ids", false, nullptr }, { 'i', "info", "Show the current torrent(s)' details", "i", Arg::None, nullptr },
{ 940, "info-files", "List the current torrent(s)' files", "if", false, nullptr }, { 944, "print-ids", "Print the current torrent(s)' ids", "ids", Arg::None, nullptr },
{ 941, "info-peers", "List the current torrent(s)' peers", "ip", false, nullptr }, { 940, "info-files", "List the current torrent(s)' files", "if", Arg::None, nullptr },
{ 942, "info-pieces", "List the current torrent(s)' pieces", "ic", false, nullptr }, { 941, "info-peers", "List the current torrent(s)' peers", "ip", Arg::None, nullptr },
{ 943, "info-trackers", "List the current torrent(s)' trackers", "it", false, nullptr }, { 942, "info-pieces", "List the current torrent(s)' pieces", "ic", Arg::None, nullptr },
{ 'j', "json", "Return RPC response as a JSON string", "j", false, nullptr }, { 943, "info-trackers", "List the current torrent(s)' trackers", "it", Arg::None, nullptr },
{ 920, "session-info", "Show the session's details", "si", false, nullptr }, { 'j', "json", "Return RPC response as a JSON string", "j", Arg::None, nullptr },
{ 921, "session-stats", "Show the session's statistics", "st", false, nullptr }, { 920, "session-info", "Show the session's details", "si", Arg::None, nullptr },
{ 'l', "list", "List all torrents", "l", false, nullptr }, { 921, "session-stats", "Show the session's statistics", "st", Arg::None, nullptr },
{ 'L', "labels", "Set the current torrents' labels", "L", true, "<label[,label...]>" }, { 'l', "list", "List all torrents", "l", Arg::None, nullptr },
{ 960, "move", "Move current torrent's data to a new folder", nullptr, true, "<path>" }, { 'L', "labels", "Set the current torrents' labels", "L", Arg::Required, "<label[,label...]>" },
{ 968, "unix-socket", "Use a Unix domain socket", nullptr, true, "<path>" }, { 960, "move", "Move current torrent's data to a new folder", nullptr, Arg::Required, "<path>" },
{ 961, "find", "Tell Transmission where to find a torrent's data", nullptr, true, "<path>" }, { 968, "unix-socket", "Use a Unix domain socket", nullptr, Arg::Required, "<path>" },
{ 964, "rename", "Rename torrents root folder or a file", nullptr, true, "<name>" }, { 961, "find", "Tell Transmission where to find a torrent's data", nullptr, Arg::Required, "<path>" },
{ 965, "path", "Provide path for rename functions", nullptr, true, "<path>" }, { 964, "rename", "Rename torrents root folder or a file", nullptr, Arg::Required, "<name>" },
{ 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", false, nullptr }, { 965, "path", "Provide path for rename functions", nullptr, Arg::Required, "<path>" },
{ 'M', "no-portmap", "Disable portmapping", "M", false, nullptr }, { 'm', "portmap", "Enable portmapping via NAT-PMP or UPnP", "m", Arg::None, nullptr },
{ 'n', "auth", "Set username and password", "n", true, "<user:pw>" }, { 'M', "no-portmap", "Disable portmapping", "M", Arg::None, nullptr },
{ 810, "authenv", "Set authentication info from the TR_AUTH environment variable (user:pw)", "ne", false, nullptr }, { 'n', "auth", "Set username and password", "n", Arg::Required, "<user:pw>" },
{ 'N', "netrc", "Set authentication info from a .netrc file", "N", true, "<file>" }, { 810, "authenv", "Set authentication info from the TR_AUTH environment variable (user:pw)", "ne", Arg::None, nullptr },
{ 820, "ssl", "Use SSL when talking to daemon", nullptr, false, nullptr }, { 'N', "netrc", "Set authentication info from a .netrc file", "N", Arg::Required, "<file>" },
{ 'o', "dht", "Enable distributed hash tables (DHT)", "o", false, nullptr }, { 820, "ssl", "Use SSL when talking to daemon", nullptr, Arg::None, nullptr },
{ 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", false, nullptr }, { 'o', "dht", "Enable distributed hash tables (DHT)", "o", Arg::None, nullptr },
{ 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", true, "<port>" }, { 'O', "no-dht", "Disable distributed hash tables (DHT)", "O", Arg::None, nullptr },
{ 962, "port-test", "Port testing", "pt", false, nullptr }, { 'p', "port", "Port for incoming peers (Default: " TR_DEFAULT_PEER_PORT_STR ")", "p", Arg::Required, "<port>" },
{ 'P', "random-port", "Random port for incoming peers", "P", false, nullptr }, { 962, "port-test", "Port testing", "pt", Arg::None, nullptr },
{ 900, "priority-high", "Try to download these file(s) first", "ph", true, "<files>" }, { 'P', "random-port", "Random port for incoming peers", "P", Arg::None, nullptr },
{ 901, "priority-normal", "Try to download these file(s) normally", "pn", true, "<files>" }, { 900, "priority-high", "Try to download these file(s) first", "ph", Arg::Required, "<files>" },
{ 902, "priority-low", "Try to download these file(s) last", "pl", true, "<files>" }, { 901, "priority-normal", "Try to download these file(s) normally", "pn", Arg::Required, "<files>" },
{ 700, "bandwidth-high", "Give this torrent first chance at available bandwidth", "Bh", false, nullptr }, { 902, "priority-low", "Try to download these file(s) last", "pl", Arg::Required, "<files>" },
{ 701, "bandwidth-normal", "Give this torrent bandwidth left over by high priority torrents", "Bn", false, nullptr }, { 700, "bandwidth-high", "Give this torrent first chance at available bandwidth", "Bh", Arg::None, nullptr },
{ 702, { 701, "bandwidth-normal", "Give this torrent bandwidth left over by high priority torrents", "Bn", Arg::None, nullptr },
"bandwidth-low", { 702,
"Give this torrent bandwidth left over by high and normal priority torrents", "bandwidth-low",
"Bl", "Give this torrent bandwidth left over by high and normal priority torrents",
false, "Bl",
nullptr }, Arg::None,
{ 600, "reannounce", "Reannounce the current torrent(s)", nullptr, false, nullptr }, nullptr },
{ 'r', "remove", "Remove the current torrent(s)", "r", false, nullptr }, { 600, "reannounce", "Reannounce the current torrent(s)", nullptr, Arg::None, nullptr },
{ 930, "peers", "Set the maximum number of peers for the current torrent(s) or globally", "pr", true, "<max>" }, { 'r', "remove", "Remove the current torrent(s)", "r", Arg::None, nullptr },
{ 840, "remove-and-delete", "Remove the current torrent(s) and delete local data", "rad", false, nullptr }, { 930, "peers", "Set the maximum number of peers for the current torrent(s) or globally", "pr", Arg::Required, "<max>" },
{ 800, "torrent-done-script", "A script to run when a torrent finishes downloading", nullptr, true, "<file>" }, { 840, "remove-and-delete", "Remove the current torrent(s) and delete local data", "rad", Arg::None, nullptr },
{ 801, "no-torrent-done-script", "Don't run the done-downloading script", nullptr, false, nullptr }, { 800, "torrent-done-script", "A script to run when a torrent finishes downloading", nullptr, Arg::Required, "<file>" },
{ 802, "torrent-done-seeding-script", "A script to run when a torrent finishes seeding", nullptr, true, "<file>" }, { 801, "no-torrent-done-script", "Don't run the done-downloading script", nullptr, Arg::None, nullptr },
{ 803, "no-torrent-done-seeding-script", "Don't run the done-seeding script", nullptr, false, nullptr }, { 802, "torrent-done-seeding-script", "A script to run when a torrent finishes seeding", nullptr, Arg::Required, "<file>" },
{ 950, "seedratio", "Let the current torrent(s) seed until a specific ratio", "sr", true, "ratio" }, { 803, "no-torrent-done-seeding-script", "Don't run the done-seeding script", nullptr, Arg::None, nullptr },
{ 951, "seedratio-default", "Let the current torrent(s) use the global seedratio settings", "srd", false, nullptr }, { 950, "seedratio", "Let the current torrent(s) seed until a specific ratio", "sr", Arg::Required, "ratio" },
{ 952, "no-seedratio", "Let the current torrent(s) seed regardless of ratio", "SR", false, nullptr }, { 951, "seedratio-default", "Let the current torrent(s) use the global seedratio settings", "srd", Arg::None, nullptr },
{ 953, { 952, "no-seedratio", "Let the current torrent(s) seed regardless of ratio", "SR", Arg::None, nullptr },
"global-seedratio", { 953,
"All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio", "global-seedratio",
"gsr", "All torrents, unless overridden by a per-torrent setting, should seed until a specific ratio",
true, "gsr",
"ratio" }, Arg::Required,
{ 954, "ratio" },
"no-global-seedratio", { 954,
"All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio", "no-global-seedratio",
"GSR", "All torrents, unless overridden by a per-torrent setting, should seed regardless of ratio",
false, "GSR",
nullptr }, Arg::None,
{ 955, nullptr },
"idle-seeding-limit", { 955,
"Let the current torrent(s) seed until a specific amount idle time", "idle-seeding-limit",
"isl", "Let the current torrent(s) seed until a specific amount idle time",
true, "isl",
"<minutes>" }, Arg::Required,
{ 956, "<minutes>" },
"default-idle-seeding-limit", { 956,
"Let the current torrent(s) use the default idle seeding settings", "default-idle-seeding-limit",
"isld", "Let the current torrent(s) use the default idle seeding settings",
false, "isld",
nullptr }, Arg::None,
{ 957, "no-idle-seeding-limit", "Let the current torrent(s) seed regardless of idle time", "ISL", false, nullptr }, nullptr },
{ 958, { 957, "no-idle-seeding-limit", "Let the current torrent(s) seed regardless of idle time", "ISL", Arg::None, nullptr },
"global-idle-seeding-limit", { 958,
"All torrents, unless overridden by a per-torrent setting, should seed until a specific amount of idle time", "global-idle-seeding-limit",
"gisl", "All torrents, unless overridden by a per-torrent setting, should seed until a specific amount of idle time",
true, "gisl",
"<minutes>" }, Arg::Required,
{ 959, "<minutes>" },
"no-global-idle-seeding-limit", { 959,
"All torrents, unless overridden by a per-torrent setting, should seed regardless of idle time", "no-global-idle-seeding-limit",
"GISL", "All torrents, unless overridden by a per-torrent setting, should seed regardless of idle time",
false, "GISL",
nullptr }, Arg::None,
{ 710, "tracker-add", "Add a tracker to a torrent", "td", true, "<tracker>" }, nullptr },
{ 712, "tracker-remove", "Remove a tracker from a torrent", "tr", true, "<trackerId>" }, { 710, "tracker-add", "Add a tracker to a torrent", "td", Arg::Required, "<tracker>" },
{ 's', "start", "Start the current torrent(s)", "s", false, nullptr }, { 712, "tracker-remove", "Remove a tracker from a torrent", "tr", Arg::Required, "<trackerId>" },
{ 'S', "stop", "Stop the current torrent(s)", "S", false, nullptr }, { 's', "start", "Start the current torrent(s)", "s", Arg::None, nullptr },
{ 't', "torrent", "Set the current torrent(s)", "t", true, "<torrent>" }, { 'S', "stop", "Stop the current torrent(s)", "S", Arg::None, nullptr },
{ 990, "start-paused", "Start added torrents paused", nullptr, false, nullptr }, { 't', "torrent", "Set the current torrent(s)", "t", Arg::Required, "<torrent>" },
{ 991, "no-start-paused", "Start added torrents unpaused", nullptr, false, nullptr }, { 990, "start-paused", "Start added torrents paused", nullptr, Arg::None, nullptr },
{ 992, "trash-torrent", "Delete torrents after adding", nullptr, false, nullptr }, { 991, "no-start-paused", "Start added torrents unpaused", nullptr, Arg::None, nullptr },
{ 993, "no-trash-torrent", "Do not delete torrents after adding", nullptr, false, nullptr }, { 992, "trash-torrent", "Delete torrents after adding", nullptr, Arg::None, nullptr },
{ 994, "sequential-download", "Download the torrent sequentially", "seq", false, nullptr }, { 993, "no-trash-torrent", "Do not delete torrents after adding", nullptr, Arg::None, nullptr },
{ 995, "no-sequential-download", "Download the torrent normally", "SEQ", false, nullptr }, { 994, "sequential-download", "Download the torrent sequentially", "seq", Arg::None, nullptr },
{ 984, "honor-session", "Make the current torrent(s) honor the session limits", "hl", false, nullptr }, { 995, "no-sequential-download", "Download the torrent normally", "SEQ", Arg::None, nullptr },
{ 985, "no-honor-session", "Make the current torrent(s) not honor the session limits", "HL", false, nullptr }, { 984, "honor-session", "Make the current torrent(s) honor the session limits", "hl", Arg::None, nullptr },
{ 'u', { 985, "no-honor-session", "Make the current torrent(s) not honor the session limits", "HL", Arg::None, nullptr },
"uplimit", { 'u',
"Set the max upload speed in " SPEED_K_STR " for the current torrent(s) or globally", "uplimit",
"u", "Set the max upload speed in " SPEED_K_STR " for the current torrent(s) or globally",
true, "u",
"<speed>" }, Arg::Required,
{ 'U', "no-uplimit", "Disable max upload speed for the current torrent(s) or globally", "U", false, nullptr }, "<speed>" },
{ 830, "utp", "Enable µTP for peer connections", nullptr, false, nullptr }, { 'U', "no-uplimit", "Disable max upload speed for the current torrent(s) or globally", "U", Arg::None, nullptr },
{ 831, "no-utp", "Disable µTP for peer connections", nullptr, false, nullptr }, { 830, "utp", "Enable µTP for peer connections", nullptr, Arg::None, nullptr },
{ 'v', "verify", "Verify the current torrent(s)", "v", false, nullptr }, { 831, "no-utp", "Disable µTP for peer connections", nullptr, Arg::None, nullptr },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 'v', "verify", "Verify the current torrent(s)", "v", Arg::None, nullptr },
{ 'w', { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
"download-dir", { 'w',
"When used in conjunction with --add, set the new torrent's download folder. " "download-dir",
"Otherwise, set the default download folder", "When used in conjunction with --add, set the new torrent's download folder. "
"w", "Otherwise, set the default download folder",
true, "w",
"<path>" }, Arg::Required,
{ 'x', "pex", "Enable peer exchange (PEX)", "x", false, nullptr }, "<path>" },
{ 'X', "no-pex", "Disable peer exchange (PEX)", "X", false, nullptr }, { 'x', "pex", "Enable peer exchange (PEX)", "x", Arg::None, nullptr },
{ 'y', "lpd", "Enable local peer discovery (LPD)", "y", false, nullptr }, { 'X', "no-pex", "Disable peer exchange (PEX)", "X", Arg::None, nullptr },
{ 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", false, nullptr }, { 'y', "lpd", "Enable local peer discovery (LPD)", "y", Arg::None, nullptr },
{ 941, "peer-info", "List the current torrent(s)' peers", "pi", false, nullptr }, { 'Y', "no-lpd", "Disable local peer discovery (LPD)", "Y", Arg::None, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } } { 941, "peer-info", "List the current torrent(s)' peers", "pi", Arg::None, nullptr },
}; { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0); static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
void show_usage() void show_usage()
{ {
tr_getopt_usage(MyName, Usage, std::data(Options)); tr_getopt_usage(MyName, Usage, std::data(Options));

View File

@@ -41,29 +41,33 @@ using namespace libtransmission::Values;
namespace namespace
{ {
auto constexpr TimeoutSecs = std::chrono::seconds{ 30 }; auto constexpr TimeoutSecs = std::chrono::seconds{ 30 };
char constexpr MyName[] = "transmission-show"; char constexpr MyName[] = "transmission-show";
char constexpr Usage[] = "Usage: transmission-show [options] <torrent-file>"; char constexpr Usage[] = "Usage: transmission-show [options] <torrent-file>";
auto options = std::array<tr_option, 14>{ using Arg = tr_option::Arg;
{ { 'd', "header", "Show only header section", "d", false, nullptr }, auto constexpr Options = std::array<tr_option, 14>{ {
{ 'i', "info", "Show only info section", "i", false, nullptr }, { 'd', "header", "Show only header section", "d", Arg::None, nullptr },
{ 't', "trackers", "Show only trackers section", "t", false, nullptr }, { 'i', "info", "Show only info section", "i", Arg::None, nullptr },
{ 'f', "files", "Show only file list", "f", false, nullptr }, { 't', "trackers", "Show only trackers section", "t", Arg::None, nullptr },
{ 'D', "no-header", "Do not show header section", "D", false, nullptr }, { 'f', "files", "Show only file list", "f", Arg::None, nullptr },
{ 'I', "no-info", "Do not show info section", "I", false, nullptr }, { 'D', "no-header", "Do not show header section", "D", Arg::None, nullptr },
{ 'T', "no-trackers", "Do not show trackers section", "T", false, nullptr }, { 'I', "no-info", "Do not show info section", "I", Arg::None, nullptr },
{ 'F', "no-files", "Do not show files section", "F", false, nullptr }, { 'T', "no-trackers", "Do not show trackers section", "T", Arg::None, nullptr },
{ 'b', "bytes", "Show file sizes in bytes", "b", false, nullptr }, { 'F', "no-files", "Do not show files section", "F", Arg::None, nullptr },
{ 'm', "magnet", "Give a magnet link for the specified torrent", "m", false, nullptr }, { 'b', "bytes", "Show file sizes in bytes", "b", Arg::None, nullptr },
{ 's', "scrape", "Ask the torrent's trackers how many peers are in the torrent's swarm", "s", false, nullptr }, { 'm', "magnet", "Give a magnet link for the specified torrent", "m", Arg::None, nullptr },
{ 'u', "unsorted", "Do not sort files by name", "u", false, nullptr }, { 's', "scrape", "Ask the torrent's trackers how many peers are in the torrent's swarm", "s", Arg::None, nullptr },
{ 'V', "version", "Show version number and exit", "V", false, nullptr }, { 'u', "unsorted", "Do not sort files by name", "u", Arg::None, nullptr },
{ 0, nullptr, nullptr, nullptr, false, nullptr } } { 'V', "version", "Show version number and exit", "V", Arg::None, nullptr },
}; { 0, nullptr, nullptr, nullptr, Arg::None, nullptr },
} };
static_assert(Options[std::size(Options) - 2].val != 0);
} // namespace
namespace
{
struct app_opts struct app_opts
{ {
std::string_view filename; std::string_view filename;
@@ -83,7 +87,7 @@ int parseCommandLine(app_opts& opts, int argc, char const* const* argv)
int c; int c;
char const* optarg; char const* optarg;
while ((c = tr_getopt(Usage, argc, argv, std::data(options), &optarg)) != TR_OPT_DONE) while ((c = tr_getopt(Usage, argc, argv, std::data(Options), &optarg)) != TR_OPT_DONE)
{ {
switch (c) switch (c)
{ {
@@ -426,7 +430,7 @@ int tr_main(int argc, char* argv[])
if (std::empty(opts.filename)) if (std::empty(opts.filename))
{ {
fmt::print(stderr, "ERROR: No torrent file specified.\n"); fmt::print(stderr, "ERROR: No torrent file specified.\n");
tr_getopt_usage(MyName, Usage, std::data(options)); tr_getopt_usage(MyName, Usage, std::data(Options));
fmt::print(stderr, "\n"); fmt::print(stderr, "\n");
return EXIT_FAILURE; return EXIT_FAILURE;
} }