lib/cli: Add a library for command line arguments parsing

This has the parser itself already implemented, all other functionality is still
missing. So are tests.

Yes, I’m making my own getopt. Let me.
BodgeMaster-unfinished
BodgeMaster 2022-07-14 03:13:48 +02:00
parent 7e049fcfd1
commit 91f3dfaa5e
3 changed files with 281 additions and 0 deletions

192
src/lib/cli.cpp Normal file
View File

@ -0,0 +1,192 @@
// Copyright 2022, FOSS-VG Developers and Contributers
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html
#include "cli.h++"
#include <string>
#include <vector>
#include <map>
#include "error.h++"
namespace CLI {
Flag::Flag() {
this->present = false;
}
Flag::Flag(char shortName, std::string longName, std::string description) {
this->shortName = shortName;
this->longName = longName;
this->description = description;
this->present = false;
}
UnpositionalArgument::UnpositionalArgument() {
this->present = false;
}
UnpositionalArgument::UnpositionalArgument(char shortName, std::string longName, std::string placeholder, std::string description) {
this->shortName = shortName;
this->longName = longName;
this->description = description;
this->placeholder = placeholder;
this->present = false;
}
PositionalArgument::PositionalArgument() {
this->present = false;
}
PositionalArgument::PositionalArgument(std::string description, std::string placeholder) {
this->description = description;
this->placeholder = placeholder;
this->present = false;
}
// using int here bc that's how main() is defined
ArgumentsParser::ArgumentsParser(int argc, char* argv[], std::vector<Flag> flags, std::vector<UnpositionalArgument> unpositionalArguments, std::vector<PositionalArgument> positionalArguments) {
this->wrongUsage = false;
this->wrongUsageMessages = std::vector<std::string>();
this->positionalArguments = positionalArguments;
// create lookup tables for all flags and unpositional arguments
// by their names
this->flagsByShortName = std::map<char, Flag*>();
this->flagsByLongName = std::map<std::string, Flag*>();
for (Flag flag: flags) {
Flag* flagPointer = new Flag();
*flagPointer = flag;
this->flagsByShortName[flag.shortName] = flagPointer;
this->flagsByLongName[flag.longName] = flagPointer;
}
this->argumentsByShortName = std::map<char, UnpositionalArgument*>();
this->argumentsByLongName = std::map<std::string, UnpositionalArgument*>();
for (UnpositionalArgument unpositionalArgument: unpositionalArguments) {
UnpositionalArgument* argumentPointer = new UnpositionalArgument();
*argumentPointer = unpositionalArgument;
this->argumentsByShortName[unpositionalArgument.shortName] = argumentPointer;
this->argumentsByLongName[unpositionalArgument.longName] = argumentPointer;
}
UnpositionalArgument* argumentWaitingForValue = nullptr;
std::vector<CLI::PositionalArgument>::size_type positionalArgumentCounter = 0;
for (int i=0; i<argc; i++) {
std::string argument(argv[i]);
if (argument[0]=='-') {
// do we have unfinished business?
if (argumentWaitingForValue!=nullptr) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+argumentWaitingForValue->longName);
argumentWaitingForValue = nullptr;
}
// long name or short name?
if (argument[1]=='-') {
// long name
//FIXME: instead of auto, this should be what string
// length is defined as
// (std::__cxx11::basic_string<char>::size_type ?)
// argument with =value specified?
auto position = argument.find("=");
if (position==std::string::npos) {
// no =value
//is argument or flag?
std::string argumentName = argument.substr(2,argument.length());
if (flagsByLongName.contains(argumentName)) {
// flag
flagsByLongName[argumentName]->present = true;
} else if (argumentsByLongName.contains(argumentName)) {
// unpositional argument
argumentsByLongName[argumentName]->present = true;
argumentWaitingForValue = argumentsByLongName[argumentName];
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Unknown argument or flag: ")+argument);
}
} else {
// has =value
std::string value = argument.substr(position, argument.length());
std::string argumentName = argument.substr(2, position);
if (argumentsByLongName.contains(argumentName)) {
argumentsByLongName[argumentName]->present = true;
argumentsByLongName[argumentName]->value = value;
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Unknown argument (or it's a flag that doesn't take a value): ")+argument);
}
}
} else {
// short name
//FIXME: instead of int, this should use what string
// length is defined as
// (std::__cxx11::basic_string<char>::size_type ?)
// starting at 1 because 0 is '-'
for (int i=1; i<(int) argument.length(); i++) {
//is argument or flag?
if (flagsByShortName.contains(argument[i])) {
flagsByShortName[argument[i]]->present = true;
} else if (argumentsByShortName.contains(argument[i])) {
argumentsByShortName[argument[i]]->present = true;
//FIXME: see above
if (i+1==(int) argument.length()) {
argumentWaitingForValue = argumentsByShortName[argument[i]];
} else {
//assume the rest of the argv is a concatenated argument value
argumentsByShortName[argument[i]]->value = argument.substr(i+1, argument.length());
break;
}
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Unknown argument or flag(s): ")+argument.substr(i, argument.length()));
// err on the side of caution to ensure that
// no unwanted options get activated on programs
// that deal gracefully with unrecognized command
// line parameters
break;
}
}
}
} else {
// positional argument or value for unpositional arg?
if (argumentWaitingForValue==nullptr) {
// positional argument
if (positionalArgumentCounter < this->positionalArguments.size()) {
this->positionalArguments.at(positionalArgumentCounter).present = true;
this->positionalArguments.at(positionalArgumentCounter).value = argument;
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Too many positional arguments. Unexpected encounter of: ")+argument);
}
positionalArgumentCounter++;
} else {
// value for unpositional argument
argumentWaitingForValue->value = argument;
}
}
}
for (PositionalArgument const& positionalArgument: this->positionalArguments) {
if (!positionalArgument.present) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Too few positional arguments! Missing: ")+positionalArgument.placeholder);
}
}
}
ArgumentsParser::~ArgumentsParser() {
//TODO: check that this actually runs
for (auto const& [shortName, flag]: this->flagsByShortName) {
delete flag;
}
for (auto const& [shortName, unpositionalArgument]: this->argumentsByShortName) {
delete unpositionalArgument;
}
}
}

