Skip to content

Commit

Permalink
Support multi command in CLI, with commands starting without -- (for …
Browse files Browse the repository at this point in the history
…instance, 'trade' instead of '--trade')
  • Loading branch information
sjanel committed Oct 1, 2023
1 parent 7e5cc4a commit 15a5194
Show file tree
Hide file tree
Showing 11 changed files with 438 additions and 302 deletions.
194 changes: 109 additions & 85 deletions README.md

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions src/engine/include/coincentercommands.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,17 @@ class CoincenterCommands {
CoincenterCommands() noexcept(std::is_nothrow_default_constructible_v<Commands>) = default;

// Builds a CoincenterCommands and add commands from given command line options.
explicit CoincenterCommands(const CoincenterCmdLineOptions &cmdLineOptions);
explicit CoincenterCommands(const CoincenterCmdLineOptions &cmdLineOptions)
: CoincenterCommands(std::span<const CoincenterCmdLineOptions>{&cmdLineOptions, 1U}) {}

static CoincenterCmdLineOptions ParseOptions(int argc, const char *argv[]);
// Builds a CoincenterCommands and add commands from given command line options span.
explicit CoincenterCommands(std::span<const CoincenterCmdLineOptions> cmdLineOptionsSpan);

static vector<CoincenterCmdLineOptions> ParseOptions(int argc, const char *argv[]);

/// @brief Set this CoincenterCommands from given command line options.
/// @return false if only help or version is asked, true otherwise
bool setFromOptions(const CoincenterCmdLineOptions &cmdLineOptions);
bool addOption(const CoincenterCmdLineOptions &cmdLineOptions);

std::span<const CoincenterCommand> commands() const { return _commands; }

Expand Down
173 changes: 83 additions & 90 deletions src/engine/include/coincenteroptions.hpp

Large diffs are not rendered by default.

14 changes: 12 additions & 2 deletions src/engine/include/commandlineoption.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ class CommandLineOption {
constexpr std::strong_ordering operator<=>(const CommandLineOption& o) const;

private:
static constexpr std::string_view kLegacyFullNamePrefixOption = "--";

std::string_view _optionGroupName;
std::string_view _fullName;
std::string_view _valueDescription;
Expand Down Expand Up @@ -92,9 +94,17 @@ struct AllowedCommandLineOptionsBase {

constexpr bool CommandLineOption::matches(std::string_view optName) const {
if (optName.size() == 2 && optName.front() == '-' && optName.back() == _shortName) {
return true;
return true; // it is a short hand flag
}
if (optName == _fullName) {
return true; // standard full match
}
if (optName.starts_with(kLegacyFullNamePrefixOption)) {
// backwards compatibility check
optName.remove_prefix(kLegacyFullNamePrefixOption.length());
return optName == _fullName;
}
return optName == _fullName;
return false;
}

constexpr std::strong_ordering CommandLineOption::operator<=>(const CommandLineOption& o) const {
Expand Down
146 changes: 88 additions & 58 deletions src/engine/include/commandlineoptionsparser.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,70 +35,73 @@ template <class... Ts>
overloaded(Ts...) -> overloaded<Ts...>;

template <class OptValueType>
class CommandLineOptionsParser : private OptValueType {
class CommandLineOptionsParser {
public:
// TODO: Once clang implements P0634R3, remove 'typename' here
using CommandLineOptionType = typename AllowedCommandLineOptionsBase<OptValueType>::CommandLineOptionType;
using CommandLineOptionWithValue = typename AllowedCommandLineOptionsBase<OptValueType>::CommandLineOptionWithValue;

template <unsigned N>
explicit CommandLineOptionsParser(const CommandLineOptionWithValue (&init)[N])
: _opts(std::begin(init), std::end(init)) {}
explicit CommandLineOptionsParser(const CommandLineOptionWithValue (&init)[N]) {
append(init);
}

template <unsigned N>
void append(const CommandLineOptionWithValue (&opts)[N]) {
_opts.append(std::begin(opts), std::end(opts));
CommandLineOptionsParser& append(const CommandLineOptionWithValue (&opts)[N]) {
auto insertedIt = _opts.insert(_opts.end(), std::begin(opts), std::end(opts));
const auto sortFunc = [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; };
std::sort(insertedIt, _opts.end(), sortFunc);
std::inplace_merge(_opts.begin(), insertedIt, _opts.end(), sortFunc);
return *this;
}

OptValueType parse(std::span<const char*> vargv) {
// First register the callbacks
for (const CommandLineOptionWithValue& arg : _opts) {
registerCallback(arg.first, arg.second);
OptValueType parse(std::span<const char*> groupedArguments) {
std::map<CommandLineOption, CallbackType> callbacks;
OptValueType data;
for (const auto& [cmdLineOption, prop] : _opts) {
callbacks[cmdLineOption] = registerCallback(cmdLineOption, prop, data);
}

vargv = vargv.last(vargv.size() - 1U); // skip first argument which is program name
const int vargvSize = static_cast<int>(vargv.size());
for (int idxOpt = 0; idxOpt < vargvSize; ++idxOpt) {
const char* argStr = vargv[idxOpt];
const int nbArgs = static_cast<int>(groupedArguments.size());
for (int argPos = 0; argPos < nbArgs; ++argPos) {
std::string_view argStr(groupedArguments[argPos]);
if (std::ranges::none_of(_opts, [argStr](const auto& opt) { return opt.first.matches(argStr); })) {
throw invalid_argument("Unrecognized command-line option {}", argStr);
}

for (auto& cbk : _callbacks) {
cbk.second(idxOpt, vargv);
for (auto& callback : callbacks) {
callback.second(argPos, groupedArguments);
}
}

return static_cast<OptValueType>(*this);
return data;
}

OptValueType parse(int argc, const char* argv[]) { return parse(std::span(argv, argc)); }

template <typename StreamType>
void displayHelp(std::string_view programName, StreamType& stream) {
stream << "usage: " << programName << " <options>" << std::endl;
void displayHelp(std::string_view programName, StreamType& stream) const {
stream << "usage: " << programName << " <general options> <command(s)>" << std::endl;
if (_opts.empty()) {
return;
}
stream << "Options:" << std::endl;

std::ranges::sort(_opts, [](const auto& lhs, const auto& rhs) { return lhs.first < rhs.first; });

int lenFirstRows = 0;
static constexpr int kMaxCharLine = 120;
for (const auto& [opt, pm] : _opts) {
int lenRows = static_cast<int>(opt.fullName().size() + opt.valueDescription().size() + 1);
if (opt.hasShortName()) {
lenRows += 4;
}
lenFirstRows = std::max(lenFirstRows, lenRows);
}
std::string_view currentGroup, previousGroup;
std::string_view currentGroup;
std::string_view previousGroup;
for (const auto& [opt, pm] : _opts) {
currentGroup = opt.optionGroupName();
if (currentGroup != previousGroup) {
stream << std::endl << ' ' << currentGroup << std::endl;
}
if (opt.fullName()[0] != '-') {
stream << std::endl;
}
string firstRowsStr(opt.fullName());
if (opt.hasShortName()) {
firstRowsStr.append(", -");
Expand Down Expand Up @@ -143,57 +146,82 @@ class CommandLineOptionsParser : private OptValueType {
}

private:
static constexpr int kMaxCharLine = 120;

template <class>
friend class CommandLineOptionsParserIterator;

using CallbackType = std::function<void(int&, std::span<const char*>)>;

vector<CommandLineOptionWithValue> _opts;
std::map<CommandLineOption, CallbackType> _callbacks;
[[nodiscard]] bool isOptionValue(std::string_view opt) const {
return std::ranges::none_of(
_opts, [opt](const CommandLineOptionWithValue& cmdLineOpt) { return cmdLineOpt.first.matches(opt); });
}

static bool IsOptionValue(const char* argv) { return argv[0] != '-' || isdigit(argv[1]); }
static bool IsOptionPositiveInt(std::string_view opt) { return std::ranges::all_of(opt, isdigit); }

void registerCallback(const CommandLineOption& commandLineOption, CommandLineOptionType prop) {
_callbacks[commandLineOption] = [this, &commandLineOption, prop](int& idx, std::span<const char*> argv) {
static bool IsOptionInt(std::string_view opt) {
return ((opt[0] == '-' || opt[0] == '+') && std::all_of(std::next(opt.begin()), opt.end(), isdigit)) ||
IsOptionPositiveInt(opt);
}

CallbackType registerCallback(const CommandLineOption& commandLineOption, CommandLineOptionType prop,
OptValueType& data) {
return [this, &commandLineOption, prop, &data](int& idx, std::span<const char*> argv) {
if (commandLineOption.matches(argv[idx])) {
std::visit(overloaded{
[this](bool OptValueType::*arg) { this->*arg = true; },
[this, &idx, argv, &commandLineOption](int OptValueType::*arg) {
if (idx + 1U < argv.size() && IsOptionValue(argv[idx + 1])) {
const char* beg = argv[idx + 1];
const char* end = beg + strlen(beg);
this->*arg = FromString<int>(std::string_view(beg, end));
++idx;
} else {
ThrowExpectingValueException(commandLineOption);
// bool value matcher
[this, &data](bool OptValueType::*arg) { data.*arg = true; },

// int value matcher
[this, &data, &idx, argv, &commandLineOption](int OptValueType::*arg) {
if (idx + 1U < argv.size()) {
std::string_view nextOpt(argv[idx + 1]);
if (IsOptionInt(nextOpt)) {
data.*arg = FromString<int>(nextOpt);
++idx;
return;
}
}
ThrowExpectingValueException(commandLineOption);
},
[this, &idx, argv](CommandLineOptionalInt OptValueType::*arg) {
if (idx + 1U < argv.size() && IsOptionValue(argv[idx + 1])) {
const char* beg = argv[idx + 1];
const char* end = beg + strlen(beg);
this->*arg = FromString<int>(std::string_view(beg, end));
++idx;
} else {
this->*arg = CommandLineOptionalInt(CommandLineOptionalInt::State::kOptionPresent);

// CommandLineOptionalInt value matcher
[this, &data, &idx, argv](CommandLineOptionalInt OptValueType::*arg) {
data.*arg = CommandLineOptionalInt(CommandLineOptionalInt::State::kOptionPresent);
if (idx + 1U < argv.size()) {
std::string_view nextOpt(argv[idx + 1]);
if (IsOptionInt(nextOpt)) {
data.*arg = FromString<int>(nextOpt);
++idx;
}
}
},
[this, &idx, argv, &commandLineOption](std::string_view OptValueType::*arg) {
if (idx + 1U < argv.size() && IsOptionValue(argv[idx + 1])) {
this->*arg = argv[idx + 1];

// std::string_view value matcher
[this, &data, &idx, argv, &commandLineOption](std::string_view OptValueType::*arg) {
if (idx + 1U < argv.size()) {
data.*arg = std::string_view(argv[idx + 1]);
++idx;
} else {
ThrowExpectingValueException(commandLineOption);
}
},
[this, &idx, argv](std::optional<std::string_view> OptValueType::*arg) {
if (idx + 1U < argv.size() && IsOptionValue(argv[idx + 1])) {
this->*arg = argv[idx + 1];

// optional std::string_view value matcher
[this, &data, &idx, argv](std::optional<std::string_view> OptValueType::*arg) {
if (idx + 1U < argv.size() && this->isOptionValue(argv[idx + 1])) {
data.*arg = std::string_view(argv[idx + 1]);
++idx;
} else {
this->*arg = std::string_view();
data.*arg = std::string_view();
}
},
[this, &idx, argv, &commandLineOption](Duration OptValueType::*arg) {
if (idx + 1U < argv.size() && IsOptionValue(argv[idx + 1])) {
this->*arg = ParseDuration(argv[idx + 1]);

// duration value matcher
[this, &data, &idx, argv, &commandLineOption](Duration OptValueType::*arg) {
if (idx + 1U < argv.size()) {
data.*arg = ParseDuration(argv[idx + 1]);
++idx;
} else {
ThrowExpectingValueException(commandLineOption);
Expand All @@ -203,7 +231,9 @@ class CommandLineOptionsParser : private OptValueType {
prop);
}
};
};
}

vector<CommandLineOptionWithValue> _opts;
};

} // namespace cct
47 changes: 34 additions & 13 deletions src/engine/src/coincentercommands.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#include "coincentercommands.hpp"

#include <algorithm>
#include <chrono>
#include <filesystem>
#include <iostream>
Expand All @@ -9,23 +10,45 @@
#include "coincentercommandtype.hpp"
#include "coincenteroptions.hpp"
#include "commandlineoptionsparser.hpp"
#include "commandlineoptionsparseriterator.hpp"
#include "stringoptionparser.hpp"
#include "tradedefinitions.hpp"

namespace cct {

CoincenterCmdLineOptions CoincenterCommands::ParseOptions(int argc, const char *argv[]) {
vector<CoincenterCmdLineOptions> CoincenterCommands::ParseOptions(int argc, const char *argv[]) {
using OptValueType = CoincenterCmdLineOptions;

auto parser = CommandLineOptionsParser<OptValueType>(CoincenterAllowedOptions<OptValueType>::value);
CoincenterCmdLineOptions parsedOptions = parser.parse(argc, argv);

auto programName = std::filesystem::path(argv[0]).filename().string();
if (parsedOptions.help) {
parser.displayHelp(programName, std::cout);
} else if (parsedOptions.version) {
CoincenterCmdLineOptions::PrintVersion(programName);

vector<CoincenterCmdLineOptions> parsedOptions;

std::span<const char *> allArguments(argv, argc);
allArguments = allArguments.last(allArguments.size() - 1U); // skip first argument which is program name

CommandLineOptionsParserIterator parserIt(parser, allArguments);

// Support for command line multiple commands. Only full name flags are supported for multi command line commands.
// Note: maybe it better to just decommission short hand flags.
while (parserIt.hasNext()) {
std::span<const char *> groupedArguments = parserIt.next();

CoincenterCmdLineOptions groupParsedOptions = parser.parse(groupedArguments);
if (groupedArguments.empty()) {
groupParsedOptions.help = true;
}
if (groupParsedOptions.help) {
parser.displayHelp(programName, std::cout);
} else if (groupParsedOptions.version) {
CoincenterCmdLineOptions::PrintVersion(programName);
} else {
// Only store commands if they are not 'help' nor 'version'
parsedOptions.push_back(std::move(groupParsedOptions));
}
}

return parsedOptions;
}

Expand Down Expand Up @@ -103,15 +126,13 @@ WithdrawOptions ComputeWithdrawOptions(const CoincenterCmdLineOptions &cmdLineOp

} // namespace

CoincenterCommands::CoincenterCommands(const CoincenterCmdLineOptions &cmdLineOptions) {
setFromOptions(cmdLineOptions);
}

bool CoincenterCommands::setFromOptions(const CoincenterCmdLineOptions &cmdLineOptions) {
if (cmdLineOptions.help || cmdLineOptions.version) {
return false;
CoincenterCommands::CoincenterCommands(std::span<const CoincenterCmdLineOptions> cmdLineOptionsSpan) {
for (const CoincenterCmdLineOptions &cmdLineOptions : cmdLineOptionsSpan) {
addOption(cmdLineOptions);
}
}

bool CoincenterCommands::addOption(const CoincenterCmdLineOptions &cmdLineOptions) {
if (cmdLineOptions.repeats.isPresent()) {
if (cmdLineOptions.repeats.isSet()) {
_repeats = *cmdLineOptions.repeats;
Expand Down
Loading

0 comments on commit 15a5194

Please sign in to comment.