Since it looks like you are just looking for ways to trigger functions in response to parsed text, I propose this Boost Spirit parser based example:
Goal, sample service
I want to be able to call pre-existing functions of various classes: /exec <functionName> <param1> <param2>
Imagine your application has the following existing classes which represent services that the user should be able to call into using text commands:
struct Echo
{
void WriteLine(const std::string& s) { std::cout << "WriteLine('" << s << "');" << std::endl; }
void WriteStr (const std::string& s) { std::cout << "Write(string: '" << s << "');" << std::endl; }
void WriteInt (int i) { std::cout << "Write(int: " << i << ");" << std::endl; }
void WriteDbl (double d) { std::cout << "Write(double: " << d << ");" << std::endl; }
void NewLine () { std::cout << "NewLine();" << std::endl; }
} echoService;
struct Admin
{
void Shutdown(const std::string& reason, int retval)
{
std::cout << "Shutdown(reason: '" << reason << "', retval: " << retval << ")" << std::endl;
// exit(retval);
}
} adminService;
Approach and Explanation (TL;DR? skip this)
So how do we tie line-based input to this 'interface'?
There are two jobs
- parse input
- evaluate input
You have been emphasizing on providing infrastructure for evaluation. However, you run into the problem of not knowing what parameters to pass. You know when parsing, of course. You would really like to avoid having to store the arguments in a generic container. (Of course, you could throw boost optionals, boost (recursive) variants, boost any at it all at once, but let's face it: that is still tedious busy-work).
This is an excellent opportunity for a parser with semantic actions. Lex/yacc, Coco/R C++, ANTLR and many more all support them. So does Boost Spirit Qi.
Without further ado, this is what a complete, minimalist line grammar could look like for the services described above:
parser = "/execute" > (
(lit("WriteLine") > stringlit)
| (lit("Write") >> +(double_ | int_ | stringlit))
| lit("NewLine")
| (lit("Shutdown") > (stringlit > -int_))
// stringlit is just a quoted string:
stringlit = lexeme[ '"' >> *~char_('"') >> '"' ];
Note: I decided to show you how you can accept arbitrary numbers of arguments of various types to the /execute Write
call
Adding the semantic actions to get tie this to our echoService
and adminService
objects, we get this REPL engine to parse and evaluate a single line:
void execute(const std::string& command)
{
typedef std::string::const_iterator It;
It f(command.begin()), l(command.end());
if (!phrase_parse(f,l, "/execute" > (
(lit("WriteLine")
> stringlit [ bind(&Echo::WriteLine, ref(echoService), _1) ])
| (lit("Write") >> +(
double_ [ bind(&Echo::WriteDbl, ref(echoService), _1) ]
| int_ [ bind(&Echo::WriteInt, ref(echoService), _1) ]
| stringlit [ bind(&Echo::WriteStr, ref(echoService), _1) ]
))
| (lit("NewLine") [ bind(&Echo::NewLine, ref(echoService)) ])
| (lit("Shutdown") > (stringlit > (int_ | attr(0)))
[ bind(&Admin::Shutdown, ref(adminService), _1, _2) ])
), space))
{
// handle error, see full code below
}
}
Now all that's left for us to do, is program a main loop:
int main()
{
std::string command;
while (std::getline(std::cin, command))
execute(command);
}
That's pretty simple, not?
Complete working example program
I posted a complete working example of this program on github 1: https://gist.github.com/1314900
It has
- complete error handling / reporting added
- uses an input file instead of std::cin for (ease of testing)
- will help you get this started (namespaces, includes)
All you need is Boost. I tested this with g++ 4.6.1 (no special options) and Boost 1.47. For the following test input (input.txt):
/execute WriteLine "bogus"
/execute Write "here comes the answer: "
/execute Write 42
/execute Write 31415e-4
/execute Write "that is the inverse of" 24 "and answers nothing"
/execute Shutdown "Bye" 9
/execute Shutdown "Test default value for retval"
The output of the demo program is
WriteLine('bogus');
Write(string: 'here comes the answer: ');
Write(double: 42);
Write(double: 3.1415);
Write(string: 'that is the inverse of');
Write(double: 24);
Write(string: 'and answers nothing');
Shutdown(reason: 'Bye', retval: 9)
Shutdown(reason: 'Test default value for retval', retval: 0)
- Note: I commented out the
exit(...)
call so I could demo how you could make the retval
parameter optional while supplying a default value (attr(0)
)
- Note: how the double
31415e-4
is prints correctly as 3.1415
- Note: how the multiple parameters of
/execute Write
get translated in separate calls to echoService.Write(...)
depending on the actual type of the input parameter
- Note: how the existence of inline whitespace (outside of string literals) is completely ignored. You can use as many tabs/spaces as you like
1 In the interest of posterity, should github cease to host my gist:
#include <boost/spirit/include/qi.hpp>
#include <boost/spirit/include/phoenix.hpp>
#include <fstream>
namespace qi = boost::spirit::qi;
namespace phx = boost::phoenix;
///////////////////////////////////
// 'domain classes' (scriptables)
struct Echo
{
void WriteLine(const std::string& s) { std::cout << "WriteLine('" << s << "');" << std::endl; }
void WriteStr (const std::string& s) { std::cout << "Write(string: '" << s << "');" << std::endl; }
void WriteInt (int i) { std::cout << "Write(int: " << i << ");" << std::endl; }
void WriteDbl (double d) { std::cout << "Write(double: " << d << ");" << std::endl; }
void NewLine () { std::cout << "NewLine();" << std::endl; }
} echoService;
struct Admin
{
void Shutdown(const std::string& reason, int retval)
{
std::cout << "Shutdown(reason: '" << reason << "', retval: " << retval << ")" << std::endl;
// exit(retval);
}
} adminService;
void execute(const std::string& command)
{
typedef std::string::const_iterator It;
It f(command.begin()), l(command.end());
using namespace qi;
using phx::bind;
using phx::ref;
rule<It, std::string(), space_type> stringlit = lexeme[ '"' >> *~char_('"') >> '"' ];
try
{
if (!phrase_parse(f,l, "/execute" > (
(lit("WriteLine")
> stringlit [ bind(&Echo::WriteLine, ref(echoService), _1) ])
| (lit("Write") >> +(
double_ [ bind(&Echo::WriteDbl, ref(echoService), _1) ] // the order matters
| int_ [ bind(&Echo::WriteInt, ref(echoService), _1) ]
| stringlit [ bind(&Echo::WriteStr, ref(echoService), _1) ]
))
| (lit("NewLine") [ bind(&Echo::NewLine, ref(echoService)) ])
| (lit("Shutdown") > (stringlit > (int_ | attr(0)))
[ bind(&Admin::Shutdown, ref(adminService), _1, _2) ])
), space))
{
if (f!=l) // allow whitespace only lines
std::cerr << "** (error interpreting command: " << command << ")" << std::endl;
}
}
catch (const expectation_failure<It>& e)
{
std::cerr << "** (unexpected input '" << std::string(e.first, std::min(e.first+10, e.last)) << "') " << std::endl;
}
if (f!=l)
std::cerr << "** (warning: skipping unhandled input '" << std::string(f,l) << "')" << std::endl;
}
int main()
{
std::ifstream ifs("input.txt");
std::string command;
while (std::getline(ifs/*std::cin*/, command))
execute(command);
}