mirror of https://github.com/djcb/mu.git
utils: Implement sexp command parser
Implement a sexp parser, and on top of that a command parser (for mu4e commands). Add tests, too.
This commit is contained in:
parent
b2bdb8b30c
commit
c7680b78b9
|
@ -1,4 +1,4 @@
|
||||||
## Copyright (C) 2019 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
## Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
##
|
##
|
||||||
## This program is free software; you can redistribute it and/or modify
|
## This program is free software; you can redistribute it and/or modify
|
||||||
## it under the terms of the GNU General Public License as published by
|
## it under the terms of the GNU General Public License as published by
|
||||||
|
@ -16,72 +16,92 @@
|
||||||
|
|
||||||
include $(top_srcdir)/gtest.mk
|
include $(top_srcdir)/gtest.mk
|
||||||
|
|
||||||
AM_CFLAGS= \
|
AM_CFLAGS= \
|
||||||
$(WARN_CFLAGS) \
|
$(WARN_CFLAGS) \
|
||||||
$(GLIB_CFLAGS) \
|
$(GLIB_CFLAGS) \
|
||||||
$(ASAN_CFLAGS) \
|
$(ASAN_CFLAGS) \
|
||||||
$(CODE_COVERAGE_CFLAGS) \
|
$(CODE_COVERAGE_CFLAGS) \
|
||||||
-DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \
|
-DMU_TESTMAILDIR=\"${abs_top_srcdir}/lib/testdir\" \
|
||||||
-DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \
|
-DMU_TESTMAILDIR2=\"${abs_top_srcdir}/lib/testdir2\" \
|
||||||
-Wno-format-nonliteral \
|
-Wno-format-nonliteral \
|
||||||
-Wno-switch-enum \
|
-Wno-switch-enum \
|
||||||
-Wno-deprecated-declarations \
|
-Wno-deprecated-declarations \
|
||||||
-Wno-inline
|
-Wno-inline \
|
||||||
|
-I${top_srcdir}/lib
|
||||||
|
|
||||||
AM_CPPFLAGS= \
|
AM_CPPFLAGS= \
|
||||||
$(CODE_COVERAGE_CPPFLAGS)
|
$(CODE_COVERAGE_CPPFLAGS)
|
||||||
|
|
||||||
AM_CXXFLAGS= \
|
AM_CXXFLAGS= \
|
||||||
$(WARN_CXXFLAGS) \
|
$(WARN_CXXFLAGS) \
|
||||||
$(GLIB_CFLAGS) \
|
$(GLIB_CFLAGS) \
|
||||||
$(ASAN_CXXFLAGS) \
|
$(ASAN_CXXFLAGS) \
|
||||||
$(CODE_COVERAGE_CFLAGS)
|
$(CODE_COVERAGE_CFLAGS) \
|
||||||
|
-I${top_srcdir}/lib
|
||||||
|
|
||||||
AM_LDFLAGS= \
|
AM_LDFLAGS= \
|
||||||
$(ASAN_LDFLAGS)
|
$(ASAN_LDFLAGS)
|
||||||
|
|
||||||
noinst_LTLIBRARIES= \
|
noinst_LTLIBRARIES= \
|
||||||
libmu-utils.la
|
libmu-utils.la
|
||||||
|
|
||||||
libmu_utils_la_SOURCES= \
|
libmu_utils_la_SOURCES= \
|
||||||
mu-date.c \
|
mu-date.c \
|
||||||
mu-date.h \
|
mu-date.h \
|
||||||
mu-error.hh \
|
mu-error.hh \
|
||||||
mu-log.c \
|
mu-log.c \
|
||||||
mu-log.h \
|
mu-log.h \
|
||||||
mu-str.c \
|
mu-command-parser.cc \
|
||||||
mu-str.h \
|
mu-command-parser.hh \
|
||||||
mu-util.c \
|
mu-sexp-parser.cc \
|
||||||
mu-util.h \
|
mu-sexp-parser.hh \
|
||||||
mu-utils.cc \
|
mu-str.c \
|
||||||
|
mu-str.h \
|
||||||
|
mu-util.c \
|
||||||
|
mu-util.h \
|
||||||
|
mu-utils.cc \
|
||||||
mu-utils.hh
|
mu-utils.hh
|
||||||
|
|
||||||
libmu_utils_la_LIBADD= \
|
libmu_utils_la_LIBADD= \
|
||||||
$(GLIB_LIBS) \
|
$(GLIB_LIBS) \
|
||||||
$(CODE_COVERAGE_LIBS)
|
$(CODE_COVERAGE_LIBS)
|
||||||
|
|
||||||
noinst_PROGRAMS= \
|
noinst_PROGRAMS= \
|
||||||
$(TEST_PROGS)
|
$(TEST_PROGS)
|
||||||
|
|
||||||
TEST_PROGS+= \
|
TEST_PROGS+= \
|
||||||
test-mu-util
|
test-mu-util
|
||||||
test_mu_util_SOURCES= \
|
test_mu_util_SOURCES= \
|
||||||
test-mu-util.c
|
test-mu-util.c
|
||||||
test_mu_util_LDADD= \
|
test_mu_util_LDADD= \
|
||||||
libmu-utils.la
|
libmu-utils.la
|
||||||
|
|
||||||
TEST_PROGS+= \
|
TEST_PROGS+= \
|
||||||
test-mu-utils
|
test-mu-utils
|
||||||
test_mu_utils_SOURCES= \
|
test_mu_utils_SOURCES= \
|
||||||
test-utils.cc
|
test-utils.cc
|
||||||
test_mu_utils_LDADD= \
|
test_mu_utils_LDADD= \
|
||||||
libmu-utils.la
|
libmu-utils.la
|
||||||
|
|
||||||
TEST_PROGS+= \
|
TEST_PROGS+= \
|
||||||
test-mu-str
|
test-mu-str
|
||||||
test_mu_str_SOURCES= \
|
test_mu_str_SOURCES= \
|
||||||
test-mu-str.c
|
test-mu-str.c
|
||||||
test_mu_str_LDADD= \
|
test_mu_str_LDADD= \
|
||||||
|
libmu-utils.la
|
||||||
|
|
||||||
|
TEST_PROGS+= \
|
||||||
|
test-sexp-parser
|
||||||
|
test_sexp_parser_SOURCES= \
|
||||||
|
test-sexp-parser.cc
|
||||||
|
test_sexp_parser_LDADD= \
|
||||||
|
libmu-utils.la
|
||||||
|
|
||||||
|
TEST_PROGS+= \
|
||||||
|
test-command-parser
|
||||||
|
test_command_parser_SOURCES= \
|
||||||
|
test-command-parser.cc
|
||||||
|
test_command_parser_LDADD= \
|
||||||
libmu-utils.la
|
libmu-utils.la
|
||||||
|
|
||||||
TESTS=$(TEST_PROGS)
|
TESTS=$(TEST_PROGS)
|
||||||
|
|
|
@ -0,0 +1,189 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** 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 General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "mu-command-parser.hh"
|
||||||
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
using namespace Command;
|
||||||
|
using namespace Sexp;
|
||||||
|
|
||||||
|
static Mu::Error
|
||||||
|
command_error(const std::string& msg)
|
||||||
|
{
|
||||||
|
return Mu::Error(Error::Code::Command, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Command::invoke(const Command::CommandMap& cmap, const Node& call)
|
||||||
|
{
|
||||||
|
if (call.type != Type::List || call.children.empty() ||
|
||||||
|
call.children[0].type != Type::Symbol)
|
||||||
|
throw command_error("call must be a list starting with a symbol");
|
||||||
|
|
||||||
|
const auto& params{call.children};
|
||||||
|
const auto cmd_it = cmap.find(params[0].value);
|
||||||
|
if (cmd_it == cmap.end())
|
||||||
|
throw command_error("unknown command '" + params[0].value + "'");
|
||||||
|
|
||||||
|
const auto& cinfo{cmd_it->second};
|
||||||
|
|
||||||
|
// all required parameters must be present
|
||||||
|
for (auto&& arg: cinfo.args) {
|
||||||
|
const auto& argname{arg.first};
|
||||||
|
const auto& arginfo{arg.second};
|
||||||
|
|
||||||
|
// calls used keyword-parameters, e.g.
|
||||||
|
// (my-function :bar 1 :cuux "fnorb")
|
||||||
|
// so, we're looking for the odd-numbered parameters.
|
||||||
|
const auto param_it = [&]() {
|
||||||
|
for (size_t i = 1; i < params.size(); i += 2)
|
||||||
|
if (params[i].type == Type::Symbol &&
|
||||||
|
params[i].value == ':' + argname)
|
||||||
|
return params.begin() + i + 1;
|
||||||
|
|
||||||
|
return params.end();
|
||||||
|
|
||||||
|
}();
|
||||||
|
|
||||||
|
// it's an error when a required parameter is missing.
|
||||||
|
if (param_it == params.end()) {
|
||||||
|
if (arginfo.required)
|
||||||
|
throw command_error("missing required parameter '" + argname + "'");
|
||||||
|
continue; // not required
|
||||||
|
}
|
||||||
|
|
||||||
|
// the types must match, but the 'nil' symbol is acceptable as
|
||||||
|
// "no value"
|
||||||
|
if (param_it->type != arginfo.type &&
|
||||||
|
!(param_it->type == Type::Symbol && param_it->value == "nil"))
|
||||||
|
throw command_error("parameter '" + argname + "' expects type " +
|
||||||
|
to_string(arginfo.type) +
|
||||||
|
" but got " + to_string(param_it->type));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cinfo.handler)
|
||||||
|
cinfo.handler(params);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Parameters::const_iterator
|
||||||
|
find_param_node (const Parameters& params, const std::string& argname)
|
||||||
|
{
|
||||||
|
for (size_t i = 1; i < params.size(); i += 2) {
|
||||||
|
if (i + 1 != params.size() &&
|
||||||
|
params[i].type == Type::Symbol &&
|
||||||
|
params[i].value == ':' + argname)
|
||||||
|
return params.begin() + i + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return params.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr auto Nil = "nil";
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_nil(const Node& node)
|
||||||
|
{
|
||||||
|
return node.type == Type::Symbol && node.value == Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string&
|
||||||
|
Command::get_string_or (const Parameters& params, const std::string& argname,
|
||||||
|
const std::string& alt)
|
||||||
|
{
|
||||||
|
const auto it = find_param_node (params, argname);
|
||||||
|
if (it == params.end() || is_nil(*it))
|
||||||
|
return alt;
|
||||||
|
else if (it->type != Type::String)
|
||||||
|
throw Error(Error::Code::InvalidArgument, "expected <string> but got %s (value: '%s')",
|
||||||
|
to_string(it->type).c_str(),
|
||||||
|
it->value.c_str());
|
||||||
|
|
||||||
|
return it->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string&
|
||||||
|
Command::get_symbol_or (const Parameters& params, const std::string& argname,
|
||||||
|
const std::string& alt)
|
||||||
|
{
|
||||||
|
const auto it = find_param_node (params, argname);
|
||||||
|
if (it == params.end() || is_nil(*it))
|
||||||
|
return alt;
|
||||||
|
else if (it->type != Type::Symbol)
|
||||||
|
throw Error(Error::Code::InvalidArgument, "expected <symbol> but got %s (value: '%s')",
|
||||||
|
to_string(it->type).c_str(),
|
||||||
|
it->value.c_str());
|
||||||
|
|
||||||
|
return it->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Command::get_int_or (const Parameters& params, const std::string& argname,
|
||||||
|
int alt)
|
||||||
|
{
|
||||||
|
const auto it = find_param_node (params, argname);
|
||||||
|
if (it == params.end() || is_nil(*it))
|
||||||
|
return alt;
|
||||||
|
else if (it->type != Type::Integer)
|
||||||
|
throw Error(Error::Code::InvalidArgument, "expected <integer> but got %s",
|
||||||
|
to_string(it->type).c_str());
|
||||||
|
else
|
||||||
|
return ::atoi(it->value.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
Command::get_bool_or (const Parameters& params, const std::string& argname,
|
||||||
|
bool alt)
|
||||||
|
{
|
||||||
|
const auto it = find_param_node (params, argname);
|
||||||
|
if (it == params.end())
|
||||||
|
return alt;
|
||||||
|
else if (it->type != Type::Symbol)
|
||||||
|
throw Error(Error::Code::InvalidArgument, "expected <symbol> but got %s",
|
||||||
|
to_string(it->type).c_str());
|
||||||
|
else
|
||||||
|
return it->value != Nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string>
|
||||||
|
Command::get_string_vec (const Parameters& params, const std::string& argname)
|
||||||
|
{
|
||||||
|
const auto it = find_param_node (params, argname);
|
||||||
|
if (it == params.end() || is_nil(*it))
|
||||||
|
return {};
|
||||||
|
else if (it->type != Type::List)
|
||||||
|
throw Error(Error::Code::InvalidArgument, "expected <list> but got %s",
|
||||||
|
to_string(it->type).c_str());
|
||||||
|
|
||||||
|
std::vector<std::string> vec;
|
||||||
|
for (const auto& n: it->children) {
|
||||||
|
if (n.type != Type::String)
|
||||||
|
throw Error(Error::Code::InvalidArgument,
|
||||||
|
"expected string element but got %s",
|
||||||
|
to_string(n.type).c_str());
|
||||||
|
vec.emplace_back (n.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return vec;
|
||||||
|
}
|
|
@ -0,0 +1,135 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** 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 General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
#ifndef MU_COMMAND_PARSER_HH__
|
||||||
|
#define MU_COMMAND_PARSER_HH__
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include <ostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "utils/mu-error.hh"
|
||||||
|
#include "utils/mu-sexp-parser.hh"
|
||||||
|
|
||||||
|
|
||||||
|
namespace Mu {
|
||||||
|
namespace Command {
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Commands are s-expressions with the follow properties:
|
||||||
|
|
||||||
|
/// 1) a command is a list with a command-name as its first argument
|
||||||
|
/// 2) the rest of the parameters are pairs of colon-prefixed symbol and a value of some type
|
||||||
|
/// (ie. 'keyword arguments')
|
||||||
|
/// 3) each command is described by its CommandInfo structure, which defines the type
|
||||||
|
/// 4) calls to the command must include all required parameters
|
||||||
|
/// 5) all parameters must be of the specified type; however the symbol 'nil' is allowed
|
||||||
|
/// for specify a non-required parameter to be absent; this is for convience on the call side.
|
||||||
|
|
||||||
|
|
||||||
|
/// Information about a function argument
|
||||||
|
struct ArgInfo {
|
||||||
|
ArgInfo (Sexp::Type typearg, bool requiredarg, std::string&& docarg):
|
||||||
|
type{typearg}, required{requiredarg},docstring{std::move(docarg)}
|
||||||
|
{}
|
||||||
|
const Sexp::Type type; /**< Sexp::Type of the argument */
|
||||||
|
const bool required; /**< Is this argument required? */
|
||||||
|
const std::string docstring; /**< Documentation */
|
||||||
|
};
|
||||||
|
|
||||||
|
/// The arguments for a function, which maps their names to the information.
|
||||||
|
using ArgMap = std::unordered_map<std::string, ArgInfo>;
|
||||||
|
// The parameters to a Handler.
|
||||||
|
using Parameters = std::vector<Sexp::Node>;
|
||||||
|
|
||||||
|
int get_int_or (const Parameters& parms, const std::string& argname, int alt=0);
|
||||||
|
bool get_bool_or (const Parameters& parms, const std::string& argname, bool alt=false);
|
||||||
|
const std::string& get_string_or (const Parameters& parms, const std::string& argname, const std::string& alt="");
|
||||||
|
const std::string& get_symbol_or (const Parameters& parms, const std::string& argname, const std::string& alt="nil");
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<std::string> get_string_vec (const Parameters& params, const std::string& argname);
|
||||||
|
|
||||||
|
|
||||||
|
// A handler function
|
||||||
|
using Handler = std::function<void(const Parameters&)>;
|
||||||
|
|
||||||
|
/// Information about some command
|
||||||
|
struct CommandInfo {
|
||||||
|
CommandInfo(ArgMap&& argmaparg, std::string&& docarg, Handler&& handlerarg):
|
||||||
|
args{std::move(argmaparg)}, docstring{std::move(docarg)}, handler{std::move(handlerarg)}
|
||||||
|
{}
|
||||||
|
const ArgMap args;
|
||||||
|
const std::string docstring;
|
||||||
|
const Handler handler;
|
||||||
|
};
|
||||||
|
/// All commands, mapping their name to information about them.
|
||||||
|
using CommandMap = std::unordered_map<std::string, CommandInfo>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that the call (a Sexp::Node) specifies a valid call, then invoke it.
|
||||||
|
*
|
||||||
|
* A call uses keyword arguments, e.g. something like:
|
||||||
|
* (foo :bar 1 :cuux "fnorb")
|
||||||
|
*
|
||||||
|
* On error, throw Error.
|
||||||
|
*
|
||||||
|
* @param cmap map of commands
|
||||||
|
* @param call node describing a call.
|
||||||
|
*/
|
||||||
|
void invoke(const Command::CommandMap& cmap, const Sexp::Node& call);
|
||||||
|
|
||||||
|
|
||||||
|
} // namespace Command
|
||||||
|
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const Command::ArgInfo& info)
|
||||||
|
{
|
||||||
|
os << info.type
|
||||||
|
<< " (" << ( info.required ? "required" : "optional" ) << ")";
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const Command::CommandInfo& info)
|
||||||
|
{
|
||||||
|
for (auto&& arg: info.args)
|
||||||
|
os << " " << arg.first << ": " << arg.second << '\n'
|
||||||
|
<< " " << arg.second.docstring << "\n";
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const Command::CommandMap& map)
|
||||||
|
{
|
||||||
|
for (auto&& c: map)
|
||||||
|
os << c.first << '\n' << c.second;
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace Mu
|
||||||
|
|
||||||
|
|
||||||
|
#endif /* MU_COMMAND_PARSER_HH__ */
|
|
@ -70,6 +70,9 @@ struct Error final: public std::exception {
|
||||||
va_end(args);
|
va_end(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Error(Error&& rhs) = default;
|
||||||
|
Error(const Error& rhs) = delete;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Build an error from a GError an error-code and a format string
|
* Build an error from a GError an error-code and a format string
|
||||||
*
|
*
|
||||||
|
|
|
@ -0,0 +1,170 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020 djcb <djcb@evergrey>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** 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 General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
#include "mu-sexp-parser.hh"
|
||||||
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
using namespace Sexp;
|
||||||
|
|
||||||
|
__attribute__((format(printf, 2, 0))) static Mu::Error
|
||||||
|
parsing_error(size_t pos, const char* frm, ...)
|
||||||
|
{
|
||||||
|
va_list args;
|
||||||
|
va_start(args, frm);
|
||||||
|
auto msg = format(frm, args);
|
||||||
|
va_end(args);
|
||||||
|
|
||||||
|
if (pos == 0)
|
||||||
|
return Mu::Error(Error::Code::Parsing, "%s", msg);
|
||||||
|
else
|
||||||
|
return Mu::Error(Error::Code::Parsing, "%zu: %s", msg.c_str());
|
||||||
|
}
|
||||||
|
static size_t
|
||||||
|
skip_whitespace (const std::string& s, size_t pos)
|
||||||
|
{
|
||||||
|
while (pos != s.size()) {
|
||||||
|
if (s[pos] == ' ' || s[pos] == '\t' || s[pos] == '\n')
|
||||||
|
++pos;
|
||||||
|
else
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node parse (const std::string& expr, size_t& pos);
|
||||||
|
|
||||||
|
static Node
|
||||||
|
parse_list (const std::string& expr, size_t& pos)
|
||||||
|
{
|
||||||
|
if (expr[pos] != '(') // sanity check.
|
||||||
|
throw parsing_error(pos, "expected: '(' but got '%c", expr[pos]);
|
||||||
|
|
||||||
|
std::vector<Node> children;
|
||||||
|
|
||||||
|
++pos;
|
||||||
|
while (expr[pos] != ')' && pos != expr.size())
|
||||||
|
children.emplace_back(parse(expr, pos));
|
||||||
|
|
||||||
|
if (expr[pos] != ')')
|
||||||
|
throw parsing_error(pos, "expected: ')' but got '%c'", expr[pos]);
|
||||||
|
++pos;
|
||||||
|
return Node{std::move(children)};
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse string
|
||||||
|
static Node
|
||||||
|
parse_string (const std::string& expr, size_t& pos)
|
||||||
|
{
|
||||||
|
if (expr[pos] != '"') // sanity check.
|
||||||
|
throw parsing_error(pos, "expected: '\"'' but got '%c", expr[pos]);
|
||||||
|
|
||||||
|
bool escape{};
|
||||||
|
std::string str;
|
||||||
|
for (++pos; pos != expr.size(); ++pos) {
|
||||||
|
|
||||||
|
auto kar = expr[pos];
|
||||||
|
if (escape && (kar == '"' || kar == '\\')) {
|
||||||
|
str += kar;
|
||||||
|
escape = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (kar == '"')
|
||||||
|
break;
|
||||||
|
else if (kar == '\\')
|
||||||
|
escape = true;
|
||||||
|
else
|
||||||
|
str += kar;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (escape || expr[pos] != '"')
|
||||||
|
throw parsing_error(pos, "unterminated string '%s'", str.c_str());
|
||||||
|
|
||||||
|
++pos;
|
||||||
|
return Node{Type::String, std::move(str)};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node
|
||||||
|
parse_integer (const std::string& expr, size_t& pos)
|
||||||
|
{
|
||||||
|
if (!isdigit(expr[pos])) // sanity check.
|
||||||
|
throw parsing_error(pos, "expected: <digit> but got '%c", expr[pos]);
|
||||||
|
|
||||||
|
std::string num;
|
||||||
|
for (; isdigit(expr[pos]); ++pos)
|
||||||
|
num += expr[pos];
|
||||||
|
|
||||||
|
return Node {Type::Integer, std::move(num)};
|
||||||
|
}
|
||||||
|
|
||||||
|
static Node
|
||||||
|
parse_symbol (const std::string& expr, size_t& pos)
|
||||||
|
{
|
||||||
|
if (!isalpha(expr[pos]) && expr[pos] != ':') // sanity check.
|
||||||
|
throw parsing_error(pos, "expected: <alpha>|: but got '%c", expr[pos]);
|
||||||
|
|
||||||
|
std::string symbol(1, expr[pos]);
|
||||||
|
for (++pos; isalnum(expr[pos]) || expr[pos] == '-'; ++pos)
|
||||||
|
symbol += expr[pos];
|
||||||
|
|
||||||
|
return Node { Type::Symbol, std::move(symbol)};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static Node
|
||||||
|
parse (const std::string& expr, size_t& pos)
|
||||||
|
{
|
||||||
|
pos = skip_whitespace(expr, pos);
|
||||||
|
|
||||||
|
if (pos == expr.size())
|
||||||
|
throw parsing_error(pos, "expected: character '%c", expr[pos]);
|
||||||
|
|
||||||
|
const auto kar = expr[pos];
|
||||||
|
const auto node =[&]() -> Node {
|
||||||
|
if (kar == '(')
|
||||||
|
return parse_list (expr, pos);
|
||||||
|
else if (kar == '"')
|
||||||
|
return parse_string(expr, pos);
|
||||||
|
else if (isdigit(kar))
|
||||||
|
return parse_integer(expr, pos);
|
||||||
|
else if (isalpha(kar) || kar == ':')
|
||||||
|
return parse_symbol(expr, pos);
|
||||||
|
else
|
||||||
|
throw parsing_error(pos, "unexpected character '%c" + kar);
|
||||||
|
}();
|
||||||
|
|
||||||
|
pos = skip_whitespace(expr, pos);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
|
||||||
|
Node
|
||||||
|
Sexp::parse (const std::string& expr)
|
||||||
|
{
|
||||||
|
size_t pos{};
|
||||||
|
auto node{::parse (expr, pos)};
|
||||||
|
|
||||||
|
if (pos != expr.size())
|
||||||
|
throw parsing_error(pos, "trailing data starting with '%c'", expr[pos]);
|
||||||
|
|
||||||
|
return node;
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020 djcb <djcb@evergrey>
|
||||||
|
**
|
||||||
|
** This program is free software; you can redistribute it and/or modify it
|
||||||
|
** under the terms of the GNU General Public License as published by the
|
||||||
|
** Free Software Foundation; either version 3, or (at your option) any
|
||||||
|
** later version.
|
||||||
|
**
|
||||||
|
** 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 General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU General Public License
|
||||||
|
** along with this program; if not, write to the Free Software Foundation,
|
||||||
|
** Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
||||||
|
**
|
||||||
|
*/
|
||||||
|
|
||||||
|
#ifndef MU_SEXP_PARSER_HH__
|
||||||
|
#define MU_SEXP_PARSER_HH__
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "utils/mu-error.hh"
|
||||||
|
|
||||||
|
namespace Mu {
|
||||||
|
namespace Sexp {
|
||||||
|
|
||||||
|
/// Simple s-expression parser that parses lists () and atoms (strings
|
||||||
|
/// ("-quoted), (positive) integers ([0..9]+) and symbol starting with alpha or
|
||||||
|
/// ':', then alphanum and '-')
|
||||||
|
///
|
||||||
|
/// (:foo (1234 "bar" nil) :quux (a b c))
|
||||||
|
|
||||||
|
/// Node type
|
||||||
|
enum struct Type { List, String, Integer, Symbol };
|
||||||
|
|
||||||
|
/// Parse node
|
||||||
|
struct Node {
|
||||||
|
/**
|
||||||
|
* Construct a new non-list node
|
||||||
|
*
|
||||||
|
* @param typearg the type of node
|
||||||
|
* @param valuearg the value
|
||||||
|
*/
|
||||||
|
Node(Type typearg, std::string&& valuearg):
|
||||||
|
type{typearg}, value{std::move(valuearg)} {
|
||||||
|
if (typearg == Type::List)
|
||||||
|
throw Error(Error::Code::Parsing,
|
||||||
|
"atomic type cannot be a <list>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a list node
|
||||||
|
|
||||||
|
* @param childrenarg the list children
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
explicit Node(std::vector<Node>&& childrenarg):
|
||||||
|
type{Type::List}, children{std::move(childrenarg)}
|
||||||
|
{}
|
||||||
|
|
||||||
|
const Type type; /**< Type of node */
|
||||||
|
const std::string value; /**< String value of node (only for non-Type::List)*/
|
||||||
|
const std::vector<Node> children; /**< Chiidren of node (only for Type::List) */
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse the string as an s-expressi9on.
|
||||||
|
*
|
||||||
|
* @param expr an s-expression string
|
||||||
|
*
|
||||||
|
* @return the parsed s-expression, or throw Error.
|
||||||
|
*/
|
||||||
|
Node parse(const std::string& expr);
|
||||||
|
|
||||||
|
} // Sexp
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, Sexp::Type id)
|
||||||
|
{
|
||||||
|
switch (id) {
|
||||||
|
case Sexp::Type::List: os << "<list>"; break;
|
||||||
|
case Sexp::Type::String: os << "<string>"; break;
|
||||||
|
case Sexp::Type::Integer: os << "<integer>"; break;
|
||||||
|
case Sexp::Type::Symbol: os << "<symbol>"; break;
|
||||||
|
default: throw std::runtime_error ("unknown node type");
|
||||||
|
}
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline std::ostream&
|
||||||
|
operator<<(std::ostream& os, const Sexp::Node& node)
|
||||||
|
{
|
||||||
|
os << node.type;
|
||||||
|
if (node.type == Sexp::Type::List) {
|
||||||
|
os << '(';
|
||||||
|
for (auto&& elm: node.children)
|
||||||
|
os << elm;
|
||||||
|
os << ')';
|
||||||
|
} else
|
||||||
|
os << '{' << node.value << '}';
|
||||||
|
|
||||||
|
return os;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // Mu
|
||||||
|
|
||||||
|
#endif /* MU_SEXP_PARSER_HH__ */
|
|
@ -0,0 +1,142 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2017 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This library is free software; you can redistribute it and/or
|
||||||
|
** modify it under the terms of the GNU Lesser General Public License
|
||||||
|
** as published by the Free Software Foundation; either version 2.1
|
||||||
|
** of the License, or (at your option) any later version.
|
||||||
|
**
|
||||||
|
** This library 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
|
||||||
|
** Lesser General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU Lesser General Public
|
||||||
|
** License along with this library; if not, write to the Free
|
||||||
|
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
** 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "mu-command-parser.hh"
|
||||||
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_param_getters()
|
||||||
|
{
|
||||||
|
const auto node { Sexp::parse(R"((foo :bar 123 :cuux "456" :boo nil :bah true))")};
|
||||||
|
|
||||||
|
std::cout << node << "\n";
|
||||||
|
|
||||||
|
g_assert_cmpint(Command::get_int_or(node.children,"bar"), ==, 123);
|
||||||
|
assert_equal(Command::get_string_or(node.children, "bra", "bla"), "bla");
|
||||||
|
assert_equal(Command::get_string_or(node.children, "cuux"), "456");
|
||||||
|
|
||||||
|
g_assert_true(Command::get_bool_or(node.children,"boo") == false);
|
||||||
|
g_assert_true(Command::get_bool_or(node.children,"bah") == true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
call (const Command::CommandMap& cmap, const std::string& sexp) try
|
||||||
|
{
|
||||||
|
const auto node{Sexp::parse(sexp)};
|
||||||
|
g_message ("invoking %s", to_string(node).c_str());
|
||||||
|
|
||||||
|
invoke (cmap, node);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const Error& err) {
|
||||||
|
g_warning ("%s", err.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command()
|
||||||
|
{
|
||||||
|
using namespace Command;
|
||||||
|
|
||||||
|
CommandMap cmap;
|
||||||
|
|
||||||
|
cmap.emplace("my-command",
|
||||||
|
CommandInfo{
|
||||||
|
ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }},
|
||||||
|
{"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}},
|
||||||
|
"My command,",
|
||||||
|
{}});
|
||||||
|
|
||||||
|
//std::cout << cmap << "\n";
|
||||||
|
|
||||||
|
g_assert_true(call(cmap, "(my-command :param1 \"hello\")"));
|
||||||
|
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123)"));
|
||||||
|
g_assert_true(call(cmap, "(my-command :param1 \"hello\" :param2 123 :param3 xxx)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command2()
|
||||||
|
{
|
||||||
|
using namespace Command;
|
||||||
|
|
||||||
|
CommandMap cmap;
|
||||||
|
cmap.emplace("bla",
|
||||||
|
CommandInfo{
|
||||||
|
ArgMap{
|
||||||
|
{"foo", ArgInfo{Sexp::Type::Integer, false, "foo"}},
|
||||||
|
{"bar", ArgInfo{Sexp::Type::String, false, "bar"}},
|
||||||
|
},"yeah",
|
||||||
|
[&](const auto& params){}});
|
||||||
|
|
||||||
|
|
||||||
|
g_assert_true (call(cmap, "(bla :foo nil :bla nil)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_command_fail()
|
||||||
|
{
|
||||||
|
using namespace Command;
|
||||||
|
|
||||||
|
allow_warnings();
|
||||||
|
|
||||||
|
CommandMap cmap;
|
||||||
|
|
||||||
|
cmap.emplace("my-command",
|
||||||
|
CommandInfo{
|
||||||
|
ArgMap{ {"param1", ArgInfo{Sexp::Type::String, true, "some string" }},
|
||||||
|
{"param2", ArgInfo{Sexp::Type::Integer, false, "some integer"}}},
|
||||||
|
"My command,",
|
||||||
|
{}});
|
||||||
|
|
||||||
|
g_assert_false (call(cmap, "(my-command)"));
|
||||||
|
g_assert_false (call(cmap, "(my-command2)"));
|
||||||
|
g_assert_false(call(cmap, "(my-command :param1 123 :param2 123)"));
|
||||||
|
g_assert_false(call(cmap, "(my-command :param1 \"hello\" :param2 \"123\")"));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
main (int argc, char *argv[]) try
|
||||||
|
{
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
|
||||||
|
g_test_add_func ("/utils/command-parser/param-getters", test_param_getters);
|
||||||
|
g_test_add_func ("/utils/command-parser/command", test_command);
|
||||||
|
g_test_add_func ("/utils/command-parser/command2", test_command2);
|
||||||
|
g_test_add_func ("/utils/command-parser/command-fail", test_command_fail);
|
||||||
|
|
||||||
|
return g_test_run ();
|
||||||
|
|
||||||
|
|
||||||
|
} catch (const std::runtime_error& re) {
|
||||||
|
std::cerr << re.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
|
@ -0,0 +1,75 @@
|
||||||
|
/*
|
||||||
|
** Copyright (C) 2020 Dirk-Jan C. Binnema <djcb@djcbsoftware.nl>
|
||||||
|
**
|
||||||
|
** This library is free software; you can redistribute it and/or
|
||||||
|
** modify it under the terms of the GNU Lesser General Public License
|
||||||
|
** as published by the Free Software Foundation; either version 2.1
|
||||||
|
** of the License, or (at your option) any later version.
|
||||||
|
**
|
||||||
|
** This library 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
|
||||||
|
** Lesser General Public License for more details.
|
||||||
|
**
|
||||||
|
** You should have received a copy of the GNU Lesser General Public
|
||||||
|
** License along with this library; if not, write to the Free
|
||||||
|
** Software Foundation, 51 Franklin Street, Fifth Floor, Boston, MA
|
||||||
|
** 02110-1301, USA.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <vector>
|
||||||
|
#include <glib.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "mu-command-parser.hh"
|
||||||
|
#include "mu-utils.hh"
|
||||||
|
|
||||||
|
using namespace Mu;
|
||||||
|
|
||||||
|
static bool
|
||||||
|
check_parse (const std::string& expr, const std::string& expected)
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
const auto parsed{to_string(Sexp::parse(expr))};
|
||||||
|
g_assert_cmpstr(parsed.c_str(), ==, expected.c_str());
|
||||||
|
return true;
|
||||||
|
|
||||||
|
} catch (const Error& err) {
|
||||||
|
g_warning ("caught exception parsing '%s': %s", expr.c_str(), err.what());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
test_parser()
|
||||||
|
{
|
||||||
|
check_parse(R"(:foo-123)", "<symbol>{:foo-123}");
|
||||||
|
check_parse(R"("foo")", "<string>{foo}");
|
||||||
|
check_parse(R"(12345)", "<integer>{12345}");
|
||||||
|
check_parse(R"((123 bar "cuux"))", "<list>(<integer>{123}<symbol>{bar}<string>{cuux})");
|
||||||
|
|
||||||
|
check_parse(R"("\"")", "<string>{\"}");
|
||||||
|
check_parse(R"("\\")", "<string>{\\}");
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
main (int argc, char *argv[]) try
|
||||||
|
{
|
||||||
|
g_test_init (&argc, &argv, NULL);
|
||||||
|
|
||||||
|
if (argc == 2) {
|
||||||
|
std::cout << Sexp::parse(argv[1]) << '\n';
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
g_test_add_func ("/utils/command-parser/parse", test_parser);
|
||||||
|
|
||||||
|
return g_test_run ();
|
||||||
|
|
||||||
|
|
||||||
|
} catch (const std::runtime_error& re) {
|
||||||
|
std::cerr << re.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
}
|
Loading…
Reference in New Issue