FOSS-VG/src/lib/cli.cpp

330 lines
16 KiB
C++

// Copyright 2022, FOSS-VG Developers and Contributers
//
// Author(s):
// BodgeMaster, Shwoomple
//
// 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 <string>
#include <vector>
#include <map>
#include "cli.hpp"
#include "error.hpp"
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;
}
Option::Option() {
this->present = false;
}
Option::Option(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;
}
Argument::Argument() {
this->present = false;
}
Argument::Argument(std::string placeholder, std::string description) {
this->description = description;
this->placeholder = placeholder;
this->present = false;
}
// using int here bc that's how main() is defined
ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments) {
this->wrongUsage = false;
this->wrongUsageMessages = std::vector<std::string>();
this->programName = std::string(argv[0]);
this->arguments = arguments;
// create lookup tables for all flags and options 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->optionsByShortName = std::map<char, Option*>();
this->optionsByLongName = std::map<std::string, Option*>();
for (Option option: options) {
Option* optionPointer = new Option();
*optionPointer = option;
this->optionsByShortName[option.shortName] = optionPointer;
this->optionsByLongName[option.longName] = optionPointer;
}
Option* optionWaitingForValue = nullptr;
std::vector<CLI::Argument>::size_type argumentCounter = 0;
for (int i=1; i<argc; i++) {
std::string argument(argv[i]);
if (argument[0]=='-') {
// do we have unfinished business?
if (optionWaitingForValue!=nullptr) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+optionWaitingForValue->longName);
optionWaitingForValue = 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 option or flag?
std::string argumentName = argument.substr(2,argument.length()-2);
if (flagsByLongName.contains(argumentName)) {
// flag
flagsByLongName[argumentName]->present = true;
} else if (optionsByLongName.contains(argumentName)) {
// option
optionsByLongName[argumentName]->present = true;
optionWaitingForValue = optionsByLongName[argumentName];
if (i+1 == argc) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+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+1, argument.length()-position-1);
std::string argumentName = argument.substr(2, position-2);
if (optionsByLongName.contains(argumentName)) {
optionsByLongName[argumentName]->present = true;
optionsByLongName[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 j=1; j<(int) argument.length(); j++) {
// is option or flag?
if (flagsByShortName.contains(argument[j])) {
// flag
flagsByShortName[argument[j]]->present = true;
} else if (optionsByShortName.contains(argument[j])) {
// option
optionsByShortName[argument[j]]->present = true;
//FIXME: see above
if (j+1==(int) argument.length()) {
optionWaitingForValue = optionsByShortName[argument[j]];
if (i+1 == argc) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Argument expects value but has none: ")+this->optionsByShortName[argument[j]]->longName);
}
} else {
//assume the rest of the argv is a concatenated argument value
optionsByShortName[argument[j]]->value = argument.substr(j+1, argument.length()-j-1);
break;
}
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Unknown argument or flag(s): ")+argument.substr(j, argument.length()-j));
// 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 {
// argument or value for option?
if (optionWaitingForValue==nullptr) {
// argument
if (argumentCounter < this->arguments.size()) {
this->arguments.at(argumentCounter).present = true;
this->arguments.at(argumentCounter).value = argument;
} else {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Too many arguments! Unexpected encounter of: ")+argument);
}
argumentCounter++;
} else {
// value for option
optionWaitingForValue->value = argument;
optionWaitingForValue = nullptr;
}
}
}
for (Argument const& argument: this->arguments) {
if (!argument.present) {
this->wrongUsage = true;
this->wrongUsageMessages.push_back(std::string("Too few arguments! Missing: ")+argument.placeholder);
}
}
}
ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments, std::string description): ArgumentsParser::ArgumentsParser(argc, argv, flags, options, arguments) {
this->description = description;
}
ArgumentsParser::ArgumentsParser(int argc, const char* const argv[], std::vector<Flag> flags, std::vector<Option> options, std::vector<Argument> arguments, std::string description, std::string additionalInfo): ArgumentsParser::ArgumentsParser(argc, argv, flags, options, arguments) {
this->description = description;
this->additionalInfo = additionalInfo;
}
ArgumentsParser::~ArgumentsParser() {
for (auto const& [shortName, flag]: this->flagsByShortName) {
delete flag;
}
for (auto const& [shortName, option]: this->optionsByShortName) {
delete option;
}
}
ErrorOr<bool> ArgumentsParser::getFlag(char shortName) {
if (!this->flagsByShortName.contains(shortName)) return ErrorOr<bool>(true, ErrorCodes::UNKNOWN_KEY, false);
if (this->wrongUsage) {
if (this->flagsByShortName[shortName]->present) return ErrorOr<bool>(true, ErrorCodes::WRONG_USAGE, true);
else return ErrorOr<bool>(true, ErrorCodes::NOT_PRESENT, false);
}
if (this->flagsByShortName[shortName]->present) return ErrorOr<bool>(true);
else return ErrorOr<bool>(false, ErrorCodes::NOT_PRESENT, false);
}
ErrorOr<bool> ArgumentsParser::getFlag(std::string longName) {
if (!this->flagsByLongName.contains(longName)) return ErrorOr<bool>(true, ErrorCodes::UNKNOWN_KEY, false);
if (this->wrongUsage) {
if (this->flagsByLongName[longName]->present) return ErrorOr<bool>(true, ErrorCodes::WRONG_USAGE, true);
else return ErrorOr<bool>(true, ErrorCodes::NOT_PRESENT, false);
}
if (this->flagsByLongName[longName]->present) return ErrorOr<bool>(true);
else return ErrorOr<bool> (false, ErrorCodes::NOT_PRESENT, false);
}
ErrorOr<std::string> ArgumentsParser::getArgument(std::vector<CLI::Argument>::size_type position){
if (position >= this->arguments.size()) return ErrorOr<std::string>(true, ErrorCodes::OUT_OF_RANGE, std::string(""));
if (this->wrongUsage) {
if (this->arguments.at(position).present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->arguments.at(position).value);
else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
}
return ErrorOr<std::string>(this->arguments.at(position).value);
}
ErrorOr<std::string> ArgumentsParser::getOption(char shortName) {
if (!this->optionsByShortName.contains(shortName)) return ErrorOr<std::string>(true, ErrorCodes::UNKNOWN_KEY, std::string(""));
if (this->wrongUsage) {
if (this->optionsByShortName[shortName]->present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->optionsByShortName[shortName]->value);
else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
}
if (this->optionsByShortName[shortName]->present) return ErrorOr<std::string>(this->optionsByShortName[shortName]->value);
// argument is not present, but this is not an error -> false, NOT_PRESENT, ""
else return ErrorOr<std::string>(false, ErrorCodes::NOT_PRESENT, std::string(""));
}
ErrorOr<std::string> ArgumentsParser::getOption(std::string longName) {
if (!this->optionsByLongName.contains(longName)) return ErrorOr<std::string>(true, ErrorCodes::UNKNOWN_KEY, std::string(""));
if (this->wrongUsage) {
if (this->optionsByLongName[longName]->present) return ErrorOr<std::string>(true, ErrorCodes::WRONG_USAGE, this->optionsByLongName[longName]->value);
else return ErrorOr<std::string>(true, ErrorCodes::NOT_PRESENT, std::string(""));
}
if (this->optionsByLongName[longName]->present) return ErrorOr<std::string>(this->optionsByLongName[longName]->value);
// argument is not present, but this is not an error -> false, NOT_PRESENT, ""
else return ErrorOr<std::string>(false, ErrorCodes::NOT_PRESENT, std::string(""));
}
std::string ArgumentsParser::getUsage(){
std::string usageString = "";
if (this->description != "") {
usageString += "Help: " + this->programName + "\n\n\t" + this->description + "\n\n";
}
usageString += "Usage: " + this->programName + " ";
if(!this->flagsByShortName.empty()){
usageString += "[-";
}
for(const auto& [key, value]: this->flagsByShortName){
usageString.push_back(key);
}
if(!this->flagsByShortName.empty()){
usageString += "] ";
}
for(const auto& [key, value]: this->optionsByShortName){
usageString += "[-";
usageString.push_back(key);
usageString += " " + value->placeholder + "] ";
}
for(const auto& argument: this->arguments){
usageString += argument.placeholder + " ";
}
usageString.push_back('\n');
if(!this->flagsByShortName.empty()){
usageString += "\nFlags:\n";
for(const auto& [key, value]: this->flagsByShortName){
usageString += "\t-";
usageString.push_back(key);
usageString += ", --" + value->longName + "\n\t\t" + value->description + "\n";
}
}
if(!this->optionsByShortName.empty()){
usageString += "\nOptions:\n";
for(const auto& [key, value]: this->optionsByShortName){
usageString += "\t-";
usageString.push_back(key);
usageString += " " + value->placeholder + ", --" + value->longName + "=" + value->placeholder + "\n\t\t" + value->description + "\n";
}
}
if(!this->arguments.empty()){
usageString += "\nArguments:\n";
for(const auto& argument: this->arguments){
usageString += "\t" + argument.placeholder + "\n\t\t" + argument.description + "\n";
}
}
if (this->additionalInfo != "") {
usageString += "\nAdditional Info:\n\n\t" + this->additionalInfo + "\n";
}
return usageString;
}
}