Completely rework and rewrite command line argument parsing.
Also fix some things: To actually set a variable to the value of the argument, & clipp::value must be used, not .set().
This commit is contained in:
parent
c360cbe5fe
commit
0e7993c194
92
include/optional.h
Normal file
92
include/optional.h
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
//
|
||||||
|
// Copyright 2018 Christoph Heiss <me@christoph-heiss.me>
|
||||||
|
// Distributed under the Boost Software License, Version 1.0.
|
||||||
|
//
|
||||||
|
// See accompanying file LICENSE in the project root directory
|
||||||
|
// or copy at http://www.boost.org/LICENSE_1_0.txt
|
||||||
|
//
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
|
||||||
|
/*! \brief Very simple re-implementation of std::{experimental::}optional.
|
||||||
|
* \param T Type of the value the optional should hold.
|
||||||
|
*
|
||||||
|
* Until std::{experimental::}optional is standardized, this can be used as a
|
||||||
|
* very simple and dumb replacement.
|
||||||
|
*/
|
||||||
|
template <typename T>
|
||||||
|
class Optional {
|
||||||
|
public:
|
||||||
|
/*! \brief Type of the value the optional holds. */
|
||||||
|
typedef T value_type;
|
||||||
|
|
||||||
|
/*! \brief Constructs a new, empty optional.
|
||||||
|
*
|
||||||
|
* The default value will be default-constructed.
|
||||||
|
*/
|
||||||
|
Optional() : has_value_{}, default_value_{} { }
|
||||||
|
|
||||||
|
/*! \brief Constructos a new optional with an default value.
|
||||||
|
* \param default_value The default value the optional will hold.
|
||||||
|
*/
|
||||||
|
Optional(T default_value) : has_value_{}, default_value_{default_value} { }
|
||||||
|
|
||||||
|
/*! \brief Indicates if the optional holds a value.
|
||||||
|
* \return If the optional holds a value.
|
||||||
|
*/
|
||||||
|
bool has_value() const
|
||||||
|
{
|
||||||
|
return has_value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! \brief Gets the value.
|
||||||
|
* \return The value the optional holds.
|
||||||
|
*
|
||||||
|
* If the optional has no value, a default-constructed object is returned.
|
||||||
|
*/
|
||||||
|
const T& value() const&
|
||||||
|
{
|
||||||
|
return value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! \brief Returns the value if the optional has one, otherwise the default value.
|
||||||
|
* \return The value of the optional if it has one, otherwise the default value.
|
||||||
|
*/
|
||||||
|
const T& value_or_default() const&
|
||||||
|
{
|
||||||
|
return has_value() ? value() : default_value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! \brief Sets the value of the optional.
|
||||||
|
* \param val The new value of the optional.
|
||||||
|
* \return The new value of the optional.
|
||||||
|
*/
|
||||||
|
const T& set_value(T&& val)
|
||||||
|
{
|
||||||
|
has_value_ = true;
|
||||||
|
value_ = std::move(val);
|
||||||
|
return value();
|
||||||
|
}
|
||||||
|
|
||||||
|
/*! \brief Returns the default value of the optional.
|
||||||
|
* \return The default value of the optional.
|
||||||
|
*/
|
||||||
|
const T& default_value() const&
|
||||||
|
{
|
||||||
|
return default_value_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
/*! \brief Indicates if this optional has a value set. */
|
||||||
|
bool has_value_;
|
||||||
|
|
||||||
|
/*! \brief The value that may or may not be present.
|
||||||
|
*
|
||||||
|
* Its present is indicated by #has_value_
|
||||||
|
*/
|
||||||
|
T value_;
|
||||||
|
|
||||||
|
/*! \brief The default value of this optional. */
|
||||||
|
const T default_value_;
|
||||||
|
};
|
|
@ -24,15 +24,14 @@
|
||||||
#pragma GCC diagnostic pop
|
#pragma GCC diagnostic pop
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
Options() : host{"localhost:6544"} { }
|
Options() : host{"localhost:6544"} { }
|
||||||
|
|
||||||
std::string host;
|
std::string host;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::ostream& operator<<(std::ostream& ostream, const rslp::Command& command)
|
std::ostream& operator<<(std::ostream& ostream, const rslp::Command& command)
|
||||||
{
|
{
|
||||||
using Type = rslp::Command_Data::DataCase;
|
using Type = rslp::Command_Data::DataCase;
|
||||||
|
@ -80,8 +79,8 @@ Options parse_commandline(int argc, char** argv)
|
||||||
bool show_help{};
|
bool show_help{};
|
||||||
|
|
||||||
auto cli = (
|
auto cli = (
|
||||||
clipp::option("-h", "--host").set(options.host)
|
clipp::option("-h", "--host") & clipp::value("host", options.host)
|
||||||
.doc("Set the host (and port, optional) to connect to [default: localhost:6544]"),
|
.doc("Sets the host and port to connect to [default: localhost:6544]"),
|
||||||
clipp::option("--help").set(show_help).doc("Show help and exit.")
|
clipp::option("--help").set(show_help).doc("Show help and exit.")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -95,7 +94,7 @@ Options parse_commandline(int argc, char** argv)
|
||||||
|
|
||||||
class GrpcResplyClient {
|
class GrpcResplyClient {
|
||||||
public:
|
public:
|
||||||
GrpcResplyClient(std::shared_ptr<grpc::Channel> channel) :
|
explicit GrpcResplyClient(std::shared_ptr<grpc::Channel> channel) :
|
||||||
stub_{rslp::ProtoAdapter::NewStub(channel)}
|
stub_{rslp::ProtoAdapter::NewStub(channel)}
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <ostream>
|
#include <ostream>
|
||||||
|
#include <sstream>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
|
||||||
|
@ -20,26 +21,22 @@
|
||||||
|
|
||||||
using namespace google;
|
using namespace google;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
Options() : host{"localhost"}, port{"6543"} { }
|
Options() : host{"localhost:6543"} { }
|
||||||
|
|
||||||
std::string host;
|
std::string host;
|
||||||
std::string port;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
Options parse_commandline(int argc, char** argv)
|
Options parse_commandline(int argc, char** argv)
|
||||||
{
|
{
|
||||||
Options options;
|
Options options;
|
||||||
bool show_help{};
|
bool show_help{};
|
||||||
|
|
||||||
auto cli = (
|
auto cli = (
|
||||||
clipp::option("-h", "--host").set(options.host)
|
clipp::option("-h", "--host") & clipp::value("host", options.host)
|
||||||
.doc("Set the host to connect to [default: localhost]"),
|
.doc("Sets the host and port to connect to [default: localhost:6543]"),
|
||||||
clipp::option("-p", "--port").set(options.port)
|
|
||||||
.doc("Set the port to connect to [default: 6543]"),
|
|
||||||
clipp::option("--help").set(show_help).doc("Show help and exit.")
|
clipp::option("--help").set(show_help).doc("Show help and exit.")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -94,15 +91,21 @@ std::ostream& operator<<(std::ostream& ostream, const rslp::Command& command)
|
||||||
|
|
||||||
class ProtobufResplyClient {
|
class ProtobufResplyClient {
|
||||||
public:
|
public:
|
||||||
ProtobufResplyClient(const std::string& host, const std::string& port) :
|
explicit ProtobufResplyClient(const std::string& host) :
|
||||||
host_{host}, port_{port}, socket_{io_context_}
|
socket_{io_context_}
|
||||||
{ }
|
{
|
||||||
|
std::stringstream stream{host};
|
||||||
|
|
||||||
|
std::getline(stream, host_, ':');
|
||||||
|
std::getline(stream, port_);
|
||||||
|
}
|
||||||
|
|
||||||
bool connect()
|
bool connect()
|
||||||
{
|
{
|
||||||
asio::error_code error_code;
|
asio::error_code error_code;
|
||||||
asio::ip::tcp::resolver resolver{io_context_};
|
asio::ip::tcp::resolver resolver{io_context_};
|
||||||
|
|
||||||
|
std::cout << "host=" << host_ << "; port=" << port_ << std::endl;
|
||||||
auto results = resolver.resolve(host_, port_, error_code);
|
auto results = resolver.resolve(host_, port_, error_code);
|
||||||
if (error_code) {
|
if (error_code) {
|
||||||
std::cerr << "Error while connecting: " << error_code.message() << std::endl;
|
std::cerr << "Error while connecting: " << error_code.message() << std::endl;
|
||||||
|
@ -182,8 +185,8 @@ private:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const std::string& host_;
|
std::string host_;
|
||||||
const std::string& port_;
|
std::string port_;
|
||||||
|
|
||||||
asio::io_context io_context_;
|
asio::io_context io_context_;
|
||||||
asio::ip::tcp::socket socket_;
|
asio::ip::tcp::socket socket_;
|
||||||
|
@ -197,13 +200,13 @@ int main(int argc, char* argv[])
|
||||||
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
GOOGLE_PROTOBUF_VERIFY_VERSION;
|
||||||
auto options{parse_commandline(argc, argv)};
|
auto options{parse_commandline(argc, argv)};
|
||||||
|
|
||||||
ProtobufResplyClient client{options.host, options.port};
|
ProtobufResplyClient client{options.host};
|
||||||
if (!client.connect()) {
|
if (!client.connect()) {
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (std::cin) {
|
while (std::cin) {
|
||||||
std::cout << options.host << ':' << options.port << "> ";
|
std::cout << options.host << "> ";
|
||||||
|
|
||||||
std::string line;
|
std::string line;
|
||||||
std::getline(std::cin, line);
|
std::getline(std::cin, line);
|
||||||
|
|
124
src/proxy.cc
124
src/proxy.cc
|
@ -18,6 +18,7 @@
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
|
#include <type_traits>
|
||||||
#include <arpa/inet.h>
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
#include "asio.hpp"
|
#include "asio.hpp"
|
||||||
|
@ -25,6 +26,7 @@
|
||||||
#include "spdlog/spdlog.h"
|
#include "spdlog/spdlog.h"
|
||||||
#include "nlohmann/json.hpp"
|
#include "nlohmann/json.hpp"
|
||||||
#include "resply.h"
|
#include "resply.h"
|
||||||
|
#include "optional.h"
|
||||||
#include "rslp.pb.h"
|
#include "rslp.pb.h"
|
||||||
#include "grpc++/grpc++.h"
|
#include "grpc++/grpc++.h"
|
||||||
#include "grpc/support/log.h"
|
#include "grpc/support/log.h"
|
||||||
|
@ -38,46 +40,60 @@ using json = nlohmann::json;
|
||||||
using namespace google;
|
using namespace google;
|
||||||
|
|
||||||
|
|
||||||
struct Options {
|
|
||||||
Options() :
|
|
||||||
config_path{".proxy-conf.json"}, daemonize{}, log_path{"proxy.log"},
|
|
||||||
protobuf_port{6543}, grpc_port{"6544"}, remote_host{"localhost:6379"}, verbose{} { }
|
|
||||||
|
|
||||||
std::string config_path;
|
|
||||||
bool daemonize;
|
|
||||||
std::string log_path;
|
|
||||||
unsigned short protobuf_port;
|
|
||||||
std::string grpc_port;
|
|
||||||
std::string remote_host;
|
|
||||||
bool verbose;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
const std::string GLOBAL_LOGGER_NAME{"Proxy"};
|
const std::string GLOBAL_LOGGER_NAME{"Proxy"};
|
||||||
|
|
||||||
void resply_result_to_rslp_data(rslp::Command_Data* data, const resply::Result& result);
|
void resply_result_to_rslp_data(rslp::Command_Data* data, const resply::Result& result);
|
||||||
|
|
||||||
|
struct Options {
|
||||||
|
bool daemonize;
|
||||||
|
std::string log_path;
|
||||||
|
unsigned short protobuf_port;
|
||||||
|
unsigned short grpc_port;
|
||||||
|
std::string redis_host;
|
||||||
|
bool verbose;
|
||||||
|
};
|
||||||
|
|
||||||
Options parse_commandline(int argc, char** argv)
|
Options parse_commandline(int argc, char** argv)
|
||||||
{
|
{
|
||||||
Options options;
|
|
||||||
bool show_help{}, show_version{};
|
bool show_help{}, show_version{};
|
||||||
|
|
||||||
|
Optional<bool> daemonize{}, verbose{};
|
||||||
|
Optional<unsigned short> protobuf_port{6543}, grpc_port{6544};
|
||||||
|
Optional<std::string> config_path{".proxy-conf.json"};
|
||||||
|
Optional<std::string> log_path{"proxy.log"};
|
||||||
|
Optional<std::string> redis_host{"localhost:6379"};
|
||||||
|
|
||||||
auto cli = (
|
auto cli = (
|
||||||
clipp::option("-c", "--conf-path") & clipp::value("path", options.config_path)
|
clipp::option("-c", "--conf-path") & clipp::value("path")
|
||||||
|
.call([&](auto p) { config_path.set_value(p); })
|
||||||
.doc("Path to the configuration file [default: $CWD/.proxy-conf.json]"),
|
.doc("Path to the configuration file [default: $CWD/.proxy-conf.json]"),
|
||||||
clipp::option("-d", "--daemonize").set(options.daemonize).doc("Fork to background."),
|
|
||||||
clipp::option("-l", "--log-path") & clipp::value("path", options.log_path)
|
clipp::option("-d", "--daemonize")
|
||||||
|
.call([&](auto d) { daemonize.set_value(d); })
|
||||||
|
.doc("Fork to background."),
|
||||||
|
|
||||||
|
clipp::option("-l", "--log-path") & clipp::value("path")
|
||||||
|
.call([&](auto p) { log_path.set_value(p); })
|
||||||
.doc("Path to the log file [default: $CWD/proxy.log] (Only applies when daemonized.)"),
|
.doc("Path to the log file [default: $CWD/proxy.log] (Only applies when daemonized.)"),
|
||||||
clipp::option("--protobuf-port") & clipp::value("port", options.protobuf_port)
|
|
||||||
|
clipp::option("--protobuf-port") & clipp::integer("port")
|
||||||
|
.call([&](auto p) { protobuf_port.set_value(static_cast<unsigned short>(std::stoi(p))); })
|
||||||
.doc("Port the protobuf server should listen on [default: 6543]"),
|
.doc("Port the protobuf server should listen on [default: 6543]"),
|
||||||
clipp::option("--grpc-port") & clipp::value("port", options.grpc_port)
|
|
||||||
|
clipp::option("--grpc-port") & clipp::integer("port")
|
||||||
|
.call([&](auto p) { grpc_port.set_value(static_cast<unsigned short>(std::stoi(p))); })
|
||||||
.doc("Port the gRPC server should listen on [default: 6544]"),
|
.doc("Port the gRPC server should listen on [default: 6544]"),
|
||||||
clipp::option("-r", "--remote-host") & clipp::value("host", options.remote_host)
|
|
||||||
.doc("Host (redis-server) to connect to [default: localhost:6379]"),
|
clipp::option("-r", "--redis-host") & clipp::value("host")
|
||||||
clipp::option("-v", "--verbose").set(options.verbose).doc("Enable verbose logging."),
|
.call([&](auto h) { redis_host.set_value(h); })
|
||||||
|
.doc("Host (redis server) to connect to [default: localhost:6379]"),
|
||||||
|
|
||||||
|
clipp::option("-v", "--verbose")
|
||||||
|
.call([&](auto v) { verbose.set_value(v); })
|
||||||
|
.doc("Enable verbose logging."),
|
||||||
|
|
||||||
clipp::option("--help").set(show_help).doc("Show help and exit."),
|
clipp::option("--help").set(show_help).doc("Show help and exit."),
|
||||||
clipp::option("--version").set(show_version).doc("Show version and exit.")
|
clipp::option("--version").set(show_version).doc("Show version and exit.")
|
||||||
);
|
);
|
||||||
|
@ -96,6 +112,34 @@ Options parse_commandline(int argc, char** argv)
|
||||||
std::exit(0);
|
std::exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
json config;
|
||||||
|
if (config_path.value_or_default().length()) {
|
||||||
|
std::ifstream file{config_path.value_or_default()};
|
||||||
|
if (file) {
|
||||||
|
file >> config;
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
auto choose_opt = [&](const std::string& name, const auto& opt)
|
||||||
|
-> typename std::remove_reference<decltype(opt)>::type::value_type {
|
||||||
|
if (opt.has_value()) {
|
||||||
|
return opt.value();
|
||||||
|
} else if (config.count(name)) {
|
||||||
|
return config[name];
|
||||||
|
}
|
||||||
|
|
||||||
|
return opt.default_value();
|
||||||
|
};
|
||||||
|
|
||||||
|
Options options;
|
||||||
|
options.daemonize = choose_opt("daemonize", daemonize);
|
||||||
|
options.log_path = choose_opt("log-path", log_path);
|
||||||
|
options.protobuf_port = choose_opt("protobuf-port", protobuf_port);
|
||||||
|
options.grpc_port = choose_opt("grpc-port", grpc_port);
|
||||||
|
options.redis_host = choose_opt("redis-host", redis_host);
|
||||||
|
options.verbose = choose_opt("verbose", verbose);
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -363,7 +407,7 @@ public:
|
||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
auto server{std::make_shared<ProtobufAdapter>(
|
auto server{std::make_shared<ProtobufAdapter>(
|
||||||
options.remote_host, io_context
|
options.redis_host, io_context
|
||||||
)};
|
)};
|
||||||
acceptor.accept(server->socket());
|
acceptor.accept(server->socket());
|
||||||
|
|
||||||
|
@ -378,7 +422,7 @@ private:
|
||||||
|
|
||||||
class GrpcAdapter final : public rslp::ProtoAdapter::Service {
|
class GrpcAdapter final : public rslp::ProtoAdapter::Service {
|
||||||
public:
|
public:
|
||||||
GrpcAdapter(const std::string& redis_host) :
|
explicit GrpcAdapter(const std::string& redis_host) :
|
||||||
client_{redis_host}, logger_{create_logger(LOGGER_NAME)}
|
client_{redis_host}, logger_{create_logger(LOGGER_NAME)}
|
||||||
{ }
|
{ }
|
||||||
|
|
||||||
|
@ -473,9 +517,12 @@ public:
|
||||||
setup_grpc_logging();
|
setup_grpc_logging();
|
||||||
|
|
||||||
grpc::ServerBuilder builder;
|
grpc::ServerBuilder builder;
|
||||||
builder.AddListeningPort("0.0.0.0:" + options.grpc_port, grpc::InsecureServerCredentials());
|
builder.AddListeningPort(
|
||||||
|
"0.0.0.0:" + std::to_string(options.grpc_port),
|
||||||
|
grpc::InsecureServerCredentials()
|
||||||
|
);
|
||||||
|
|
||||||
GrpcAdapter adapter{options.remote_host};
|
GrpcAdapter adapter{options.redis_host};
|
||||||
adapter.initialize();
|
adapter.initialize();
|
||||||
builder.RegisterService(&adapter);
|
builder.RegisterService(&adapter);
|
||||||
|
|
||||||
|
@ -515,13 +562,11 @@ private:
|
||||||
|
|
||||||
class Proxy {
|
class Proxy {
|
||||||
public:
|
public:
|
||||||
Proxy(const Options& options) :
|
explicit Proxy(const Options& options) :
|
||||||
options_{options}, logger_{spdlog::stdout_color_mt(LOGGER_NAME)} { }
|
options_{options}, logger_{spdlog::stdout_color_mt(LOGGER_NAME)} { }
|
||||||
|
|
||||||
void run()
|
void run()
|
||||||
{
|
{
|
||||||
read_config_file();
|
|
||||||
|
|
||||||
if (options_.daemonize) {
|
if (options_.daemonize) {
|
||||||
daemonize();
|
daemonize();
|
||||||
}
|
}
|
||||||
|
@ -541,22 +586,6 @@ public:
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void read_config_file()
|
|
||||||
{
|
|
||||||
if (!options_.config_path.length()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::ifstream file{options_.config_path};
|
|
||||||
|
|
||||||
if (!file) {
|
|
||||||
logger_->warn("Configuration file not found! Using compiled-in defaults ..");
|
|
||||||
} else {
|
|
||||||
file >> config_;
|
|
||||||
file.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void daemonize()
|
void daemonize()
|
||||||
{
|
{
|
||||||
logger_->info("Daemonizing server, logfile: {}", options_.log_path);
|
logger_->info("Daemonizing server, logfile: {}", options_.log_path);
|
||||||
|
@ -576,7 +605,6 @@ private:
|
||||||
|
|
||||||
const Options& options_;
|
const Options& options_;
|
||||||
std::shared_ptr<spdlog::logger> logger_;
|
std::shared_ptr<spdlog::logger> logger_;
|
||||||
json config_;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -16,22 +16,21 @@
|
||||||
#include "resply.h"
|
#include "resply.h"
|
||||||
|
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
struct Options {
|
struct Options {
|
||||||
Options() : host{"localhost:6379"} { }
|
Options() : host{"localhost:6379"} { }
|
||||||
|
|
||||||
std::string host;
|
std::string host;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
Options parse_commandline(int argc, char** argv)
|
Options parse_commandline(int argc, char** argv)
|
||||||
{
|
{
|
||||||
Options options;
|
Options options;
|
||||||
bool show_help{}, show_version{};
|
bool show_help{}, show_version{};
|
||||||
|
|
||||||
auto cli = (
|
auto cli = (
|
||||||
clipp::option("-h", "--host").set(options.host)
|
clipp::option("-h", "--host") & clipp::value("host", options.host)
|
||||||
.doc("Set the host (and port, optional) to connect to [default: localhost:6379]"),
|
.doc("Set the host (and port, optional) to connect to [default: localhost:6379]"),
|
||||||
clipp::option("--help").set(show_help).doc("Show help and exit."),
|
clipp::option("--help").set(show_help).doc("Show help and exit."),
|
||||||
clipp::option("--version").set(show_version).doc("Show version and exit.")
|
clipp::option("--version").set(show_version).doc("Show version and exit.")
|
||||||
|
|
Reference in a new issue