diff --git a/.gitignore b/.gitignore index b155b5e..c2b05e6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .ccls-cache +.ccls *.o .tup diff --git a/examples/dc/dc.argspec b/examples/dc/dc.argspec new file mode 100644 index 0000000..3f26a79 --- /dev/null +++ b/examples/dc/dc.argspec @@ -0,0 +1,19 @@ +Program: +program dc +version "dc (GNU bc 1.07.1) 1.4.1\n\nCopyright 1994, 1997, 1998, 2000, 2001, 2003-2006, 2008, 2010, 2012-2017 Free Software Foundation, Inc." +license "This is free software; see the source for copying conditions.\nThere is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, to the extent permitted by law." +help "Email bug reports to: bug-dc@gnu.org" + +Usage: + +dc +dc OPTIONS + +OPTIONS = --expression | --file + +Arguments: + +--help, -h, , "shows this help message" +--version, -V, , "prints the version of the program" +--expression, -e, EXPR, "evaluate expression" +--file, -f, FILE, "evaluate contents of file" diff --git a/examples/dc/main.cpp b/examples/dc/main.cpp new file mode 100644 index 0000000..afdbcc9 --- /dev/null +++ b/examples/dc/main.cpp @@ -0,0 +1,18 @@ +#include "src/dcArgGrammarDriver.hpp" +#include + +using Result = dcArgGrammar::Driver::Result; + +int main(int argc, char* argv[]) { + dcArgGrammar::Driver driver{ argc, argv }; + auto res = driver.parse(); + + if (res != Result::success) { + if (res == Result::completedAction) + return 0; + + return -1; + } + + return 0; +} diff --git a/generator/.gitignore b/generator/.gitignore new file mode 100644 index 0000000..d107e94 --- /dev/null +++ b/generator/.gitignore @@ -0,0 +1,2 @@ +fcap +generated/ diff --git a/generator/Tupfile b/generator/Tupfile new file mode 100644 index 0000000..9a2eb7d --- /dev/null +++ b/generator/Tupfile @@ -0,0 +1,20 @@ +LEXER = flex +PARSER = bison +CXX = g++ +CXXFLAGS = -pedantic -std=c++17 -Wall -Wno-unused-parameter -Wno-reorder -Wno-sign-compare -Wno-address -Wno-noexcept-type -Wno-unknown-attributes -Wno-unknown-warning-option +CPPFLAGS = +LDFLAGS = +LIBS = -lmstch + +SRC_DIR = src +GEN_DIR = generated +TEMPL_DIR = templates +BUILD_DIR = build +PROG = fcap + +: foreach $(SRC_DIR)/*.yy |> $(PARSER) -b "$(GEN_DIR)/%B" "%f" |> "$(GEN_DIR)/%B.tab.cc" | "$(GEN_DIR)/%B.tab.hh" +: foreach $(SRC_DIR)/*.ll | $(GEN_DIR)/*.hh |> $(LEXER) -o "%o" "%f" |> "$(GEN_DIR)/%B.yy.cc" +: foreach $(TEMPL_DIR)/*.* |> ld --relocatable --format=binary --output="%o" "%f" |> "$(GEN_DIR)/template-%b.data" +: foreach $(GEN_DIR)/*.data |> objcopy --rename-section .data=.rodata,alloc,load,readonly,data,contents "%f" "%o" |> "$(BUILD_DIR)/%B.o" +: foreach $(GEN_DIR)/*.cc $(SRC_DIR)/*.cpp | $(GEN_DIR)/*.hh |> $(CXX) $(CPPFLAGS) $(CXXFLAGS) -I "$(SRC_DIR)" -I "$(GEN_DIR)" -c "%f" -o "%o" |> "$(BUILD_DIR)/%B.o" +: $(BUILD_DIR)/*.o |> $(CXX) %f $(LDFLAGS) $(LIBS) -o "%o" |> "$(PROG)" diff --git a/generator/demo/README.md b/generator/demo/README.md new file mode 100644 index 0000000..93f7620 --- /dev/null +++ b/generator/demo/README.md @@ -0,0 +1,31 @@ +# Demonstration of templates being re-written for a specific argument specification + +## Requirements + +* mustache +* bison (to compile) +* flex (to compile) + +## How to re-write + +```sh +mustache demo.yml ../templates/ +``` + +To save the result to a file: + +```sh +mustache demo.yml ../templates/ /path/to/output/file +``` + +## How to run + +Run `mustache` on all the templates in `../templates/`, and on _`main.cpp`_ (in this folder) to an empty output directory (`/path/to/output`). +Then: + +```sh +bison /path/to/output/*.yy +flex /path/to/output/*.ll +g++ /path/to/output/*.cpp /path/to/output*.cc -o demo +/path/to/output/demo +``` diff --git a/generator/demo/demo.yml b/generator/demo/demo.yml new file mode 100644 index 0000000..aefdedb --- /dev/null +++ b/generator/demo/demo.yml @@ -0,0 +1,53 @@ +--- +argspec: "dc" +any_parameters: true + +argument_tokens: + - argument: "expression" + usage: "evaluates an expression" + short_argument: "e" + clean_token: "EXPRESSION" + has_parameters: true + parameters: + - index: 1 + name: "expression" + - argument: "file" + usage: "evaluates the contents of a file" + short_argument: "f" + clean_token: "FILE" + has_parameters: true + parameters: + - index: 1 + name: "file" +help_token: + - usage: false + short_argument: "h" + +version_token: + - usage: false + short_argument: false + +license_token: + - usage: false + short_argument: false + +argument_explanation: false + +usage: + - flags: + - "OPTIONS" + positional: false + +usage_rules: + - rule_name: "OPTIONS" + options: + - option: "EXPRESSION" + has_next: true + - option: "FILE" + +version: "dc (GNU bc 1.07.1) 1.4.1\\n\\nCopyright 1994, 1997, 1998, 2000, 2001, 2003-2006, 2008, 2010, 2012-2017 Free Software Foundation, Inc." + +license: "This is free software; see the source for copying conditions.\\nThere is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, to the extent permitted by law." + +help_addendum: "Email bug reports to: bug-dc@gnu.org" +--- diff --git a/generator/demo/main.cpp b/generator/demo/main.cpp new file mode 100644 index 0000000..85ffb2b --- /dev/null +++ b/generator/demo/main.cpp @@ -0,0 +1,19 @@ +{{=@@ @@=}} +#include "@@argspec@@ArgGrammarDriver.hpp" +#include + +using Result = @@argspec@@ArgGrammar::Driver::Result; + +int main(int argc, char* argv[]) { + @@argspec@@ArgGrammar::Driver driver{ argc, argv }; + auto res = driver.parse(); + + if (res != Result::success) { + if (res == Result::completedAction) + return 0; + + return -1; + } + + return 0; +} diff --git a/generator/src/argument.cpp b/generator/src/argument.cpp new file mode 100644 index 0000000..c757b40 --- /dev/null +++ b/generator/src/argument.cpp @@ -0,0 +1,100 @@ +#include "argument.hpp" +#include + +Argument::Argument(std::string argument, std::optional shortArgument, + std::string usage) + : argument{argument}, usage{usage}, shortArgument{shortArgument} {} + +Argument::~Argument() {} + +mstch::map Argument::render() const { + mstch::map result{ + {std::string{"argument"}, argument}, + {"usage", usage}, + {"short_argument", shortArgument + ? mstch::node{std::string{*shortArgument}} + : mstch::node{false}}, + {"clean_token", cleanToken()}, + {"has_parameters", hasParameters()}, + }; + + return result; +} + +std::string Argument::cleanToken(const std::string &token) { + std::string result(token.size(), static_cast(NULL)); + std::transform(token.begin(), token.end(), result.begin(), + [](char character) { + if (isalpha(character) || isdigit(character)) + return character; + else + return '_'; + }); + + return result; +} + +std::string Argument::cleanToken() const { return cleanToken(argument); } + +#include + +bool Argument::operator==(const Argument &other) const { + return this->argument == other.argument; +} + +bool Argument::operator<(const Argument &other) const { + return this->argument < other.argument; +} + +bool Argument::operator>(const Argument &other) const { + return this->argument > other.argument; +} + +size_t Argument::argStrLength() const { return argument.size(); } + +FlagArgument::FlagArgument(std::string argument, + std::optional shortArgument, std::string usage) + : Argument{argument, shortArgument, usage} {} + +mstch::map FlagArgument::render() const { + auto result = Argument::render(); + result.insert({"parameters", mstch::array{}}); + return result; +} + +bool FlagArgument::hasParameters() const { return false; } + +size_t FlagArgument::paramStrLength() const { return 0; } + +ParameterArgument::ParameterArgument(std::string argument, + std::optional shortArgument, + std::vector parameters, + std::string usage) + : Argument{argument, shortArgument, usage}, parameters{parameters} {} + +mstch::map ParameterArgument::render() const { + auto result = Argument::render(); + mstch::array resultParameters{}; + + for (int i = 0; i < parameters.size(); i++) { + mstch::map resultParameter{{"index", i + 1}, {"name", parameters[i]}}; + + if (i + 1 < parameters.size()) + resultParameter.insert({"next", i + 2}); + + resultParameters.push_back(resultParameter); + } + + result.insert({"parameters", resultParameters}); + return result; +} + +bool ParameterArgument::hasParameters() const { return true; } + +size_t ParameterArgument::paramStrLength() const { + size_t totalSize = std::accumulate( + parameters.begin(), parameters.end(), 0, + [](size_t count, const std::string &str) { return count + str.size(); }); + + return totalSize + (parameters.size() - 1); +} diff --git a/generator/src/argument.hpp b/generator/src/argument.hpp new file mode 100644 index 0000000..968b5b9 --- /dev/null +++ b/generator/src/argument.hpp @@ -0,0 +1,52 @@ +#pragma once +#include +#include +#include + +class Argument { +public: + Argument(std::string argument, std::optional shortArgument, + std::string usage); + virtual ~Argument() = 0; + + virtual mstch::map render() const = 0; + static std::string cleanToken(const std::string &longToken); + std::string cleanToken() const; + virtual bool hasParameters() const = 0; + virtual size_t argStrLength() const; + virtual size_t paramStrLength() const = 0; + + bool operator==(const Argument &other) const; + bool operator<(const Argument &other) const; + bool operator>(const Argument &other) const; + +public: + std::string argument, usage; + std::optional shortArgument; +}; + +class FlagArgument : public Argument { +public: + FlagArgument(std::string argument, std::optional shortArgument, + std::string usage); + ~FlagArgument() = default; + + mstch::map render() const override; + bool hasParameters() const override; + size_t paramStrLength() const override; + + bool operator<(const FlagArgument &other) const; +}; + +class ParameterArgument : public Argument { +public: + ParameterArgument(std::string argument, std::optional shortArgument, + std::vector parameters, std::string usage); + + mstch::map render() const override; + bool hasParameters() const override; + size_t paramStrLength() const override; + +public: + std::vector parameters; +}; diff --git a/generator/src/driver.cpp b/generator/src/driver.cpp new file mode 100644 index 0000000..e5f8ef7 --- /dev/null +++ b/generator/src/driver.cpp @@ -0,0 +1,221 @@ +#include +#include +#include + +#include "driver.hpp" + +Grammar::Driver::Driver() + : arguments{ArgumentComparator{}}, maxArgLength{0}, maxParamLength{0} { + addArg(std::make_unique("help", std::nullopt, + "shows this help message")); + addArg(std::make_unique("version", std::nullopt, + "shows the version of this software")); + addArg(std::make_unique("license", std::nullopt, + "shows the license of this software")); +} + +Grammar::Driver::~Driver() { + delete (scanner); + delete (parser); +} + +void Grammar::Driver::parse(const char *const filename) { + assert(filename != nullptr); + std::ifstream iss{filename}; + + if (!iss) + exit(EXIT_FAILURE); + + parse_helper(iss); +} + +void Grammar::Driver::parse(std::istream &iss) { + if (!iss.good() && iss.eof()) + return; + + parse_helper(iss); +} + +void Grammar::Driver::parse_helper(std::istream &iss) { + delete scanner; + try { + scanner = new Grammar::Scanner(&iss); + } catch (const std::bad_alloc &ba) { + std::cerr << "Failed to allocate scanner: \"" << ba.what() + << "\". Exiting!\n"; + exit(EXIT_FAILURE); + } + + delete parser; + try { + parser = new Grammar::Parser(*scanner, *this); + } catch (const std::bad_alloc &ba) { + std::cerr << "Failed to allocate parser: \"" << ba.what() + << "\". Exiting!\n"; + exit(EXIT_FAILURE); + } + + if (parser->parse() != 0) { + std::cerr << "Parsing failure!\n"; + } + return; +} + +void Grammar::Driver::setProgramName(std::string programName) { + this->programName = programName; +} + +void Grammar::Driver::setVersion(std::string version) { + this->version = version; +} + +void Grammar::Driver::setLicense(std::string license) { + this->license = license; +} + +void Grammar::Driver::setHelpAddendum(std::string addendum) { + this->helpAddendum = addendum; +} + +void Grammar::Driver::addArg(std::unique_ptr argument) { + maxArgLength = std::max(maxArgLength, argument->argStrLength()); + maxParamLength = std::max(maxParamLength, argument->paramStrLength()); + const auto prevVal = arguments.find(argument); + + if (prevVal != arguments.end()) { + arguments.erase(prevVal); + } + + arguments.insert(std::move(argument)); +} + +void Grammar::Driver::addUsage(Usage usage) { usages.push_back(usage); } + +void Grammar::Driver::addRule(std::string ruleName, + std::vector options) { + rules.push_back({ruleName, options}); +} + +mstch::map Grammar::Driver::getContext() const { + return mstch::map{{"argspec", getSafeName()}, + {"any_parameters", usesAnyParameters()}, + {"argument_tokens", generateArgumentTokens()}, + {"argument_explanations", generateArgumentExplanation()}, + {"usage", generateUsageList()}, + {"usage_rules", generateUsageRuleList()}, + {"version", version}, + {"license", license}, + {"help_addendum", getHelpAddendum()}}; +} + +std::string Grammar::Driver::getSafeName() const { + std::string safeName{programName}; + safeName.erase(std::remove_if(safeName.begin(), safeName.end(), + [](char letter) { return !isalpha(letter); }), + safeName.end()); + + return safeName; +} + +bool Grammar::Driver::usesAnyParameters() const { + for (auto &argument : arguments) { + if (argument->hasParameters()) + return true; + } + + return false; +} + +mstch::array Grammar::Driver::generateArgumentTokens() const { + mstch::array tokens{}; + + for (auto &argument : arguments) { + tokens.push_back(alignArg(*argument)); + } + + return tokens; +} + +mstch::array Grammar::Driver::generateArgumentExplanation() const { + mstch::array argumentExplanations{}; + + for (const auto &rule : rules) + argumentExplanations.push_back(explainRule(rule.first, rule.second)); + + return argumentExplanations; +} + +mstch::array Grammar::Driver::generateUsageList() const { + mstch::array usageList{}; + + for (const auto &usage : usages) { + const mstch::array flags{usage.flags.begin(), usage.flags.end()}; + const mstch::array positional{usage.positional.begin(), + usage.positional.end()}; + + usageList.push_back( + mstch::map{{"flags", flags}, {"positional", positional}}); + } + + return usageList; +} + +mstch::array Grammar::Driver::generateUsageRuleList() const { + mstch::array usageRuleList{}; + + for (const auto &rulePair : rules) { + mstch::map usageRule{{"rule_name", Argument::cleanToken(rulePair.first)}}; + + mstch::array ruleOptions{}; + + for (size_t i = 0; i < rulePair.second.size(); i++) { + mstch::map ruleOption{ + {"option", Argument::cleanToken(rulePair.second[i])}}; + + if (i + 1 < rulePair.second.size()) + ruleOption.insert({"has_next", true}); + + ruleOptions.push_back(ruleOption); + } + + usageRule.insert({"options", ruleOptions}); + usageRuleList.push_back(usageRule); + } + + return usageRuleList; +} + +mstch::node Grammar::Driver::getHelpAddendum() const { + return helpAddendum ? mstch::node{*helpAddendum} : mstch::node{false}; +} + +std::string +Grammar::Driver::explainRule(const std::string &ruleName, + const std::vector &ruleOptions) { + return ruleName + " can be " + + std::accumulate(std::next(ruleOptions.begin()), ruleOptions.end(), + "--" + ruleOptions[0], + [](const std::string &left, const std::string &right) { + return left + " or --" + right; + }); +} + +std::string Grammar::Driver::spaceN(size_t spaceCount) { + std::string spaces(spaceCount, ' '); + return spaces; +} + +mstch::map Grammar::Driver::alignArg(Argument &arg) const { + auto result = arg.render(); + result.insert( + {"parameter_align_spacing", spaceN(maxArgLength - arg.argStrLength())}); + result.insert( + {"explain_align_spacing", spaceN(maxParamLength - arg.paramStrLength())}); + return result; +} + +bool Grammar::Driver::ArgumentComparator:: +operator()(const std::unique_ptr &left, + const std::unique_ptr &right) const { + return *left < *right; +} diff --git a/generator/src/driver.hpp b/generator/src/driver.hpp new file mode 100644 index 0000000..a26f3ec --- /dev/null +++ b/generator/src/driver.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "argument.hpp" +#include "parser.tab.hh" +#include "scanner.hpp" +#include "usage.hpp" +#include +#include +#include +#include + +namespace Grammar { +class Driver { +public: + Driver(); + virtual ~Driver(); + + void parse(const char *const filename); + void parse(std::istream &iss); + + /* + * functions to store data retrieved by grammar / parser + */ + + void setProgramName(std::string programName); + void setVersion(std::string version); + void setLicense(std::string license); + void setHelpAddendum(std::string addendum); + void addUsage(Usage usage); + void addRule(std::string ruleName, std::vector options); + void addArg(std::unique_ptr argument); + + mstch::map getContext() const; + std::string getSafeName() const; + +private: + void parse_helper(std::istream &iss); + + Grammar::Parser *parser = nullptr; + Grammar::Scanner *scanner = nullptr; + + bool usesAnyParameters() const; + mstch::array generateArgumentTokens() const; + mstch::map generateHelpToken() const; + mstch::map generateVersionToken() const; + mstch::map generateLicenseToken() const; + mstch::array generateArgumentExplanation() const; + mstch::array generateUsageList() const; + mstch::array generateUsageRuleList() const; + mstch::node getHelpAddendum() const; + + static std::string explainRule(const std::string &ruleName, + const std::vector &ruleOptions); + static std::string spaceN(size_t spaceCount); + mstch::map alignArg(Argument &arg) const; + + struct ArgumentComparator { + bool operator()(const std::unique_ptr &left, + const std::unique_ptr &right) const; + }; + + /* + * specialised data store + */ + + friend Argument; + std::string programName, version, license; + std::set, ArgumentComparator> arguments; + std::optional helpAddendum; + std::vector usages; + std::vector>> rules; + size_t maxArgLength, maxParamLength; +}; +} // namespace Grammar diff --git a/generator/src/lexer.ll b/generator/src/lexer.ll new file mode 100644 index 0000000..e95c638 --- /dev/null +++ b/generator/src/lexer.ll @@ -0,0 +1,295 @@ +/* Simply lexes arguments */ +%{ +#include +#include +#include +#include +#include "parser.tab.hh" +#include "scanner.hpp" +#include "argument.hpp" + +#undef YY_DECL +#define YY_DECL int Grammar::Scanner::yylex(Grammar::Parser::semantic_type* const lval, Grammar::Parser::location_type* lloc) +#define YY_USER_ACTION lloc->step(); lloc->columns(yyleng); + +using token = Grammar::Parser::token; + +#define LOG std::clog +%} + +%option debug +%option nodefault +%option yyclass="Grammar::Scanner" +%option noyywrap +%option c++ + +%x PROGRAM_SECTION PROGRAM_NAME PROGRAM_VALUE PROGRAM_QUOTED_VALUE PROGRAM_QUOTED_VALUE_END USAGE_SECTION USAGE_DETAILS_BEGIN USAGE_DETAILS USAGE_RULE_BEGIN USAGE_RULE ARGUMENTS_SECTION ARGUMENTS_LONGOPT ARGUMENTS_SHORTOPT ARGUMENTS_PARAMETERS ARGUMENTS_DESCRIPTION_BEGIN ARGUMENTS_DESCRIPTION ARGUMENTS_DESCRIPTION_END + +NAME [^[:space:]]+ +SL_SPACE [[:space:]]{-}[\n] +LONGOPT [[:alpha:]][[:alnum:]\-]* +SHORTOPT [^[:space:]] +PARAM [[:alpha:]][[:alnum:]]* +RULE_TOKEN [[:alpha:]][[:alpha:]_-]* + +%% + +%{ /** Code executed at the beginning of yylex **/ + yyval = lval; + static std::string quotedVal{}; + static std::string programName{}; + static std::vector arguments{}; +%} + +"Program:" { + BEGIN(PROGRAM_SECTION); +} + +"Usage:" { + BEGIN(USAGE_SECTION); +} + +"Arguments:" { + BEGIN(ARGUMENTS_SECTION); +} + +{SL_SPACE}+ { + /* ignored */ +} + +\n { + lloc->lines(); +} + +program { + BEGIN(PROGRAM_NAME); + return token::PROGRAM; +} + +{SL_SPACE}+ { + /* ignored */ +} + +{NAME} { + programName = yytext; + yyval->build(programName); + return token::VALUE; +} + +\n { + BEGIN(PROGRAM_SECTION); + lloc->lines(); +} + +version { + BEGIN(PROGRAM_VALUE); + return token::VERSION; +} + +{SL_SPACE}+ { + /* ignored */ +} + +\n { + BEGIN(PROGRAM_SECTION); + throw std::runtime_error{ "Error: no value set on line " + std::to_string(lloc->begin.line) }; +} + +\" { + BEGIN(PROGRAM_QUOTED_VALUE); + quotedVal = ""; +} + +[^\"\\]+ { + quotedVal += yytext; +} + +\\. { + quotedVal += yytext; +} + +\" { + BEGIN(PROGRAM_QUOTED_VALUE_END); + yyval->build(quotedVal); + quotedVal = ""; + return token::VALUE; +} + +[^[:space:]]+$ { + /* received more text at end of line, after quoted value */ + throw std::runtime_error{ "Unknown text after value: \'" + std::string{ yytext } + "\'" }; +} + +\n { + BEGIN(PROGRAM_SECTION); + lloc->lines(); +} + +license { + BEGIN(PROGRAM_VALUE); + return token::LICENSE; +} + +help { + BEGIN(PROGRAM_VALUE); + return token::HELP; +} + +{SL_SPACE}+ { + /* ignored */ +} + +\n { + /* ignored */ + lloc->lines(); +} + +{NAME} { + if (yytext != programName) { + BEGIN(USAGE_RULE_BEGIN); + yyless(0); + } else { + BEGIN(USAGE_DETAILS_BEGIN); + } +} + +{SL_SPACE}+ { + /* ignore */ + BEGIN(USAGE_DETAILS); +} + +\n { + BEGIN(USAGE_SECTION); + yyval->build(); + lloc->lines(); + + return token::USAGE; +} + +[^[:space:]].*$ { + BEGIN(USAGE_SECTION); + const std::string_view usage{ yytext }; + auto trimEndIter = std::find_if_not(usage.rend(), + usage.rbegin(), + [](char charac){ return std::isspace(charac); }); + const std::string trimUsage{ usage.begin(), trimEndIter.base() }; + + yyval->build(trimUsage); + + return token::USAGE; +} + +{RULE_TOKEN} { + BEGIN(USAGE_RULE); + yyval->build(yytext); + return token::RULE_NAME; +} + +{NAME} { + throw std::runtime_error{ "Invalid rule name: " + std::string{ yytext } }; +} + +{SL_SPACE} { + /* ignored */ +} + += { + return token::RULE_EQUALS; +} + +--{LONGOPT} { + yyval->build(yytext + 2); + return token::RULE_TOKEN; +} + +{RULE_TOKEN} { + yyval->build(yytext); + return token::RULE_TOKEN; +} + +"|" { + return token::RULE_OR; +} + +\n { + BEGIN(USAGE_SECTION); + lloc->lines(); +} + + +{SL_SPACE}+ { + /* ignored */ +} + +\n { + lloc->lines(); +} + +--{LONGOPT} { + yyval->build(yytext + 2); + return token::LONGOPT; +} + +, { + BEGIN(ARGUMENTS_LONGOPT); +} + +-{SHORTOPT} { + yyval->build(*(yytext + 1)); + arguments = {}; + return token::SHORTOPT; +} + +, { + BEGIN(ARGUMENTS_SHORTOPT); +} + +{SL_SPACE}+ { + /* ignored */ +} + +{PARAM} { + BEGIN(ARGUMENTS_PARAMETERS); + arguments.push_back({ yytext }); +} + +, { + BEGIN(ARGUMENTS_DESCRIPTION_BEGIN); + if (!arguments.empty()) { + yyval->build>(arguments); + arguments = {}; + return token::PARAMETERS; + } +} + +\" { + BEGIN(ARGUMENTS_DESCRIPTION); + quotedVal = ""; +} + +[^\\\"]+ { + quotedVal += yytext; +} + +\\. { + quotedVal += yytext; +} + +\" { + BEGIN(ARGUMENTS_DESCRIPTION_END); + yyval->build(quotedVal); + quotedVal = ""; + return token::DESCRIPTION; +} + +\n { + BEGIN(ARGUMENTS_SECTION); + lloc->lines(); +} + +<*>. { + std::cerr << "Scanning error in state: " << YY_START << std::endl; + throw std::runtime_error{ "Invalid input: \'" + std::string{ yytext } + "\' on line " + std::to_string(lloc->begin.line) }; +} + +%% diff --git a/generator/src/main.cpp b/generator/src/main.cpp new file mode 100644 index 0000000..d7bb929 --- /dev/null +++ b/generator/src/main.cpp @@ -0,0 +1,42 @@ +#include "driver.hpp" +#include "templates.hpp" +#include +#include +#include + +std::string symbolNameToOutputFile(const std::string &symbolName); + +int main(int argc, char *argv[]) { + if (argc != 2) { + std::cerr << "Usage: " << argv[0] << " \n"; + return 1; + } + + Grammar::Driver driver{}; + std::ifstream specFile{argv[1]}; + driver.parse(specFile); + const auto context = driver.getContext(); + + for (const auto &templateFile : templateFiles) { + std::string strTemplate{templateFile.contents}; + std::ofstream outputFile{driver.getSafeName() + + symbolNameToOutputFile(templateFile.symbolName)}; + + outputFile << mstch::render(strTemplate, context); + outputFile.close(); + } + + return 0; +} + +std::string symbolNameToOutputFile(const std::string &symbolName) { + std::string outputFileName(symbolName.size(), static_cast(NULL)); + std::transform(symbolName.begin(), symbolName.end(), outputFileName.begin(), + [](char letter) { + if (letter == '_') + return '.'; + return letter; + }); + + return outputFileName; +} diff --git a/generator/src/parser.yy b/generator/src/parser.yy new file mode 100644 index 0000000..fecee34 --- /dev/null +++ b/generator/src/parser.yy @@ -0,0 +1,170 @@ +/* Parses an argument specification */ +%skeleton "lalr1.cc" +%require "3.2" +%debug +%defines +%define api.namespace {Grammar} +%define api.parser.class {Parser} + +%code requires{ +# include + + namespace Grammar { + class Driver; + class Scanner; + } + +# ifndef YY_NULLPTR +# if defined __cplusplus && 201103L <= __cplusplus +# define YY_NULLPTR nullptr +# else +# define YY_NULLPTR 0 +# endif +# endif +} + +%parse-param { Scanner& scanner } +%parse-param { Driver& driver } + +%code{ +# include "driver.hpp" +# include +# include +# include +# include + + +# undef yylex +# define yylex scanner.yylex +} + +%define api.value.type variant +%define parse.assert +%locations +%define api.location.file none + +%token PROGRAM +%token VERSION +%token LICENSE +%token HELP +%token VALUE +%token USAGE +%token LONGOPT +%token SHORTOPT +%token > PARAMETERS +%token DESCRIPTION +%token RULE_NAME +%token RULE_EQUALS +%token RULE_OR +%token RULE_TOKEN + +%type > OPTIONAL_SHORTOPT +%type >> OPTIONAL_PARAMETERS +%type > RULE_OPTIONS + +%start ARGSPEC + +%% + +ARGSPEC + : PROGRAM_DETAILS + USAGE_DETAILS + ARGUMENTS_DETAILS + ; + +PROGRAM_DETAILS + : PROGRAM VALUE + VERSION VALUE + LICENSE VALUE + OPTIONAL_HELP { + driver.setProgramName($2); + driver.setVersion($4); + driver.setLicense($6); + } + ; + +OPTIONAL_HELP + : %empty + | HELP VALUE { + driver.setHelpAddendum($2); + } + ; + +USAGE_DETAILS + : USAGE_DETAIL + | USAGE_DETAILS USAGE_DETAIL + ; + +USAGE_DETAIL + : USAGE { + if ($1.empty()) + driver.addUsage(Usage{ {}, {} }); + else + driver.addUsage(Usage{ { $1 }, {} }); + } + | RULE_NAME RULE_EQUALS RULE_OPTIONS { + driver.addRule($1, $3); + } + ; + +RULE_OPTIONS + : RULE_TOKEN { + $$ = std::vector{ $1 }; + } + | RULE_OPTIONS RULE_OR RULE_TOKEN { + $$ = $1; + $$.push_back($3); + } + ; + +ARGUMENTS_DETAILS + : %empty + | ARGUMENTS_DETAILS ARGUMENT_DETAILS + ; + +ARGUMENT_DETAILS + : LONGOPT + OPTIONAL_SHORTOPT OPTIONAL_PARAMETERS + DESCRIPTION { + if ($1 == "help" && $3) { + std::cerr << "--help cannot take parameters\n"; + throw std::runtime_error{ "Invalid rule for --help" }; + } else if ($1 == "version" && $3) { + std::cerr << "--version cannot take parameters\n"; + throw std::runtime_error{ "Invalid rule for --version" }; + } else if ($1 == "license" && $3) { + std::cerr << "--license cannot take parameters\n"; + throw std::runtime_error{ "Invalid rule for --license" }; + } + + if ($3) /* if parameters exist */ + driver.addArg(std::make_unique($1, $2, *$3, $4)); + else + driver.addArg(std::make_unique($1, $2, $4)); + } + ; + +OPTIONAL_SHORTOPT + : %empty { + $$ = std::nullopt; + } + | SHORTOPT { + $$ = $1; + } + ; + +OPTIONAL_PARAMETERS + : %empty { + $$ = std::nullopt; + } + | PARAMETERS { + $$ = $1; + } + ; + +%% + +void Grammar::Parser::error(const location_type& loc, const std::string& err_message) +{ + std::cerr << "Error: \'" << err_message << "\' at " << loc << '\n'; +} diff --git a/generator/src/scanner.hpp b/generator/src/scanner.hpp new file mode 100644 index 0000000..c25f08b --- /dev/null +++ b/generator/src/scanner.hpp @@ -0,0 +1,25 @@ +#pragma once + +#if !defined(yyFlexLexerOnce) +#include +#endif + +#include "parser.tab.hh" + +namespace Grammar { +class Scanner : public yyFlexLexer { +public: + Scanner(std::istream *in) : yyFlexLexer{in} {}; + + virtual ~Scanner(){}; + + using FlexLexer::yylex; + + virtual int yylex(Grammar::Parser::semantic_type *const lval, + Grammar::Parser::location_type *lloc); + +private: + Grammar::Parser::semantic_type *yyval = nullptr; + Grammar::Parser::location_type *loc = nullptr; +}; +} // namespace Grammar diff --git a/generator/src/templates.hpp b/generator/src/templates.hpp new file mode 100644 index 0000000..4366920 --- /dev/null +++ b/generator/src/templates.hpp @@ -0,0 +1,32 @@ +#pragma once +#include +#include + +#define PREFIX(filename) _binary_templates_ArgGrammar##filename +#define DECLARE_TEMPLATE_FILE(filename) \ + extern const char PREFIX(filename##_start); \ + extern const char PREFIX(filename##_end); + +DECLARE_TEMPLATE_FILE(Driver_cpp) +DECLARE_TEMPLATE_FILE(Driver_hpp) +DECLARE_TEMPLATE_FILE(Scanner_ll) +DECLARE_TEMPLATE_FILE(Parser_yy) +DECLARE_TEMPLATE_FILE(Scanner_cpp) +DECLARE_TEMPLATE_FILE(Scanner_hpp) + +struct TemplateFile { + std::string symbolName; + std::string_view contents; +}; + +#define STRING_TEMPLATE(filename) \ + std::string_view(&PREFIX(filename##_start), \ + &PREFIX(filename##_end) - &PREFIX(filename##_start)) + +#define FILE_TEMPLATE(filename) \ + TemplateFile { "ArgGrammar" #filename, STRING_TEMPLATE(filename) } + +const TemplateFile templateFiles[] = { + FILE_TEMPLATE(Driver_cpp), FILE_TEMPLATE(Driver_hpp), + FILE_TEMPLATE(Scanner_ll), FILE_TEMPLATE(Parser_yy), + FILE_TEMPLATE(Scanner_cpp), FILE_TEMPLATE(Scanner_hpp)}; diff --git a/generator/src/usage.hpp b/generator/src/usage.hpp new file mode 100644 index 0000000..b1e3e59 --- /dev/null +++ b/generator/src/usage.hpp @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +struct Usage { + std::vector flags, positional; +}; diff --git a/generator/templates/ArgGrammarDriver.cpp b/generator/templates/ArgGrammarDriver.cpp new file mode 100644 index 0000000..2062e52 --- /dev/null +++ b/generator/templates/ArgGrammarDriver.cpp @@ -0,0 +1,80 @@ +{{=@@ @@=}} +#include +#include + +#include "@@{argspec}@@ArgGrammarDriver.hpp" +#include "@@{argspec}@@ArgGrammarScanner.hpp" + +namespace @@{argspec}@@ArgGrammar { + Driver::Driver(int argc, char* argv[]) : argc{ argc }, argv{ argv }, scanner{}, parser{} { + assert(argc > 0 && "Arguments must include program invocation"); + } + + Driver::~Driver() { + delete scanner; + delete parser; + } + + Driver::Result Driver::parse() { + if (scanner) + delete scanner; + + try { + scanner = new Scanner(argc, argv, *this); + } catch (const std::bad_alloc& ba) { + std::cerr << "Failed to allocate scanner: \"" << ba.what() << "\". Exiting!\n"; + exit(EXIT_FAILURE); + } + + if (parser) + delete parser; + try { + parser = new Parser(*scanner, *this); + } catch (const std::bad_alloc& ba) { + std::cerr << "Failed to allocate parser: \"" << ba.what() << "\". Exiting!\n"; + exit(EXIT_FAILURE); + } + + if (parser->parse() != 0) { + std::cerr << "Parsing failure!\n"; + } + + return result; + } + + void Driver::addArg(const FlagArg flag) { + flagArguments.insert(flag); + }@@#any_parameters@@ + + void Driver::addArg(const ParamArg flag, std::vector parameters) { + paramArguments.insert_or_assign(flag, parameters); + }@@/any_parameters@@ + + bool Driver::getArg(const FlagArg flag) const { + return (flagArguments.find(flag) != flagArguments.end()); + }@@#any_parameters@@ + + std::optional> Driver::getArg(const ParamArg flag) const { + try { + return std::make_optional(paramArguments.at(flag)); + } catch (const std::out_of_range& ignored) { + return std::nullopt; + } + }@@/any_parameters@@ + + void Driver::setResult(Driver::Result result) { + this->result = result; + } + + const std::set& Driver::getFlagArgs() const { + return flagArguments; + }@@#any_parameters@@ + + const std::map>& Driver::getParamArgs() const { + return paramArguments; + }@@/any_parameters@@ + + char** Driver::getArgv() { + return argv; + } +} // namespace trueArgGrammar diff --git a/generator/templates/ArgGrammarDriver.hpp b/generator/templates/ArgGrammarDriver.hpp new file mode 100644 index 0000000..b10a4bb --- /dev/null +++ b/generator/templates/ArgGrammarDriver.hpp @@ -0,0 +1,58 @@ +{{=@@ @@=}} +#pragma once + +#include "@@{argspec}@@ArgGrammarParser.tab.hh" +#include +#include @@#any_parameters@@ +#include +#include @@/any_parameters@@ +#include + +namespace @@{argspec}@@ArgGrammar { + enum class FlagArg { @@#argument_tokens@@@@^has_parameters@@@@{clean_token}@@, @@/has_parameters@@@@/argument_tokens@@ };@@#any_parameters@@ + enum class ParamArg { @@#argument_tokens@@@@#has_parameters@@@@{clean_token}@@, @@/has_parameters@@@@/argument_tokens@@ };@@/any_parameters@@ + + class Scanner; + + class Driver { + public: + enum class Result { success, completedAction, wrongArgument }; + + Driver(int argc, char* argv[]); + virtual ~Driver(); + + Result parse(); + + void addArg(const FlagArg flag);@@#any_parameters@@ + void addArg(const ParamArg flag, const std::vector arguments);@@/any_parameters@@ + + bool getArg(const FlagArg flag) const;@@#any_parameters@@ + std::optional> getArg(const ParamArg flag) const;@@/any_parameters@@ + void setResult(Result result); + + const std::set& getFlagArgs() const;@@#any_parameters@@ + const std::map>& getParamArgs() const;@@/any_parameters@@ + + std::ostream& print(std::ostream& oss); + + char** getArgv(); + + private: + int argc; + char** argv; + + Result result = Result::success; + + void parseHelper(std::istream& iss); + + @@{argspec}@@ArgGrammar::Parser* parser = nullptr; + @@{argspec}@@ArgGrammar::Scanner* scanner = nullptr; + + /* + * specialised data store + */ + + std::set flagArguments;@@#any_parameters@@ + std::map> paramArguments;@@/any_parameters@@ + }; +} // namespace trueArgGrammar diff --git a/generator/templates/ArgGrammarParser.yy b/generator/templates/ArgGrammarParser.yy new file mode 100644 index 0000000..501acc6 --- /dev/null +++ b/generator/templates/ArgGrammarParser.yy @@ -0,0 +1,94 @@ +{{=@@ @@=}} +%skeleton "lalr1.cc" +%require "3.2" +%debug +%defines +%define api.namespace {@@{argspec}@@ArgGrammar} +%define api.parser.class {Parser} +%file-prefix "@@{argspec}@@ArgGrammarParser" + +%code requires{ + namespace @@{argspec}@@ArgGrammar { + class Driver; + class Scanner; + } + +#ifndef YY_NULLPTR +# if defined __cplusplus && 201103L <= __cplusplus +# define YY_NULLPTR nullptr +# else +# define YY_NULLPTR 0 +# endif +#endif +} + +%parse-param { Scanner& scanner } +%parse-param { Driver& driver } + +%code{ +#include +#include +#include + +#include "@@{argspec}@@ArgGrammarDriver.hpp" +#include "@@{argspec}@@ArgGrammarScanner.hpp" + +#undef yylex +#define yylex scanner.yylex +} + +%define api.value.type variant +%define parse.assert + +%token POSITIONAL_ARGUMENT@@#argument_tokens@@ +%token ARGUMENT_@@{clean_token}@@@@/argument_tokens@@ + +%start ARGUMENTS + +%% + +ARGUMENTS + : ARGUMENT_help { + std::cout + << "Usage:\n" + << "\n"@@#usage@@ + << driver.getArgv()[0] << "@@#flags@@ @@{.}@@@@/flags@@@@#positional@@ <@@{.}@@>@@/positional@@\n"@@/usage@@ + << driver.getArgv()[0] << " --help\n" + << driver.getArgv()[0] << " --version\n" + << driver.getArgv()[0] << " --license\n" + << "\n" + << "Arguments:\n" + << "\n"@@#argument_explanations@@ + << "@@{.}@@\n" + << "\n"@@/argument_explanations@@@@#argument_tokens@@ + << " @@#short_argument@@-@@{short_argument}@@,@@/short_argument@@@@^short_argument@@ @@/short_argument@@ --@@{argument}@@@@{parameter_align_spacing}@@@@#parameters@@ <@@{name}@@>@@#next_state@@,@@/next_state@@@@/parameters@@@@^parameters@@ @@/parameters@@ @@{explain_align_spacing}@@@@{usage}@@\n"@@/argument_tokens@@ + << "\n" + << "@@{help_addendum}@@"@@/help_addendum@@ + << std::endl; + driver.addArg(@@{argspec}@@ArgGrammar::FlagArg::help); + driver.setResult(Driver::Result::completedAction); + } + | ARGUMENT_version { + std::cout << "@@{version}@@" << std::endl; + driver.addArg(@@{argspec}@@ArgGrammar::FlagArg::version); + driver.setResult(Driver::Result::completedAction); + } + | ARGUMENT_license { + std::cout << "@@{license}@@" << std::endl; + driver.addArg(@@{argspec}@@ArgGrammar::FlagArg::license); + driver.setResult(Driver::Result::completedAction); + } @@#usage@@ + |@@#flags@@ ARGUMENT_@@{.}@@@@/flags@@@@#positional@@ @@{.}@@@@/positional@@@@^flags@@@@^positional@@ %empty@@/positional@@@@/flags@@@@/usage@@ + ;@@#usage_rules@@ + +ARGUMENT_@@{rule_name}@@ + :@@#options@@ ARGUMENT_@@{option}@@@@#has_next@@ + |@@/has_next@@@@/options@@ + ;@@/usage_rules@@ + +%% + +void @@{argspec}@@ArgGrammar::Parser::error(const std::string& err_message) +{ + std::cerr << "Error: " << err_message << '\n'; +} diff --git a/generator/templates/ArgGrammarScanner.cpp b/generator/templates/ArgGrammarScanner.cpp new file mode 100644 index 0000000..adb72a4 --- /dev/null +++ b/generator/templates/ArgGrammarScanner.cpp @@ -0,0 +1,31 @@ +{{=@@ @@=}} +#include "@@{argspec}@@ArgGrammarScanner.hpp" +#include "@@{argspec}@@ArgGrammarDriver.hpp" + +namespace @@{argspec}@@ArgGrammar { + Scanner::Scanner(int argc, char* argv[], Driver& driver) + : yyFlexLexer{}, argc{ argc }, argv{ argv }, argi{ 1 }, streamInput{}, resetVal{}, driver{ + driver + } { + if (argi < argc) + streamInput << argv[argi]; + + switch_streams(&streamInput); + } + + int Scanner::yywrap() { + ++argi; + bool more = (argi < argc); + + if (more) { + streamInput << argv[argi]; + resetOnWrap(); + } + + return more ? 0 : 1; + } + + void Scanner::setResult(Scanner::Result result) { + driver.setResult(result); + } +} // namespace @@{argspec}@@ArgGrammar diff --git a/generator/templates/ArgGrammarScanner.hpp b/generator/templates/ArgGrammarScanner.hpp new file mode 100644 index 0000000..5b9f7f8 --- /dev/null +++ b/generator/templates/ArgGrammarScanner.hpp @@ -0,0 +1,38 @@ +{{=@@ @@=}} +#pragma once + +#if !defined(yyFlexLexerOnce) +# include +#endif + +#include "@@{argspec}@@ArgGrammarDriver.hpp" +#include +#include +#include + +namespace @@{argspec}@@ArgGrammar { + class Scanner : public yyFlexLexer { + public: + using Result = Driver::Result; + + Scanner(int argc, char* argv[], Driver& driver); + virtual ~Scanner() = default; + + using FlexLexer::yylex; + + virtual int yylex(Parser::semantic_type* const lval); + void resetOnWrap(); + void setResult(Result result); + + private: + int argc, argi; + char** argv; + Driver& driver; + std::stringstream streamInput; + std::optional resetVal; + Parser::semantic_type* yyval = nullptr; + + protected: + int yywrap() override; + }; +} // namespace trueArgGrammar diff --git a/generator/templates/ArgGrammarScanner.ll b/generator/templates/ArgGrammarScanner.ll new file mode 100644 index 0000000..436f59c --- /dev/null +++ b/generator/templates/ArgGrammarScanner.ll @@ -0,0 +1,123 @@ +{{=@@ @@=}} +%{ +#include +#include "@@{argspec}@@ArgGrammarParser.tab.hh" + +#include "@@{argspec}@@ArgGrammarScanner.hpp" +#undef YY_DECL +#define YY_DECL int @@{argspec}@@ArgGrammar::Scanner::yylex(@@{argspec}@@ArgGrammar::Parser::semantic_type* const lval) + +using token = @@{argspec}@@ArgGrammar::Parser::token; +%} + +%option debug +%option nodefault +%option yyclass="@@{argspec}@@ArgGrammar::Scanner" +%option noyywrap +%option c++ +%option outfile="@@{argspec}@@ArgGrammarScannerDef.cpp" + +%x SHORT_ARGUMENTS ARGUMENT_VALUE POSITIONAL_ARGUMENTS@@#argument_tokens@@@@#parameters@@ @@{clean_token}@@_PARAMETER_@@{index}@@@@/parameters@@@@/argument_tokens@@ + +ANYTHING .|\n +LONGARG [[:alnum:]][[:alnum:]\-]* + +%% + +%{ /** Code executed at the beginning of yylex **/ + yyval = lval; + static std::vector parameters; +%}@@#argument_tokens@@@@#has_parameters@@ + +<@@#parameters@@@@{clean_token}@@_PARAMETER_@@{index}@@@@#next_state@@,@@/next_state@@@@/parameters@@>-- { /** invalid usage of --@@{argument}@@ flag **/ + BEGIN(POSITIONAL_ARGUMENTS); + std::cerr << "--@@{argument}@@ has not been given sufficient parameters:\n"; + + for (const auto& parameter : parameters) + std::cerr << "\t- \'" << parameter << "\'\n"; + + setResult(Result::wrongArgument); +}@@/has_parameters@@@@/argument_tokens@@ + +<*>-- { /* end of arguments */ + BEGIN(POSITIONAL_ARGUMENTS); +} + + /** Arguments **/ +@@#argument_tokens@@ + +--@@{argument}@@ {@@#has_parameters@@ + BEGIN(@@{clean_token}@@_PARAMETER_1); + parameters = {};@@/has_parameters@@@@^has_parameters@@ + driver.addArg(@@{argspec}@@ArgGrammar::FlagArg::@@{clean_token}@@); + return token::ARGUMENT_@@{clean_token}@@;@@/has_parameters@@ +}@@#short_argument@@ + +@@{short_argument}@@ {@@#has_parameters@@ + BEGIN(@@{clean_token}@@_PARAMETER_1); + resetVal = std::nullopt; + parameters = {};@@/has_parameters@@@@^has_parameters@@ + driver.addArg(@@{argspec}@@ArgGrammar::FlagArg::@@{clean_token}@@); + return token::ARGUMENT_@@{clean_token}@@;@@/has_parameters@@ +}@@/short_argument@@@@#parameters@@ + +<@@{clean_token}@@_PARAMETER_@@{index}@@>{ANYTHING}+ {@@#next_state@@ + BEGIN(@@{clean_token}@@_PARAMETER_@@{.}@@);@@/next_state@@@@^next_state@@ + BEGIN(INITIAL);@@/next_state@@ + parameters.push_back(yytext);@@^next_state@@ + driver.addArg(@@{argspec}@@ArgGrammar::ParamArg::@@{clean_token}@@, parameters); + return token::ARGUMENT_@@{clean_token}@@;@@/next_state@@ +}@@/parameters@@ +@@/argument_tokens@@ + + /** Default (error) arguments */ + +--{LONGARG} { + std::cerr << "Unknown argument: \'" << yytext << "\' found\n"; + setResult(Result::wrongArgument); +} + +--{ANYTHING}+ { /* show common error for invalid option */ + std::cerr << "Invalid argument: \'" << yytext << "\' found\n"; + setResult(Result::wrongArgument); +} + +{ANYTHING} { + std::cerr << "Unknown argument \'-" << yytext << "\' found\n"; + setResult(Result::wrongArgument); +} + + /** Check for short argument **/ + +-/{ANYTHING}+ { + BEGIN(SHORT_ARGUMENTS); + resetVal = INITIAL; +} + + /** Check for possible unknown arguments **/ + +{ANYTHING}+ { + /* std::cerr << "Unknown positional argument \'" << yytext << "\' found\n"; + setResult(Result::wrongArgument); */ + yyval->build(yytext); + return token::POSITIONAL_ARGUMENT; +} + +{ANYTHING}* { /* unknown positional argument */ + BEGIN(POSITIONAL_ARGUMENTS); + /* std::cerr << "Unknown positional argument \'" << yytext << "\' found\n"; + setResult(Result::wrongArgument); */ + yyval->build(yytext); + return token::POSITIONAL_ARGUMENT; +} + +%% + +namespace @@{argspec}@@ArgGrammar { + void Scanner::resetOnWrap() { + if (resetVal) + BEGIN(*resetVal); + + resetVal = INITIAL; + } +}