Add portable option parser.

GitOrigin-RevId: 0d4f7e2f5bec4826e1c12e3aa1aee642fcf6da07
This commit is contained in:
levlam 2020-06-17 03:04:36 +03:00
parent 4bb6fe7e78
commit c46910d75f
8 changed files with 253 additions and 143 deletions

View File

@ -4331,7 +4331,7 @@ void main(int argc, char **argv) {
}
return std::string();
}(std::getenv("TD_API_HASH"));
// TODO port OptionsParser to Windows
// TODO use OptionParser
for (int i = 1; i < argc; i++) {
if (!std::strcmp(argv[i], "--test")) {
use_test_dc = true;

View File

@ -4,14 +4,6 @@ if (NOT DEFINED CMAKE_INSTALL_LIBDIR)
set(CMAKE_INSTALL_LIBDIR "lib")
endif()
if (WIN32)
if (WINGETOPT_FOUND)
set(TD_HAVE_GETOPT 1)
endif()
else()
set(TD_HAVE_GETOPT 1)
endif()
if (NOT ZLIB_FOUND)
find_package(ZLIB)
endif()
@ -104,7 +96,7 @@ set(TDUTILS_SOURCE
td/utils/misc.cpp
td/utils/MimeType.cpp
td/utils/MpmcQueue.cpp
td/utils/OptionsParser.cpp
td/utils/OptionParser.cpp
td/utils/PathView.cpp
td/utils/Random.cpp
td/utils/SharedSlice.cpp
@ -222,7 +214,7 @@ set(TDUTILS_SOURCE
td/utils/ObjectPool.h
td/utils/Observer.h
td/utils/optional.h
td/utils/OptionsParser.h
td/utils/OptionParser.h
td/utils/OrderedEventsProcessor.h
td/utils/overloaded.h
td/utils/Parser.h
@ -280,6 +272,7 @@ set(TDUTILS_TEST_SOURCE
${CMAKE_CURRENT_SOURCE_DIR}/test/MpmcQueue.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/MpmcWaiter.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/MpscLinkQueue.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/OptionParser.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/OrderedEventsProcessor.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/port.cpp
${CMAKE_CURRENT_SOURCE_DIR}/test/pq.cpp
@ -324,10 +317,6 @@ if (ABSL_FOUND)
target_link_libraries(tdutils PUBLIC absl::flat_hash_map absl::flat_hash_set absl::hash)
endif()
if (WIN32 AND WINGETOPT_FOUND)
target_link_libraries(tdutils PRIVATE wingetopt)
endif()
if (ANDROID)
target_link_libraries(tdutils PRIVATE log)
endif()

View File

@ -0,0 +1,153 @@
//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/utils/OptionParser.h"
#include "td/utils/misc.h"
#include <cstring>
#include <unordered_map>
namespace td {
void OptionParser::set_description(string description) {
description_ = std::move(description);
}
void OptionParser::add_option(Option::Type type, char short_key, Slice long_key, Slice description,
std::function<Status(Slice)> callback) {
options_.push_back(Option{type, short_key, long_key.str(), description.str(), std::move(callback)});
}
void OptionParser::add_option(char short_key, Slice long_key, Slice description,
std::function<Status(Slice)> callback) {
add_option(Option::Type::Arg, short_key, long_key, description, std::move(callback));
}
void OptionParser::add_option(char short_key, Slice long_key, Slice description, std::function<Status(void)> callback) {
// Ouch. There must be some better way
add_option(Option::Type::NoArg, short_key, long_key, description,
std::bind([](std::function<Status(void)> &func, Slice) { return func(); }, std::move(callback),
std::placeholders::_1));
}
Result<vector<char *>> OptionParser::run(int argc, char *argv[]) {
std::unordered_map<char, const Option *> short_options;
std::unordered_map<string, const Option *> long_options;
for (auto &opt : options_) {
if (opt.short_key != '\0') {
short_options[opt.short_key] = &opt;
}
if (!opt.long_key.empty()) {
long_options[opt.long_key] = &opt;
}
}
vector<char *> non_options;
for (int arg_pos = 1; arg_pos < argc; arg_pos++) {
const char *arg = argv[arg_pos];
if (arg[0] != '-' || arg[1] == '\0') {
non_options.push_back(argv[arg_pos]);
continue;
}
if (arg[1] == '-' && arg[2] == '\0') {
// "--"; after it everything is non-option
while (++arg_pos < argc) {
non_options.push_back(argv[arg_pos]);
}
break;
}
if (arg[1] == '-') {
// long option
Slice long_arg(arg + 2, std::strlen(arg + 2));
Slice param;
auto equal_pos = long_arg.find('=');
bool has_equal = equal_pos != Slice::npos;
if (has_equal) {
param = long_arg.substr(equal_pos + 1);
long_arg = long_arg.substr(0, equal_pos);
}
auto it = long_options.find(long_arg.str());
if (it == long_options.end()) {
return Status::Error(PSLICE() << "Option " << long_arg << " was unrecognized");
}
auto option = it->second;
switch (option->type) {
case Option::Type::NoArg:
if (has_equal) {
return Status::Error(PSLICE() << "Option " << long_arg << " must not have argument");
}
break;
case Option::Type::Arg:
if (!has_equal) {
if (++arg_pos == argc) {
return Status::Error(PSLICE() << "Option " << long_arg << " must have argument");
}
param = Slice(argv[arg_pos], std::strlen(argv[arg_pos]));
}
break;
default:
UNREACHABLE();
}
TRY_STATUS(option->arg_callback(param));
continue;
}
for (size_t opt_pos = 1; arg[opt_pos] != '\0'; opt_pos++) {
auto it = short_options.find(arg[opt_pos]);
if (it == short_options.end()) {
return Status::Error(PSLICE() << "Option " << arg[opt_pos] << " was unrecognized");
}
auto option = it->second;
Slice param;
switch (option->type) {
case Option::Type::NoArg:
// nothing to do
break;
case Option::Type::Arg:
if (arg[opt_pos + 1] == '\0') {
if (++arg_pos == argc) {
return Status::Error(PSLICE() << "Option " << arg[opt_pos] << " must have argument");
}
param = Slice(argv[arg_pos], std::strlen(argv[arg_pos]));
} else {
param = Slice(arg + opt_pos + 1, std::strlen(arg + opt_pos + 1));
opt_pos += param.size();
}
break;
default:
UNREACHABLE();
}
TRY_STATUS(option->arg_callback(param));
}
}
return std::move(non_options);
}
StringBuilder &operator<<(StringBuilder &sb, const OptionParser &o) {
sb << o.description_ << "\n";
for (auto &opt : o.options_) {
sb << "-" << opt.short_key;
if (!opt.long_key.empty()) {
sb << "|--" << opt.long_key;
}
if (opt.type != OptionParser::Option::Type::NoArg) {
sb << "<arg>";
}
sb << "\t" << opt.description;
sb << "\n";
}
return sb;
}
} // namespace td

View File

@ -15,7 +15,7 @@
namespace td {
class OptionsParser {
class OptionParser {
class Option {
public:
enum class Type { NoArg, Arg };
@ -36,9 +36,10 @@ class OptionsParser {
void add_option(char short_key, Slice long_key, Slice description, std::function<Status(void)> callback);
Result<int> run(int argc, char *argv[]) TD_WARN_UNUSED_RESULT;
// returns found non-option parameters
Result<vector<char *>> run(int argc, char *argv[]) TD_WARN_UNUSED_RESULT;
friend StringBuilder &operator<<(StringBuilder &sb, const OptionsParser &o);
friend StringBuilder &operator<<(StringBuilder &sb, const OptionParser &o);
private:
vector<Option> options_;

View File

@ -1,123 +0,0 @@
//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/utils/OptionsParser.h"
#if TD_HAVE_GETOPT
#include "getopt.h"
#endif
#if !TD_WINDOWS
#include <getopt.h>
#include <unistd.h>
#endif
namespace td {
void OptionsParser::set_description(string description) {
description_ = std::move(description);
}
void OptionsParser::add_option(Option::Type type, char short_key, Slice long_key, Slice description,
std::function<Status(Slice)> callback) {
options_.push_back(Option{type, short_key, long_key.str(), description.str(), std::move(callback)});
}
void OptionsParser::add_option(char short_key, Slice long_key, Slice description,
std::function<Status(Slice)> callback) {
add_option(Option::Type::Arg, short_key, long_key, description, std::move(callback));
}
void OptionsParser::add_option(char short_key, Slice long_key, Slice description,
std::function<Status(void)> callback) {
// Ouch. There must be some better way
add_option(Option::Type::NoArg, short_key, long_key, description,
std::bind([](std::function<Status(void)> &func, Slice) { return func(); }, std::move(callback),
std::placeholders::_1));
}
Result<int> OptionsParser::run(int argc, char *argv[]) {
#if TD_HAVE_GETOPT
char buff[1024];
StringBuilder sb(MutableSlice{buff, sizeof(buff)});
for (auto &opt : options_) {
sb << opt.short_key;
if (opt.type == Option::Type::Arg) {
sb << ":";
}
}
if (sb.is_error()) {
return Status::Error("Can't parse options");
}
CSlice short_options = sb.as_cslice();
vector<option> long_options;
for (auto &opt : options_) {
if (opt.long_key.empty()) {
continue;
}
option o;
o.flag = nullptr;
o.val = opt.short_key;
o.has_arg = opt.type == Option::Type::Arg ? required_argument : no_argument;
o.name = opt.long_key.c_str();
long_options.push_back(o);
}
long_options.push_back({nullptr, 0, nullptr, 0});
while (true) {
int opt_i = getopt_long(argc, argv, short_options.c_str(), &long_options[0], nullptr);
if (opt_i == ':') {
return Status::Error("Missing argument");
}
if (opt_i == '?') {
return Status::Error("Unrecognized option");
}
if (opt_i == -1) {
break;
}
bool found = false;
for (auto &opt : options_) {
if (opt.short_key == opt_i) {
Slice arg;
if (opt.type == Option::Type::Arg) {
arg = Slice(optarg);
}
auto status = opt.arg_callback(arg);
if (status.is_error()) {
return std::move(status);
}
found = true;
break;
}
}
if (!found) {
return Status::Error("Unknown argument");
}
}
return optind;
#else
return -1;
#endif
}
StringBuilder &operator<<(StringBuilder &sb, const OptionsParser &o) {
sb << o.description_ << "\n";
for (auto &opt : o.options_) {
sb << "-" << opt.short_key;
if (!opt.long_key.empty()) {
sb << "|--" << opt.long_key;
}
if (opt.type != OptionsParser::Option::Type::NoArg) {
sb << "<arg>";
}
sb << "\t" << opt.description;
sb << "\n";
}
return sb;
}
} // namespace td

View File

@ -5,5 +5,4 @@
#cmakedefine01 TD_HAVE_CRC32C
#cmakedefine01 TD_HAVE_COROUTINES
#cmakedefine01 TD_HAVE_ABSL
#cmakedefine01 TD_HAVE_GETOPT
#cmakedefine01 TD_FD_DEBUG

View File

@ -0,0 +1,91 @@
//
// Copyright Aliaksei Levin (levlam@telegram.org), Arseny Smirnov (arseny30@gmail.com) 2014-2020
//
// Distributed under the Boost Software License, Version 1.0. (See accompanying
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
//
#include "td/utils/common.h"
#include "td/utils/misc.h"
#include "td/utils/OptionParser.h"
#include "td/utils/Slice.h"
#include "td/utils/Status.h"
#include "td/utils/tests.h"
TEST(OptionParser, run) {
td::OptionParser options;
options.set_description("test description");
td::vector<td::string> args;
auto run_option_parser = [&](td::string command_line) {
args = td::full_split(command_line, ' ');
td::vector<char *> argv;
argv.push_back("exename");
for (auto &arg : args) {
argv.push_back(&arg[0]);
}
return options.run(static_cast<int>(argv.size()), &argv[0]);
};
td::uint64 chosen_options = 0;
td::vector<td::string> chosen_parameters;
auto test_success = [&](td::string command_line, td::uint64 expected_options,
td::vector<td::string> expected_parameters, td::vector<td::string> expected_result) {
chosen_options = 0;
chosen_parameters.clear();
auto result = run_option_parser(command_line);
ASSERT_TRUE(result.is_ok());
ASSERT_EQ(expected_options, chosen_options);
ASSERT_EQ(expected_parameters, chosen_parameters);
ASSERT_EQ(expected_result.size(), result.ok().size());
for (size_t i = 0; i < expected_result.size(); i++) {
ASSERT_STREQ(expected_result[i], td::string(result.ok()[i]));
}
};
auto test_fail = [&](td::string command_line) {
auto result = run_option_parser(command_line);
ASSERT_TRUE(result.is_error());
};
options.add_option('q', "", "", [&] {
chosen_options += 1;
return td::Status::OK();
});
options.add_option('\0', "http-port2", "", [&] {
chosen_options += 10;
return td::Status::OK();
});
options.add_option('p', "http-port", "", [&](td::Slice parameter) {
chosen_options += 100;
chosen_parameters.push_back(parameter.str());
return td::Status::OK();
});
options.add_option('v', "test", "", [&] {
chosen_options += 1000;
return td::Status::OK();
});
test_fail("-http-port2");
test_success("-", 0, {}, {"-"});
test_fail("--http-port");
test_fail("--http-port3");
test_fail("--http-por");
test_fail("--http-port2=1");
test_fail("--q");
test_fail("-qvp");
test_fail("-p");
test_fail("-u");
test_success("-q", 1, {}, {});
test_success("-vvvvvvvvvv", 10000, {}, {});
test_success("-qpv", 101, {"v"}, {});
test_success("-qp -v", 101, {"-v"}, {});
test_success("-qp --http-port2", 101, {"--http-port2"}, {});
test_success("-qp -- -v", 1101, {"--"}, {});
test_success("-qvqvpqv", 2102, {"qv"}, {});
test_success("aba --http-port2 caba --http-port2 dabacaba", 20, {}, {"aba", "caba", "dabacaba"});
test_success("das -pqwerty -- -v asd --http-port", 100, {"qwerty"}, {"das", "-v", "asd", "--http-port"});
test_success("-p option --http-port option2 --http-port=option3 --http-port=", 400,
{"option", "option2", "option3", ""}, {});
test_success("", 0, {}, {});
test_success("a", 0, {}, {"a"});
test_success("", 0, {}, {});
}

View File

@ -19,7 +19,7 @@
int main(int argc, char **argv) {
td::init_openssl_threads();
// TODO port OptionsParser to Windows
// TODO use OptionParser
td::TestsRunner &runner = td::TestsRunner::get_default();
SET_VERBOSITY_LEVEL(VERBOSITY_NAME(ERROR));
for (int i = 1; i < argc; i++) {