First usable generator source.

Still issues with usage rules and error reporting, but functional.
This commit is contained in:
2019-09-28 16:01:40 +01:00
parent 54de004ede
commit 60d0446cf0
24 changed files with 1605 additions and 0 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.ccls-cache
.ccls
*.o
.tup

19
examples/dc/dc.argspec Normal file
View File

@@ -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"

18
examples/dc/main.cpp Normal file
View File

@@ -0,0 +1,18 @@
#include "src/dcArgGrammarDriver.hpp"
#include <sstream>
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;
}

2
generator/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
fcap
generated/

20
generator/Tupfile Normal file
View File

@@ -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)"

31
generator/demo/README.md Normal file
View File

@@ -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/<file>
```
To save the result to a file:
```sh
mustache demo.yml ../templates/<file> /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
```

53
generator/demo/demo.yml Normal file
View File

@@ -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"
---

19
generator/demo/main.cpp Normal file
View File

@@ -0,0 +1,19 @@
{{=@@ @@=}}
#include "@@argspec@@ArgGrammarDriver.hpp"
#include <sstream>
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;
}

100
generator/src/argument.cpp Normal file
View File

@@ -0,0 +1,100 @@
#include "argument.hpp"
#include <numeric>
Argument::Argument(std::string argument, std::optional<char> 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<char>(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 <iostream>
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<char> 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<char> shortArgument,
std::vector<std::string> 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);
}

View File

@@ -0,0 +1,52 @@
#pragma once
#include <mstch/mstch.hpp>
#include <optional>
#include <string>
class Argument {
public:
Argument(std::string argument, std::optional<char> 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<char> shortArgument;
};
class FlagArgument : public Argument {
public:
FlagArgument(std::string argument, std::optional<char> 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<char> shortArgument,
std::vector<std::string> parameters, std::string usage);
mstch::map render() const override;
bool hasParameters() const override;
size_t paramStrLength() const override;
public:
std::vector<std::string> parameters;
};

221
generator/src/driver.cpp Normal file
View File

@@ -0,0 +1,221 @@
#include <cassert>
#include <fstream>
#include <numeric>
#include "driver.hpp"
Grammar::Driver::Driver()
: arguments{ArgumentComparator{}}, maxArgLength{0}, maxParamLength{0} {
addArg(std::make_unique<FlagArgument>("help", std::nullopt,
"shows this help message"));
addArg(std::make_unique<FlagArgument>("version", std::nullopt,
"shows the version of this software"));
addArg(std::make_unique<FlagArgument>("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> 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<std::string> 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<std::string> &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<Argument> &left,
const std::unique_ptr<Argument> &right) const {
return *left < *right;
}

74
generator/src/driver.hpp Normal file
View File

@@ -0,0 +1,74 @@
#pragma once
#include "argument.hpp"
#include "parser.tab.hh"
#include "scanner.hpp"
#include "usage.hpp"
#include <iostream>
#include <memory>
#include <set>
#include <string>
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<std::string> options);
void addArg(std::unique_ptr<Argument> 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<std::string> &ruleOptions);
static std::string spaceN(size_t spaceCount);
mstch::map alignArg(Argument &arg) const;
struct ArgumentComparator {
bool operator()(const std::unique_ptr<Argument> &left,
const std::unique_ptr<Argument> &right) const;
};
/*
* specialised data store
*/
friend Argument;
std::string programName, version, license;
std::set<std::unique_ptr<Argument>, ArgumentComparator> arguments;
std::optional<std::string> helpAddendum;
std::vector<Usage> usages;
std::vector<std::pair<std::string, std::vector<std::string>>> rules;
size_t maxArgLength, maxParamLength;
};
} // namespace Grammar

295
generator/src/lexer.ll Normal file
View File

@@ -0,0 +1,295 @@
/* Simply lexes arguments */
%{
#include <stdexcept>
#include <algorithm>
#include <cctype>
#include <vector>
#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<std::string> arguments{};
%}
<INITIAL>"Program:" {
BEGIN(PROGRAM_SECTION);
}
<PROGRAM_SECTION>"Usage:" {
BEGIN(USAGE_SECTION);
}
<USAGE_SECTION>"Arguments:" {
BEGIN(ARGUMENTS_SECTION);
}
<PROGRAM_SECTION>{SL_SPACE}+ {
/* ignored */
}
<PROGRAM_SECTION>\n {
lloc->lines();
}
<PROGRAM_SECTION>program {
BEGIN(PROGRAM_NAME);
return token::PROGRAM;
}
<PROGRAM_NAME>{SL_SPACE}+ {
/* ignored */
}
<PROGRAM_NAME>{NAME} {
programName = yytext;
yyval->build<std::string>(programName);
return token::VALUE;
}
<PROGRAM_NAME>\n {
BEGIN(PROGRAM_SECTION);
lloc->lines();
}
<PROGRAM_SECTION>version {
BEGIN(PROGRAM_VALUE);
return token::VERSION;
}
<PROGRAM_VALUE>{SL_SPACE}+ {
/* ignored */
}
<PROGRAM_VALUE>\n {
BEGIN(PROGRAM_SECTION);
throw std::runtime_error{ "Error: no value set on line " + std::to_string(lloc->begin.line) };
}
<PROGRAM_VALUE>\" {
BEGIN(PROGRAM_QUOTED_VALUE);
quotedVal = "";
}
<PROGRAM_QUOTED_VALUE>[^\"\\]+ {
quotedVal += yytext;
}
<PROGRAM_QUOTED_VALUE>\\. {
quotedVal += yytext;
}
<PROGRAM_QUOTED_VALUE>\" {
BEGIN(PROGRAM_QUOTED_VALUE_END);
yyval->build<std::string>(quotedVal);
quotedVal = "";
return token::VALUE;
}
<PROGRAM_QUOTED_VALUE_END>[^[:space:]]+$ {
/* received more text at end of line, after quoted value */
throw std::runtime_error{ "Unknown text after value: \'" + std::string{ yytext } + "\'" };
}
<PROGRAM_QUOTED_VALUE_END>\n {
BEGIN(PROGRAM_SECTION);
lloc->lines();
}
<PROGRAM_SECTION>license {
BEGIN(PROGRAM_VALUE);
return token::LICENSE;
}
<PROGRAM_SECTION>help {
BEGIN(PROGRAM_VALUE);
return token::HELP;
}
<USAGE_SECTION>{SL_SPACE}+ {
/* ignored */
}
<USAGE_SECTION>\n {
/* ignored */
lloc->lines();
}
<USAGE_SECTION>{NAME} {
if (yytext != programName) {
BEGIN(USAGE_RULE_BEGIN);
yyless(0);
} else {
BEGIN(USAGE_DETAILS_BEGIN);
}
}
<USAGE_DETAILS_BEGIN>{SL_SPACE}+ {
/* ignore */
BEGIN(USAGE_DETAILS);
}
<USAGE_DETAILS_BEGIN>\n {
BEGIN(USAGE_SECTION);
yyval->build<std::string>();
lloc->lines();
return token::USAGE;
}
<USAGE_DETAILS>[^[: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<std::string>(trimUsage);
return token::USAGE;
}
<USAGE_RULE_BEGIN>{RULE_TOKEN} {
BEGIN(USAGE_RULE);
yyval->build<std::string>(yytext);
return token::RULE_NAME;
}
<USAGE_RULE_BEGIN>{NAME} {
throw std::runtime_error{ "Invalid rule name: " + std::string{ yytext } };
}
<USAGE_RULE>{SL_SPACE} {
/* ignored */
}
<USAGE_RULE>= {
return token::RULE_EQUALS;
}
<USAGE_RULE>--{LONGOPT} {
yyval->build<std::string>(yytext + 2);
return token::RULE_TOKEN;
}
<USAGE_RULE>{RULE_TOKEN} {
yyval->build<std::string>(yytext);
return token::RULE_TOKEN;
}
<USAGE_RULE>"|" {
return token::RULE_OR;
}
<USAGE_RULE>\n {
BEGIN(USAGE_SECTION);
lloc->lines();
}
<ARGUMENTS_SECTION>{SL_SPACE}+ {
/* ignored */
}
<ARGUMENTS_SECTION>\n {
lloc->lines();
}
<ARGUMENTS_SECTION>--{LONGOPT} {
yyval->build<std::string>(yytext + 2);
return token::LONGOPT;
}
<ARGUMENTS_SECTION>, {
BEGIN(ARGUMENTS_LONGOPT);
}
<ARGUMENTS_LONGOPT>-{SHORTOPT} {
yyval->build<char>(*(yytext + 1));
arguments = {};
return token::SHORTOPT;
}
<ARGUMENTS_LONGOPT>, {
BEGIN(ARGUMENTS_SHORTOPT);
}
<ARGUMENTS_LONGOPT,ARGUMENTS_SHORTOPT,ARGUMENTS_PARAMETERS,ARGUMENTS_DESCRIPTION_BEGIN>{SL_SPACE}+ {
/* ignored */
}
<ARGUMENTS_SHORTOPT,ARGUMENTS_PARAMETERS>{PARAM} {
BEGIN(ARGUMENTS_PARAMETERS);
arguments.push_back({ yytext });
}
<ARGUMENTS_SHORTOPT,ARGUMENTS_PARAMETERS>, {
BEGIN(ARGUMENTS_DESCRIPTION_BEGIN);
if (!arguments.empty()) {
yyval->build<std::vector<std::string>>(arguments);
arguments = {};
return token::PARAMETERS;
}
}
<ARGUMENTS_DESCRIPTION_BEGIN>\" {
BEGIN(ARGUMENTS_DESCRIPTION);
quotedVal = "";
}
<ARGUMENTS_DESCRIPTION>[^\\\"]+ {
quotedVal += yytext;
}
<ARGUMENTS_DESCRIPTION>\\. {
quotedVal += yytext;
}
<ARGUMENTS_DESCRIPTION>\" {
BEGIN(ARGUMENTS_DESCRIPTION_END);
yyval->build<std::string>(quotedVal);
quotedVal = "";
return token::DESCRIPTION;
}
<ARGUMENTS_DESCRIPTION_END>\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) };
}
%%

42
generator/src/main.cpp Normal file
View File

@@ -0,0 +1,42 @@
#include "driver.hpp"
#include "templates.hpp"
#include <fstream>
#include <iostream>
#include <mstch/mstch.hpp>
std::string symbolNameToOutputFile(const std::string &symbolName);
int main(int argc, char *argv[]) {
if (argc != 2) {
std::cerr << "Usage: " << argv[0] << " <filename-to-parse>\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<char>(NULL));
std::transform(symbolName.begin(), symbolName.end(), outputFileName.begin(),
[](char letter) {
if (letter == '_')
return '.';
return letter;
});
return outputFileName;
}

170
generator/src/parser.yy Normal file
View File

@@ -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 <optional>
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 <string>
# include <vector>
# include <utility>
# include <memory>
# 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 <std::string> VALUE
%token <std::string> USAGE
%token <std::string> LONGOPT
%token <char> SHORTOPT
%token <std::vector<std::string>> PARAMETERS
%token <std::string> DESCRIPTION
%token <std::string> RULE_NAME
%token RULE_EQUALS
%token RULE_OR
%token <std::string> RULE_TOKEN
%type <std::optional<char>> OPTIONAL_SHORTOPT
%type <std::optional<std::vector<std::string>>> OPTIONAL_PARAMETERS
%type <std::vector<std::string>> 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<ParameterArgument>($1, $2, *$3, $4));
else
driver.addArg(std::make_unique<FlagArgument>($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';
}

25
generator/src/scanner.hpp Normal file
View File

@@ -0,0 +1,25 @@
#pragma once
#if !defined(yyFlexLexerOnce)
#include <FlexLexer.h>
#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

View File

@@ -0,0 +1,32 @@
#pragma once
#include <cstdint>
#include <string>
#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)};

7
generator/src/usage.hpp Normal file
View File

@@ -0,0 +1,7 @@
#pragma once
#include <string>
#include <vector>
struct Usage {
std::vector<std::string> flags, positional;
};

View File

@@ -0,0 +1,80 @@
{{=@@ @@=}}
#include <cassert>
#include <fstream>
#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<std::string> 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<std::vector<std::string>> 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<FlagArg>& Driver::getFlagArgs() const {
return flagArguments;
}@@#any_parameters@@
const std::map<ParamArg, std::vector<std::string>>& Driver::getParamArgs() const {
return paramArguments;
}@@/any_parameters@@
char** Driver::getArgv() {
return argv;
}
} // namespace trueArgGrammar

View File

@@ -0,0 +1,58 @@
{{=@@ @@=}}
#pragma once
#include "@@{argspec}@@ArgGrammarParser.tab.hh"
#include <iostream>
#include <set>@@#any_parameters@@
#include <map>
#include <optional>@@/any_parameters@@
#include <string>
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<std::string> arguments);@@/any_parameters@@
bool getArg(const FlagArg flag) const;@@#any_parameters@@
std::optional<std::vector<std::string>> getArg(const ParamArg flag) const;@@/any_parameters@@
void setResult(Result result);
const std::set<FlagArg>& getFlagArgs() const;@@#any_parameters@@
const std::map<ParamArg, std::vector<std::string>>& 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<FlagArg> flagArguments;@@#any_parameters@@
std::map<ParamArg, std::vector<std::string>> paramArguments;@@/any_parameters@@
};
} // namespace trueArgGrammar

View File

@@ -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 <string>
#include <map>
#include <utility>
#include "@@{argspec}@@ArgGrammarDriver.hpp"
#include "@@{argspec}@@ArgGrammarScanner.hpp"
#undef yylex
#define yylex scanner.yylex
}
%define api.value.type variant
%define parse.assert
%token <std::string> 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';
}

View File

@@ -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

View File

@@ -0,0 +1,38 @@
{{=@@ @@=}}
#pragma once
#if !defined(yyFlexLexerOnce)
# include <FlexLexer.h>
#endif
#include "@@{argspec}@@ArgGrammarDriver.hpp"
#include <cstring>
#include <optional>
#include <sstream>
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<int> resetVal;
Parser::semantic_type* yyval = nullptr;
protected:
int yywrap() override;
};
} // namespace trueArgGrammar

View File

@@ -0,0 +1,123 @@
{{=@@ @@=}}
%{
#include <stdexcept>
#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<std::string> 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@@
<INITIAL>--@@{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_ARGUMENTS>@@{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 */
<INITIAL>--{LONGARG} {
std::cerr << "Unknown argument: \'" << yytext << "\' found\n";
setResult(Result::wrongArgument);
}
<INITIAL>--{ANYTHING}+ { /* show common error for invalid option */
std::cerr << "Invalid argument: \'" << yytext << "\' found\n";
setResult(Result::wrongArgument);
}
<SHORT_ARGUMENTS>{ANYTHING} {
std::cerr << "Unknown argument \'-" << yytext << "\' found\n";
setResult(Result::wrongArgument);
}
/** Check for short argument **/
<INITIAL>-/{ANYTHING}+ {
BEGIN(SHORT_ARGUMENTS);
resetVal = INITIAL;
}
/** Check for possible unknown arguments **/
<POSITIONAL_ARGUMENTS>{ANYTHING}+ {
/* std::cerr << "Unknown positional argument \'" << yytext << "\' found\n";
setResult(Result::wrongArgument); */
yyval->build<std::string>(yytext);
return token::POSITIONAL_ARGUMENT;
}
<INITIAL>{ANYTHING}* { /* unknown positional argument */
BEGIN(POSITIONAL_ARGUMENTS);
/* std::cerr << "Unknown positional argument \'" << yytext << "\' found\n";
setResult(Result::wrongArgument); */
yyval->build<std::string>(yytext);
return token::POSITIONAL_ARGUMENT;
}
%%
namespace @@{argspec}@@ArgGrammar {
void Scanner::resetOnWrap() {
if (resetVal)
BEGIN(*resetVal);
resetVal = INITIAL;
}
}