From 91f3dfaa5e375c5d863b18a6ee8d534bd3cf144c Mon Sep 17 00:00:00 2001 From: BodgeMaster <> Date: Thu, 14 Jul 2022 03:13:48 +0200 Subject: [PATCH] lib/cli: Add a library for command line arguments parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/lib/cli.cpp | 192 +++++++++++++++++++++++++++++++ src/lib/cli.h++ | 89 ++++++++++++++ src/test/cli_argument_parser.cpp | 0 3 files changed, 281 insertions(+) create mode 100644 src/lib/cli.cpp create mode 100644 src/lib/cli.h++ create mode 100644 src/test/cli_argument_parser.cpp diff --git a/src/lib/cli.cpp b/src/lib/cli.cpp new file mode 100644 index 0000000..539e942 --- /dev/null +++ b/src/lib/cli.cpp @@ -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 +#include +#include + +#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 flags, std::vector unpositionalArguments, std::vector positionalArguments) { + this->wrongUsage = false; + this->wrongUsageMessages = std::vector(); + this->positionalArguments = positionalArguments; + // create lookup tables for all flags and unpositional arguments + // by their names + this->flagsByShortName = std::map(); + this->flagsByLongName = std::map(); + for (Flag flag: flags) { + Flag* flagPointer = new Flag(); + *flagPointer = flag; + this->flagsByShortName[flag.shortName] = flagPointer; + this->flagsByLongName[flag.longName] = flagPointer; + } + this->argumentsByShortName = std::map(); + this->argumentsByLongName = std::map(); + 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::size_type positionalArgumentCounter = 0; + for (int i=0; iwrongUsage = 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::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::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; + } + } + +} diff --git a/src/lib/cli.h++ b/src/lib/cli.h++ new file mode 100644 index 0000000..9d1a058 --- /dev/null +++ b/src/lib/cli.h++ @@ -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 +#include +#include + +#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 ] " + + 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 ] " + + bool present; + std::string value; + + PositionalArgument(); + PositionalArgument(std::string description, std::string placeholder); + }; + + class ArgumentsParser { + bool wrongUsage; + std::vector wrongUsageMessages; + + std::string programName; + std::map flagsByShortName; + std::map flagsByLongName; + std::map argumentsByShortName; + std::map argumentsByLongName; + std::vector positionalArguments; + + // using int here bc that's how main() is defined + ArgumentsParser(int argc, char* argv[], std::vector flags, std::vector unpositionalArguments, std::vector positionalArguments); + ~ArgumentsParser(); + + ErrorOr getProgramName(); + ErrorOr getFlag(int argc, char* argv, char shortName); + ErrorOr getFlag(int argc, char* argv, std::string longName); + ErrorOr getPositionalArgument(int argc, char* argv, int position); + ErrorOr getUnpositionalArgument(int argc, char* argv, char shortName); + ErrorOr getUnpositionalArgument(int argc, char* argv, std::string longName); + + std::string getUsage(); + }; + +} diff --git a/src/test/cli_argument_parser.cpp b/src/test/cli_argument_parser.cpp new file mode 100644 index 0000000..e69de29