open-story-teller/story-editor-legacy/ost_convert/command_line.cpp
2023-12-13 13:44:23 +01:00

152 lines
4.5 KiB
C++

#include "command_line.h"
#include <algorithm>
#include <iomanip>
CommandLine::CommandLine(std::string description)
: mDescription(std::move(description)) {
}
void CommandLine::addArgument(std::vector<std::string> const& flags,
Value const& value, std::string const& help) {
mArguments.emplace_back(Argument{flags, value, help});
}
void CommandLine::printHelp(std::ostream& os) const {
// Print the general description.
os << mDescription << std::endl;
// Find the argument with the longest combined flag length (in order
// to align the help messages).
uint32_t maxFlagLength = 0;
for (auto const& argument : mArguments) {
uint32_t flagLength = 0;
for (auto const& flag : argument.mFlags) {
// Plus comma and space.
flagLength += static_cast<uint32_t>(flag.size()) + 2;
}
maxFlagLength = std::max(maxFlagLength, flagLength);
}
// Now print each argument.
for (auto const& argument : mArguments) {
std::string flags;
for (auto const& flag : argument.mFlags) {
flags += flag + ", ";
}
// Remove last comma and space and add padding according to the
// longest flags in order to align the help messages.
std::stringstream sstr;
sstr << std::left << std::setw(maxFlagLength)
<< flags.substr(0, flags.size() - 2);
// Print the help for each argument. This is a bit more involved
// since we do line wrapping for long descriptions.
size_t spacePos = 0;
size_t lineWidth = 0;
while (spacePos != std::string::npos) {
size_t nextspacePos = argument.mHelp.find_first_of(' ', spacePos + 1);
sstr << argument.mHelp.substr(spacePos, nextspacePos - spacePos);
lineWidth += nextspacePos - spacePos;
spacePos = nextspacePos;
if (lineWidth > 60) {
os << sstr.str() << std::endl;
sstr = std::stringstream();
sstr << std::left << std::setw(maxFlagLength - 1) << " ";
lineWidth = 0;
}
}
}
}
void CommandLine::parse(int argc, char* argv[]) const {
// Skip the first argument (name of the program).
int i = 1;
while (i < argc) {
// First we have to identify wether the value is separated by a space
// or a '='.
std::string flag(argv[i]);
std::string value;
bool valueIsSeparate = false;
// If there is an '=' in the flag, the part after the '=' is actually
// the value.
size_t equalPos = flag.find('=');
if (equalPos != std::string::npos) {
value = flag.substr(equalPos + 1);
flag = flag.substr(0, equalPos);
}
// Else the following argument is the value.
else if (i + 1 < argc) {
value = argv[i + 1];
valueIsSeparate = true;
}
// Search for an argument with the provided flag.
bool foundArgument = false;
for (auto const& argument : mArguments) {
if (std::find(argument.mFlags.begin(), argument.mFlags.end(), flag)
!= std::end(argument.mFlags)) {
foundArgument = true;
// In the case of booleans, there must not be a value present.
// So if the value is neither 'true' nor 'false' it is considered
// to be the next argument.
if (std::holds_alternative<bool*>(argument.mValue)) {
if (!value.empty() && value != "true" && value != "false") {
valueIsSeparate = false;
}
*std::get<bool*>(argument.mValue) = (value != "false");
}
// In all other cases there must be a value.
else if (value.empty()) {
throw std::runtime_error(
"Failed to parse command line arguments: "
"Missing value for argument \"" + flag + "\"!");
}
// For a std::string, we take the entire value.
else if (std::holds_alternative<std::string*>(argument.mValue)) {
*std::get<std::string*>(argument.mValue) = value;
}
// In all other cases we use a std::stringstream to
// convert the value.
else {
std::visit(
[&value](auto&& arg) {
std::stringstream sstr(value);
sstr >> *arg;
},
argument.mValue);
}
break;
}
}
// Print a warning if there was an unknown argument.
if (!foundArgument) {
std::cerr << "Ignoring unknown command line argument \"" << flag
<< "\"." << std::endl;
}
// Advance to the next flag.
++i;
// If the value was separated, we have to advance our index once more.
if (foundArgument && valueIsSeparate) {
++i;
}
}
}