89
src/lib/cli.h++ Normal file
View File

@ -0,0 +1,89 @@
// Copyright 2022, FOSS-VG Developers and Contributers
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published
// by the Free Software Foundation, version 3.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied
// warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
// See the GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// version 3 along with this program.
// If not, see https://www.gnu.org/licenses/agpl-3.0.en.html
#pragma once
#include <string>
#include <vector>
#include <map>
#include "error.h++"
namespace CLI {
struct Flag {
char shortName;
std::string longName;
// used for automatic usage generation
std::string description;
bool present;
Flag();
Flag(char shortName, std::string longName, std::string description);
};
struct UnpositionalArgument {
char shortName;
std::string longName;
// used for automatic usage generation
std::string description;
std::string placeholder; // the "COUNT" in "ping [-c <COUNT>] <HOST>"
bool present;
std::string value;
UnpositionalArgument();
UnpositionalArgument(char shortName, std::string longName, std::string placeholder, std::string description);
};
struct PositionalArgument {
// used for automatic usage generation
std::string description;
std::string placeholder; // the "HOST" in "ping [-c <COUNT>] <HOST>"
bool present;
std::string value;
PositionalArgument();
PositionalArgument(std::string description, std::string placeholder);
};
class ArgumentsParser {
bool wrongUsage;
std::vector<std::string> wrongUsageMessages;
std::string programName;
std::map<char, Flag*> flagsByShortName;
std::map<std::string, Flag*> flagsByLongName;
std::map<char, UnpositionalArgument*> argumentsByShortName;
std::map<std::string, UnpositionalArgument*> argumentsByLongName;
std::vector<PositionalArgument> positionalArguments;
// using int here bc that's how main() is defined
ArgumentsParser(int argc, char* argv[], std::vector<Flag> flags, std::vector<UnpositionalArgument> unpositionalArguments, std::vector<PositionalArgument> positionalArguments);
~ArgumentsParser();
ErrorOr<std::string> getProgramName();
ErrorOr<bool> getFlag(int argc, char* argv, char shortName);
ErrorOr<bool> getFlag(int argc, char* argv, std::string longName);
ErrorOr<std::string> getPositionalArgument(int argc, char* argv, int position);
ErrorOr<std::string> getUnpositionalArgument(int argc, char* argv, char shortName);
ErrorOr<std::string> getUnpositionalArgument(int argc, char* argv, std::string longName);
std::string getUsage();
};
}

View File