├── .gitignore ├── LICENSE ├── README.md ├── command_interpreter.hpp ├── repl.cpp └── test_ci.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *~ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2021, Empirical Software Solutions, LLC 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ### Command Interpreter 2 | 3 | This header-only library makes it easy to add command evaluation to a C++ program. 4 | 5 | ```cpp 6 | #include "command_interpreter.hpp" 7 | 8 | class Arithmetic : public CommandInterpreter { 9 | static int add(int x, int y) { 10 | return x + y; 11 | } 12 | 13 | int inc(int x) { 14 | return x + 1; 15 | } 16 | 17 | void register_commands() override { 18 | register_command(add, "add", "Add two numbers"); 19 | register_command(&Arithmetic::inc, "inc", "Increment a number"); 20 | } 21 | }; 22 | ``` 23 | 24 | A command is simply a function. Just register any function with a helper string; type safety occurs automatically. 25 | 26 | We can create a simple REPL with the above interpreter. 27 | 28 | ```cpp 29 | #include 30 | 31 | int main() { 32 | Arithmetic arithmetic; 33 | std::string text; 34 | std::cout << ">>> "; 35 | while (std::getline(std::cin, text)) { 36 | std::cout << arithmetic.eval(text) << std::endl << std::endl; 37 | std::cout << ">>> "; 38 | } 39 | return 0; 40 | } 41 | ``` 42 | 43 | And then we have a command interpreter. 44 | 45 | ``` 46 | >>> help 47 | add Add two numbers 48 | inc Increment a number 49 | help Show this help 50 | 51 | >>> add 3 4 52 | 7 53 | 54 | >>> inc 21 55 | 22 56 | 57 | >>> add 3 4 5 58 | Error: expected 2 arguments; got 3 59 | 60 | >>> add 3 four 61 | Error: invalid argument type at position 1; expected type i 62 | 63 | ``` 64 | 65 | Command Interpreter was created for adding a way to query state in microservices, like getting the list of outstanding orders in a trading system. 66 | 67 | **This library requires Boost's lexical cast and preprocessor macros.** 68 | -------------------------------------------------------------------------------- /command_interpreter.hpp: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Empirical Software Solutions, LLC 3 | * All rights reserved. 4 | * 5 | * Redistribution and use in source and binary forms, with or without 6 | * modification, are permitted provided that the following conditions 7 | * are met: 8 | * * Redistributions of source code must retain the above copyright 9 | * notice, this list of conditions and the following disclaimer. 10 | * * Redistributions in binary form must reproduce the above copyright 11 | * notice, this list of conditions and the following disclaimer in 12 | * the documentation and/or other materials provided with the 13 | * distribution. 14 | * 15 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 16 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 17 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 18 | * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 19 | * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, 20 | * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 21 | * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS 22 | * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED 23 | * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT 25 | * OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 26 | * SUCH DAMAGE. 27 | */ 28 | 29 | #pragma once 30 | 31 | #include 32 | #include 33 | #include 34 | 35 | #include 36 | #include 37 | #include 38 | 39 | // pre-C++14 will get decay_t defined 40 | #if __cplusplus < 201300 41 | namespace std { 42 | template 43 | using decay_t = typename decay::type; 44 | } 45 | #endif 46 | 47 | // store the help string and attempt to invoke command 48 | #define register_command(func, name, help) \ 49 | commands_.emplace_back(name, help); \ 50 | if (func_ == name) result_ = call_command(func); 51 | 52 | /* 53 | * Make C++ functions available to an interpreter. 54 | * 55 | * Note that the functions cannot be templated or overloaded. 56 | */ 57 | 58 | class CommandInterpreter { 59 | protected: 60 | 61 | // individual command info 62 | struct Command { 63 | std::string name; 64 | std::string help; 65 | Command(const std::string& n, const std::string& h) : name(n), help(h) {} 66 | }; 67 | 68 | // list of user commands 69 | std::vector commands_; 70 | 71 | // function, arguments, and the result 72 | std::string func_; 73 | std::vector args_; 74 | std::string result_; 75 | 76 | // report wrong number of arguments 77 | std::string err_num_args(size_t expected, size_t got) { 78 | std::ostringstream oss; 79 | oss << "Error: expected " << expected << " argument"; 80 | if (expected != 1) { 81 | oss << 's'; 82 | } 83 | oss << "; got " << got; 84 | return oss.str(); 85 | } 86 | 87 | // report wrong argument type 88 | std::string err_type_args(const std::string& expected, size_t position) { 89 | std::ostringstream oss; 90 | oss << "Error: invalid argument type at position " << position 91 | << "; expected type " << expected; 92 | return oss.str(); 93 | } 94 | 95 | // split the text into function and arguments 96 | void parse(const std::string& text) { 97 | func_.clear(); 98 | args_.clear(); 99 | 100 | std::string word; 101 | for (char c: text) { 102 | if (std::isspace(c)) { 103 | if (!word.empty()) { 104 | args_.push_back(word); 105 | word.clear(); 106 | } 107 | } 108 | else { 109 | word.push_back(c); 110 | } 111 | } 112 | if (!word.empty()) { 113 | args_.push_back(word); 114 | } 115 | 116 | if (!args_.empty()) { 117 | func_ = args_[0]; 118 | args_.erase(args_.begin()); 119 | } 120 | } 121 | 122 | // display user's commands 123 | std::string help() { 124 | // find longest func name 125 | size_t longest = 0; 126 | for (auto& c: commands_) { 127 | longest = std::max(longest, c.name.size()); 128 | } 129 | longest += 2; 130 | 131 | // record each func name padded with space, and then the help text 132 | std::string str; 133 | for (auto& c: commands_) { 134 | str += c.name; 135 | for (size_t i = 0; i < longest - c.name.size(); i++) { 136 | str += ' '; 137 | } 138 | str += c.help + '\n'; 139 | } 140 | str.pop_back(); 141 | return str; 142 | } 143 | 144 | // Generate the call_command functions for varying numbers of parameters. 145 | // The call_command will then template-match the passed function's 146 | // arguments, which allows lexical_cast to know what type to convert 147 | // the user's string to. Ie., foo(int) will require the argument string 148 | // to be cast to a single integer. 149 | 150 | #define REP(z, n, text) , text##n 151 | 152 | #define REP_COMMA(z, n, text) BOOST_PP_COMMA_IF(n) text##n 153 | 154 | #define CAST(z, n, text) \ 155 | typedef typename std::decay_t TT##n; \ 156 | TT##n x##n; \ 157 | try { \ 158 | x##n = boost::lexical_cast(args_[n]); \ 159 | } \ 160 | catch (boost::bad_lexical_cast& err) { \ 161 | return err_type_args(typeid(x##n).name(), n); \ 162 | } 163 | 164 | #define CALL_COMMAND(z, n, text) \ 165 | template \ 166 | std::string call_command(R (*func)( BOOST_PP_REPEAT(n, REP_COMMA, T) )) { \ 167 | if (args_.size () != n) { \ 168 | return err_num_args(n, args_.size()); \ 169 | } \ 170 | BOOST_PP_REPEAT(n, CAST, ~) \ 171 | return boost::lexical_cast(func( BOOST_PP_REPEAT(n, REP_COMMA, x) )); \ 172 | } \ 173 | template \ 174 | std::string call_command(R (C::*func)( BOOST_PP_REPEAT(n, REP_COMMA, T) )) { \ 175 | if (args_.size () != n) { \ 176 | return err_num_args(n, args_.size()); \ 177 | } \ 178 | BOOST_PP_REPEAT(n, CAST, ~) \ 179 | C* obj = (C*)(this); \ 180 | return boost::lexical_cast((obj->*func)( BOOST_PP_REPEAT(n, REP_COMMA, x) )); \ 181 | } 182 | 183 | BOOST_PP_REPEAT(8, CALL_COMMAND, ~) 184 | 185 | #undef REP 186 | #undef REP_COMMA 187 | #undef CAST 188 | #undef CALL_COMMAND 189 | 190 | public: 191 | 192 | // evaluate the user's input 193 | std::string eval(const std::string& text) { 194 | parse(text); 195 | result_.clear(); 196 | commands_.clear(); 197 | 198 | // the result will be overwritten if the command is recognized 199 | if (!func_.empty()) { 200 | result_ = "Unrecognized command: " + func_; 201 | register_commands(); 202 | register_command(&CommandInterpreter::help, "help", "Show this help"); 203 | } 204 | 205 | return result_; 206 | } 207 | 208 | // user must define with each command 209 | virtual void register_commands() = 0; 210 | }; 211 | 212 | -------------------------------------------------------------------------------- /repl.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "command_interpreter.hpp" 3 | 4 | class Arithmetic : public CommandInterpreter { 5 | static int add(int x, int y) { 6 | return x + y; 7 | } 8 | 9 | int inc(int x) { 10 | return x + 1; 11 | } 12 | 13 | void register_commands() override { 14 | register_command(add, "add", "Add two numbers"); 15 | register_command(&Arithmetic::inc, "inc", "Increment a number"); 16 | } 17 | }; 18 | 19 | int main() { 20 | Arithmetic arithmetic; 21 | std::string text; 22 | std::cout << ">>> "; 23 | while (std::getline(std::cin, text)) { 24 | std::cout << arithmetic.eval(text) << std::endl << std::endl; 25 | std::cout << ">>> "; 26 | } 27 | return 0; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /test_ci.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "command_interpreter.hpp" 3 | 4 | // example of a non-member function 5 | int add(int x, int y) { 6 | return x + y; 7 | } 8 | 9 | class Arithmetic : public CommandInterpreter { 10 | // example of a static member function 11 | static int inc(int x) { 12 | return x + 1; 13 | } 14 | 15 | // example of a non-static member function 16 | int twice(int x) { 17 | return 2 * x; 18 | } 19 | 20 | // example with a const ref 21 | std::string rev(const std::string& s) { 22 | std::string r(s); 23 | std::reverse(r.begin(), r.end()); 24 | return r; 25 | } 26 | 27 | // just need to register the commands with a help string 28 | void register_commands() override { 29 | register_command(add, "add", "Add two numbers"); 30 | register_command(inc, "inc", "Increment a number"); 31 | register_command(&Arithmetic::twice, "twice", "Double a number"); 32 | register_command(&Arithmetic::rev, "rev", "Reverse a string"); 33 | } 34 | } arithmetic; 35 | 36 | // regression tests 37 | int ret_val = 0; 38 | void test(const std::string& text, const std::string& expected) { 39 | std::string result = arithmetic.eval(text); 40 | if (result != expected) { 41 | std::cout << "Text: " << text << std::endl; 42 | std::cout << "Expected: " << expected << std::endl; 43 | std::cout << "Result: " << result << std::endl; 44 | ret_val = 1; 45 | } 46 | } 47 | 48 | // bring it altogether 49 | int main() { 50 | test("inc 17", "18"); 51 | test("add 4 5\n", "9"); 52 | test("twice 7", "14"); 53 | test("rev Hello", "olleH"); 54 | 55 | test("inc", "Error: expected 1 argument; got 0"); 56 | test("inc 1 7", "Error: expected 1 argument; got 2"); 57 | test("inc 1.7", "Error: invalid argument type at position 0; expected type i"); 58 | 59 | test("add 4", "Error: expected 2 arguments; got 1"); 60 | test("add 4 5 6", "Error: expected 2 arguments; got 3"); 61 | test("add 4.4 5", "Error: invalid argument type at position 0; expected type i"); 62 | test("add 4 5.5", "Error: invalid argument type at position 1; expected type i"); 63 | 64 | test("help", "add Add two numbers\ninc Increment a number\ntwice Double a number\nrev Reverse a string\nhelp Show this help"); 65 | 66 | return ret_val; 67 | } 68 | 69 | --------------------------------------------------------------------------------