diff options
author | Remko Tronçon <git@el-tramo.be> | 2013-12-28 16:13:08 (GMT) |
---|---|---|
committer | Remko Tronçon <git@el-tramo.be> | 2014-01-03 11:09:06 (GMT) |
commit | 0b19dc7292b7672c9fbb711a411c392bc5b2bb34 (patch) | |
tree | d5af147193d52a3e63d348dd794ca92cdd934974 /Sluift | |
parent | 5a89265623214164fa7ce36721de05183d53058d (diff) | |
download | swift-0b19dc7292b7672c9fbb711a411c392bc5b2bb34.zip swift-0b19dc7292b7672c9fbb711a411c392bc5b2bb34.tar.bz2 |
Sluift: Custom console/interpreter
- Prints results of each command (if it can be interpreted as an
expression)
- Stores results of last command in _1, _2, ...
- Supports tab completion
- Compatible with Lua 5.2
Other changes:
- Add support for specifying custom editline library
- Don't load sluift into global namespace. Tab completion should be
convenient enough.
Change-Id: I2a26346469d67c281d09d47cacaa0b267f5ea9f9
Diffstat (limited to 'Sluift')
-rw-r--r-- | Sluift/.gitignore | 1 | ||||
-rw-r--r-- | Sluift/Completer.cpp | 12 | ||||
-rw-r--r-- | Sluift/Completer.h | 19 | ||||
-rw-r--r-- | Sluift/Console.cpp | 279 | ||||
-rw-r--r-- | Sluift/Console.h | 41 | ||||
-rw-r--r-- | Sluift/EditlineTerminal.cpp | 98 | ||||
-rw-r--r-- | Sluift/EditlineTerminal.h | 25 | ||||
-rw-r--r-- | Sluift/Lua/LuaUtils.h | 6 | ||||
-rw-r--r-- | Sluift/SConscript | 24 | ||||
-rw-r--r-- | Sluift/SConscript.variant | 29 | ||||
-rw-r--r-- | Sluift/StandardTerminal.cpp | 38 | ||||
-rw-r--r-- | Sluift/StandardTerminal.h | 22 | ||||
-rw-r--r-- | Sluift/Terminal.cpp | 15 | ||||
-rw-r--r-- | Sluift/Terminal.h | 35 | ||||
-rw-r--r-- | Sluift/UnitTest/TokenizeTest.cpp | 64 | ||||
-rw-r--r-- | Sluift/linit.c | 37 | ||||
-rw-r--r-- | Sluift/main.cpp | 168 | ||||
-rw-r--r-- | Sluift/sluift.h | 4 | ||||
-rw-r--r-- | Sluift/tokenize.cpp | 86 | ||||
-rw-r--r-- | Sluift/tokenize.h | 16 |
20 files changed, 951 insertions, 68 deletions
diff --git a/Sluift/.gitignore b/Sluift/.gitignore index 940fc86..e2d8bbf 100644 --- a/Sluift/.gitignore +++ b/Sluift/.gitignore @@ -1,4 +1,5 @@ lua.c +Version.h sluift_dll.cpp sluift dll.c diff --git a/Sluift/Completer.cpp b/Sluift/Completer.cpp new file mode 100644 index 0000000..4bd1eaf --- /dev/null +++ b/Sluift/Completer.cpp @@ -0,0 +1,12 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/Completer.h> + +using namespace Swift; + +Completer::~Completer() { +} diff --git a/Sluift/Completer.h b/Sluift/Completer.h new file mode 100644 index 0000000..9651eb1 --- /dev/null +++ b/Sluift/Completer.h @@ -0,0 +1,19 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> +#include <vector> + +namespace Swift { + class Completer { + public: + virtual ~Completer(); + + virtual std::vector<std::string> getCompletions(const std::string& buffer, int start, int end) = 0; + }; +} diff --git a/Sluift/Console.cpp b/Sluift/Console.cpp new file mode 100644 index 0000000..7b5b437 --- /dev/null +++ b/Sluift/Console.cpp @@ -0,0 +1,279 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/Console.h> +#include <lua.hpp> +#include <stdexcept> +#include <iostream> +#include <boost/optional.hpp> +#include <boost/lexical_cast.hpp> +#include <boost/algorithm/string/predicate.hpp> +#include <boost/numeric/conversion/cast.hpp> +#include <Sluift/Terminal.h> +#include <Sluift/tokenize.h> +#include <Sluift/Lua/LuaUtils.h> +#include <cctype> + +using namespace Swift; + +/** + * This function is called by pcall() when an error happens. + * Adds the backtrace to the error message. + */ +static int traceback(lua_State* L) { + if (!lua_isstring(L, 1)) { + return 1; + } + lua_getglobal(L, "debug"); + if (!lua_istable(L, -1)) { + lua_pop(L, 1); + return 1; + } + lua_getfield(L, -1, "traceback"); + if (!lua_isfunction(L, -1)) { + lua_pop(L, 2); + return 1; + } + lua_pushvalue(L, 1); + lua_pushinteger(L, 2); + lua_call(L, 2, 1); + return 1; +} + + +Console::Console(lua_State* L, Terminal* terminal) : L(L), terminal(terminal), previousNumberOfReturnArguments(0) { + terminal->setCompleter(this); +} + +Console::~Console() { +} + +void Console::run() { + while (true) { + lua_settop(L, 0); + try { + if (!readCommand()) { + return; + } + int result = call(L, 0, true); + if (result != 0) { + throw std::runtime_error(getErrorMessage()); + } + + // Clear the previous results + for (int i = 0; i < previousNumberOfReturnArguments; ++i) { + lua_pushnil(L); + lua_setglobal(L, ("_" + boost::lexical_cast<std::string>(i+1)).c_str()); + } + + // Store the results + for (int i = 0; i < lua_gettop(L); ++i) { + lua_pushvalue(L, i+1); + lua_setglobal(L, ("_" + boost::lexical_cast<std::string>(i+1)).c_str()); + } + previousNumberOfReturnArguments = lua_gettop(L); + + // Print results + if (lua_gettop(L) > 0) { + lua_getglobal(L, "print"); + lua_insert(L, 1); + if (lua_pcall(L, lua_gettop(L)-1, 0, 0) != 0) { + throw std::runtime_error("Error calling 'print': " + getErrorMessage()); + } + } + } + catch (const std::exception& e) { + terminal->printError(e.what()); + } + } + +} + +int Console::tryLoadCommand(const std::string& originalCommand) { + std::string command = originalCommand; + + // Replace '=' by 'return' (for compatibility with Lua console) + if (boost::algorithm::starts_with(command, "=")) { + command = "return " + command.substr(1); + } + + std::string commandAsExpression = "return " + command; + + // Try to load the command as an expression + if (luaL_loadbuffer(L, commandAsExpression.c_str(), commandAsExpression.size(), "=stdin") == 0) { + return 0; + } + lua_pop(L, 1); + + // Try to load the command as a regular command + return luaL_loadbuffer(L, command.c_str(), command.size(), "=stdin"); +} + +bool Console::readCommand() { + boost::optional<std::string> line = terminal->readLine(getPrompt(true)); + if (!line) { + return false; + } + std::string command = *line; + while (true) { + int result = tryLoadCommand(command); + + // Check if we need to read more + if (result == LUA_ERRSYNTAX) { + std::string errorMessage(lua_tostring(L, -1)); + if (boost::algorithm::ends_with(errorMessage, "'<eof>'")) { + lua_pop(L, 1); + + // Read another line + boost::optional<std::string> line = terminal->readLine(getPrompt(false)); + if (!line) { + return false; + } + command = command + "\n" + *line; + continue; + } + } + if (!command.empty()) { + terminal->addToHistory(command); + } + if (result != 0) { + throw std::runtime_error(getErrorMessage()); + } + return true; + } +} + +std::string Console::getErrorMessage() const { + if (lua_isnil(L, -1)) { + return "<null error>"; + } + const char* errorMessage = lua_tostring(L, -1); + return errorMessage ? errorMessage : "<error is not a string>"; +} + +int Console::call(lua_State* L, int numberOfArguments, bool keepResult) { + // Put traceback function on stack below call + int tracebackIndex = lua_gettop(L) - numberOfArguments; + lua_pushcfunction(L, traceback); + lua_insert(L, tracebackIndex); + + int result = lua_pcall(L, numberOfArguments, keepResult ? LUA_MULTRET : 0, tracebackIndex); + + // Remove traceback + lua_remove(L, tracebackIndex); + + return result; +} + +std::string Console::getPrompt(bool firstLine) const { + lua_getglobal(L,firstLine ? "_PROMPT" : "_PROMPT2"); + const char* rawPrompt = lua_tostring(L, -1); + std::string prompt; + if (rawPrompt) { + prompt = std::string(rawPrompt); + } + else { + prompt = firstLine ? "> " : ">> "; + } + lua_pop(L, 1); + return prompt; +} + +static void addMatchingTableKeys(lua_State* L, const std::string& match, std::vector<std::string>& result) { + for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) { + const char* rawKey = lua_tostring(L, -2); + if (rawKey) { + std::string key(rawKey); + if (boost::starts_with(key, match) && !(match == "" && boost::starts_with(key, "_"))) { + result.push_back(key); + } + } + } +} + +static void addMatchingTableValues(lua_State* L, const std::string& match, std::vector<std::string>& result) { + for (lua_pushnil(L); lua_next(L, -2); lua_pop(L, 1)) { + const char* rawValue = lua_tostring(L, -1); + if (rawValue) { + std::string key(rawValue); + if (boost::starts_with(key, match) && !(match == "" && boost::starts_with(key, "_"))) { + result.push_back(key); + } + } + } +} + +std::vector<std::string> Console::getCompletions(const std::string& input, int start, int end) { + std::string prefix = input.substr(boost::numeric_cast<size_t>(start), boost::numeric_cast<size_t>(end - start)); + + std::vector<std::string> tokens; + if (end) { + tokens = Lua::tokenize(input.substr(0, boost::numeric_cast<size_t>(end))); + } + + // Don't autocomplete strings + if (!tokens.empty() && ((*tokens.rbegin())[0] == '\'' || (*tokens.rbegin())[0] == '"')) { + return std::vector<std::string>(); + } + + std::vector<std::string> context; + for (std::vector<std::string>::reverse_iterator i = tokens.rbegin(); i != tokens.rend(); ++i) { + if (std::isalpha((*i)[0]) || (*i)[0] == '_') { + if (i != tokens.rbegin()) { + context.push_back(*i); + } + } + else if (*i != "." && *i != ":") { + break; + } + } + + // Drill into context + int top = lua_gettop(L); + lua_pushglobaltable(L); + for (std::vector<std::string>::reverse_iterator i = context.rbegin(); i != context.rend(); ++i) { + if (lua_istable(L, -1) || lua_isuserdata(L, -1)) { + lua_getfield(L, -1, i->c_str()); + if (!lua_isnil(L, 1)) { + continue; + } + } + lua_settop(L, top); + return std::vector<std::string>(); + } + + // Collect all keys from the table + std::vector<std::string> result; + if (lua_istable(L, -1)) { + addMatchingTableKeys(L, prefix, result); + } + + // Collect all keys from the metatable + if (lua_getmetatable(L, -1)) { + lua_getfield(L, -1, "__index"); + if (lua_istable(L, -1)) { + addMatchingTableKeys(L, prefix, result); + } + lua_pop(L, 1); + + lua_getfield(L, -1, "_completions"); + if (lua_isfunction(L, -1)) { + lua_pushvalue(L, -3); + if (lua_pcall(L, 1, 1, 0) != 0) { + throw std::runtime_error("Error calling '_completions': " + getErrorMessage()); + } + } + if (lua_istable(L, -1)) { + addMatchingTableValues(L, prefix, result); + } + lua_pop(L, 2); + } + + lua_settop(L, top); + + return result; +} + diff --git a/Sluift/Console.h b/Sluift/Console.h new file mode 100644 index 0000000..2d10b38 --- /dev/null +++ b/Sluift/Console.h @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> +#include <boost/optional/optional_fwd.hpp> +#include <Sluift/Completer.h> +#include <Swiften/Base/Override.h> + +struct lua_State; + +namespace Swift { + class Terminal; + + class Console : public Completer { + public: + Console(lua_State* L, Terminal* terminal); + virtual ~Console(); + + void run(); + + static int call(lua_State* L, int numberOfArguments, bool keepResult); + + private: + std::string getPrompt(bool firstLine) const; + std::string getErrorMessage() const; + bool readCommand(); + int tryLoadCommand(const std::string& command); + + virtual std::vector<std::string> getCompletions(const std::string&, int start, int end) SWIFTEN_OVERRIDE; + + private: + lua_State* L; + Terminal* terminal; + int previousNumberOfReturnArguments; + }; +} diff --git a/Sluift/EditlineTerminal.cpp b/Sluift/EditlineTerminal.cpp new file mode 100644 index 0000000..ec2c7a2 --- /dev/null +++ b/Sluift/EditlineTerminal.cpp @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/EditlineTerminal.h> + +#include <boost/optional.hpp> +#include <iostream> +#include <editline/readline.h> +#include <boost/numeric/conversion/cast.hpp> +#include <cassert> +#include <vector> +#include <cstring> + +#include <Swiften/Base/Platform.h> +#include <Sluift/Completer.h> + +using namespace Swift; + +static EditlineTerminal* globalInstance = NULL; + +static int completionStart = -1; +static int completionEnd = -1; + +#if defined(SWIFTEN_PLATFORM_WINDOWS) +static char* getEmptyCompletions(const char*, int) { +#else +static int getEmptyCompletions(const char*, int) { +#endif + return 0; +} + +static char* getCompletions(const char*, int state) { + rl_completion_append_character = 0; +#if RL_READLINE_VERSION >= 0x0600 + rl_completion_suppress_append = 1; +#endif + + static std::vector<std::string> completions; + if (state == 0) { + assert(globalInstance); + completions.clear(); + if (globalInstance->getCompleter()) { + completions = globalInstance->getCompleter()->getCompletions(rl_line_buffer, completionStart, completionEnd); + } + } + if (boost::numeric_cast<size_t>(state) >= completions.size()) { + return 0; + } + return strdup(completions[boost::numeric_cast<size_t>(state)].c_str()); +} + +static char** getAttemptedCompletions(const char* text, int start, int end) { + completionStart = start; + completionEnd = end; + return rl_completion_matches(text, getCompletions); +} + +EditlineTerminal& EditlineTerminal::getInstance() { + static EditlineTerminal instance; + globalInstance = &instance; + return instance; +} + +EditlineTerminal::EditlineTerminal() { + rl_attempted_completion_function = getAttemptedCompletions; + rl_completion_entry_function = getEmptyCompletions; // Fallback. Do nothing. +#if defined(SWIFTEN_PLATFORM_WINDOWS) + // rl_basic_word_break is a cons char[] in MinGWEditLine. + // This one seems to work, although it doesn't on OS X for some reason. + rl_completer_word_break_characters = strdup(" \t\n.:+-*/><=;|&()[]{}"); +#else + rl_basic_word_break_characters = strdup(" \t\n.:+-*/><=;|&()[]{}"); +#endif +} + +EditlineTerminal::~EditlineTerminal() { +} + +void EditlineTerminal::printError(const std::string& message) { + std::cout << message << std::endl; +} + +boost::optional<std::string> EditlineTerminal::readLine(const std::string& prompt) { + const char* line = readline(prompt.c_str()); + return line ? std::string(line) : boost::optional<std::string>(); +} + +void EditlineTerminal::addToHistory(const std::string& line) { +#if defined(SWIFTEN_PLATFORM_WINDOWS) + // MinGWEditLine copies the string, so this is safe + add_history(const_cast<char*>(line.c_str())); +#else + add_history(line.c_str()); +#endif +} diff --git a/Sluift/EditlineTerminal.h b/Sluift/EditlineTerminal.h new file mode 100644 index 0000000..2f671b7 --- /dev/null +++ b/Sluift/EditlineTerminal.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <Swiften/Base/Override.h> +#include <Sluift/Terminal.h> + +namespace Swift { + class EditlineTerminal : public Terminal { + public: + static EditlineTerminal& getInstance(); + + private: + EditlineTerminal(); + virtual ~EditlineTerminal(); + + virtual boost::optional<std::string> readLine(const std::string& prompt) SWIFTEN_OVERRIDE; + virtual void printError(const std::string& message) SWIFTEN_OVERRIDE; + virtual void addToHistory(const std::string& command); + }; +} diff --git a/Sluift/Lua/LuaUtils.h b/Sluift/Lua/LuaUtils.h index bad307c..f677307 100644 --- a/Sluift/Lua/LuaUtils.h +++ b/Sluift/Lua/LuaUtils.h @@ -8,8 +8,12 @@ #include <lua.hpp> #include <boost/optional.hpp> +#include <string> +#include <vector> -struct lua_State; +#if LUA_VERSION_NUM < 502 +#define lua_pushglobaltable(L) lua_pushvalue(L, LUA_GLOBALSINDEX) +#endif namespace Swift { namespace Lua { diff --git a/Sluift/SConscript b/Sluift/SConscript index c8f1108..116c5f1 100644 --- a/Sluift/SConscript +++ b/Sluift/SConscript @@ -47,22 +47,10 @@ elif env["SCONS_STAGE"] == "build" : if sluift_env["PLATFORM"] == "win32" : sluift_env.Append(CPPDEFINES = ["SLUIFT_BUILD_DLL"]) - # Generate a customized lua.c - sluift_env["SLUIFT_VERSION"] = Version.getBuildVersion(env.Dir("#").abspath, "sluift") - def patchLua(env, target, source) : - f = open(source[0].abspath, "r") - contents = f.read() - f.close() - if env["PLATFORM"] == "win32" : - key = "Z" - else : - key = "D" - contents = contents.replace("LUA_RELEASE", "\"== Sluift XMPP Console (%(version)s) == \\nPress Ctrl-%(key)s to exit\"" % {"version": source[1].get_contents(), "key" : key}) - contents = contents.replace("LUA_COPYRIGHT", "") - f = open(target[0].abspath, "w") - f.write(contents) - f.close() - sluift_env.Command("lua.c", ["#/3rdParty/Lua/src/lua.c", sluift_env.Value(sluift_env["SLUIFT_VERSION"])], env.Action(patchLua, cmdstr = "$GENCOMSTR")) + # Generate Version.h + version_header = "#pragma once\n\n" + version_header += "#define SLUIFT_VERSION_STRING \"" + Version.getBuildVersion(env.Dir("#").abspath, "sluift") + "\"\n" + sluift_env.WriteVal("Version.h", sluift_env.Value(version_header)) # Generate core.cpp def generate_embedded_lua(env, target, source) : @@ -74,10 +62,6 @@ elif env["SCONS_STAGE"] == "build" : f.close() sluift_env.Command("core.c", ["core.lua"], env.Action(generate_embedded_lua, cmdstr="$GENCOMSTR")) - if sluift_env.get("HAVE_READLINE", False) : - sluift_env.Append(CPPDEFINES = ["LUA_USE_READLINE"]) - sluift_env.MergeFlags(sluift_env["READLINE_FLAGS"]) - sluift_env.WriteVal("dll.c", sluift_env.Value("")) sluift_sources = [env.File(x) for x in sluift_sources] diff --git a/Sluift/SConscript.variant b/Sluift/SConscript.variant index 92ee493..d0e2b18 100644 --- a/Sluift/SConscript.variant +++ b/Sluift/SConscript.variant @@ -6,12 +6,29 @@ Import('sluift_variant') Import('sluift_sources') if sluift_variant == 'exe' : - env["SLUIFT"] = sluift_env.Program("sluift", sluift_sources + [ - "#/Sluift/lua.c", - "#/Sluift/linit.c", - ]) - if sluift_env.get("SLUIFT_INSTALLDIR", "") : - sluift_env.Install(os.path.join(sluift_env["SLUIFT_INSTALLDIR"], "bin"), env["SLUIFT"]) + common_objects = sluift_env.StaticObject(sluift_sources) + + sluift_exe_env = sluift_env.Clone() + tokenize = sluift_exe_env.StaticObject("#/Sluift/tokenize.cpp") + exe_sources = tokenize + [ + "#/Sluift/Console.cpp", + "#/Sluift/Terminal.cpp", + "#/Sluift/StandardTerminal.cpp", + "#/Sluift/Completer.cpp", + "#/Sluift/main.cpp", + ] + + if sluift_exe_env.get("HAVE_EDITLINE", False) : + sluift_exe_env.Append(CPPDEFINES = ["HAVE_EDITLINE"]) + sluift_exe_env.MergeFlags(sluift_exe_env["EDITLINE_FLAGS"]) + exe_sources += ["#/Sluift/EditlineTerminal.cpp"] + + env["SLUIFT"] = sluift_exe_env.Program("sluift", common_objects + exe_sources) + if sluift_exe_env.get("SLUIFT_INSTALLDIR", "") : + sluift_exe_env.Install(os.path.join(sluift_exe_env["SLUIFT_INSTALLDIR"], "bin"), env["SLUIFT"]) + + # Unit tests + env.Append(UNITTEST_OBJECTS = tokenize + ["#/Sluift/UnitTest/TokenizeTest.cpp"]) else : sluift_env["SLUIFT_DLL_SUFFIX"] = "${SHLIBSUFFIX}" if sluift_env["PLATFORM"] == "darwin" : diff --git a/Sluift/StandardTerminal.cpp b/Sluift/StandardTerminal.cpp new file mode 100644 index 0000000..f055684 --- /dev/null +++ b/Sluift/StandardTerminal.cpp @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/StandardTerminal.h> + +#include <boost/optional.hpp> +#include <iostream> +#include <stdexcept> + +using namespace Swift; + +StandardTerminal::StandardTerminal() { +} + +StandardTerminal::~StandardTerminal() { +} + +void StandardTerminal::printError(const std::string& message) { + std::cout << message << std::endl; +} + +boost::optional<std::string> StandardTerminal::readLine(const std::string& prompt) { + std::cout << prompt << std::flush; + std::string input; + if (!std::getline(std::cin, input)) { + if (std::cin.eof()) { + return boost::optional<std::string>(); + } + throw std::runtime_error("Input error"); + } + return input; +} + +void StandardTerminal::addToHistory(const std::string&) { +} diff --git a/Sluift/StandardTerminal.h b/Sluift/StandardTerminal.h new file mode 100644 index 0000000..420df6b --- /dev/null +++ b/Sluift/StandardTerminal.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <Swiften/Base/Override.h> +#include <Sluift/Terminal.h> + +namespace Swift { + class StandardTerminal : public Terminal { + public: + StandardTerminal(); + virtual ~StandardTerminal(); + + virtual boost::optional<std::string> readLine(const std::string& prompt) SWIFTEN_OVERRIDE; + virtual void printError(const std::string& message) SWIFTEN_OVERRIDE; + virtual void addToHistory(const std::string& command); + }; +} diff --git a/Sluift/Terminal.cpp b/Sluift/Terminal.cpp new file mode 100644 index 0000000..9233233 --- /dev/null +++ b/Sluift/Terminal.cpp @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/Terminal.h> + +using namespace Swift; + +Terminal::Terminal() : completer(NULL) { +} + +Terminal::~Terminal() { +} diff --git a/Sluift/Terminal.h b/Sluift/Terminal.h new file mode 100644 index 0000000..2d5e41b --- /dev/null +++ b/Sluift/Terminal.h @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <string> +#include <boost/optional/optional_fwd.hpp> + +namespace Swift { + class Completer; + + class Terminal { + public: + Terminal(); + virtual ~Terminal(); + + Completer* getCompleter() const { + return completer; + } + + void setCompleter(Completer* completer) { + this->completer = completer; + } + + virtual boost::optional<std::string> readLine(const std::string& prompt) = 0; + virtual void addToHistory(const std::string& command) = 0; + virtual void printError(const std::string& message) = 0; + + private: + Completer* completer; + }; +} diff --git a/Sluift/UnitTest/TokenizeTest.cpp b/Sluift/UnitTest/TokenizeTest.cpp new file mode 100644 index 0000000..1bf20ed --- /dev/null +++ b/Sluift/UnitTest/TokenizeTest.cpp @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <cppunit/extensions/HelperMacros.h> +#include <cppunit/extensions/TestFactoryRegistry.h> + +#include <Sluift/tokenize.h> + +using namespace Swift; + +class TokenizeTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(TokenizeTest); + CPPUNIT_TEST(testTokenize); + CPPUNIT_TEST(testTokenize); + CPPUNIT_TEST(testTokenize_String); + CPPUNIT_TEST(testTokenize_IncompleteString); + CPPUNIT_TEST(testTokenize_Identifier); + CPPUNIT_TEST_SUITE_END(); + + public: + void testTokenize() { + std::vector<std::string> tokens = Lua::tokenize("foo.bar + 1.23 - bam"); + + CPPUNIT_ASSERT_EQUAL(7, static_cast<int>(tokens.size())); + CPPUNIT_ASSERT_EQUAL(std::string("foo"), tokens[0]); + CPPUNIT_ASSERT_EQUAL(std::string("."), tokens[1]); + CPPUNIT_ASSERT_EQUAL(std::string("bar"), tokens[2]); + CPPUNIT_ASSERT_EQUAL(std::string("+"), tokens[3]); + CPPUNIT_ASSERT_EQUAL(std::string("1.23"), tokens[4]); + CPPUNIT_ASSERT_EQUAL(std::string("-"), tokens[5]); + CPPUNIT_ASSERT_EQUAL(std::string("bam"), tokens[6]); + } + + void testTokenize_String() { + std::vector<std::string> tokens = Lua::tokenize(" foo .. \"1234\\\"bla blo\""); + + CPPUNIT_ASSERT_EQUAL(3, static_cast<int>(tokens.size())); + CPPUNIT_ASSERT_EQUAL(std::string("foo"), tokens[0]); + CPPUNIT_ASSERT_EQUAL(std::string(".."), tokens[1]); + CPPUNIT_ASSERT_EQUAL(std::string("\"1234\\\"bla blo\""), tokens[2]); + } + + void testTokenize_IncompleteString() { + std::vector<std::string> tokens = Lua::tokenize("\"1234"); + + CPPUNIT_ASSERT_EQUAL(1, static_cast<int>(tokens.size())); + CPPUNIT_ASSERT_EQUAL(std::string("\"1234"), tokens[0]); + } + + void testTokenize_Identifier() { + std::vector<std::string> tokens = Lua::tokenize("foo.bar_baz"); + + CPPUNIT_ASSERT_EQUAL(3, static_cast<int>(tokens.size())); + CPPUNIT_ASSERT_EQUAL(std::string("foo"), tokens[0]); + CPPUNIT_ASSERT_EQUAL(std::string("."), tokens[1]); + CPPUNIT_ASSERT_EQUAL(std::string("bar_baz"), tokens[2]); + } + +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(TokenizeTest); diff --git a/Sluift/linit.c b/Sluift/linit.c deleted file mode 100644 index ad27b37..0000000 --- a/Sluift/linit.c +++ /dev/null @@ -1,37 +0,0 @@ -#include <lua.h> -#include <lualib.h> -#include <lauxlib.h> -#include "sluift.h" - -static const luaL_Reg lualibs[] = { - {"", luaopen_base}, - {LUA_LOADLIBNAME, luaopen_package}, - {LUA_TABLIBNAME, luaopen_table}, - {LUA_IOLIBNAME, luaopen_io}, - {LUA_OSLIBNAME, luaopen_os}, - {LUA_STRLIBNAME, luaopen_string}, - {LUA_MATHLIBNAME, luaopen_math}, - {LUA_DBLIBNAME, luaopen_debug}, - {"sluift", luaopen_sluift}, - {NULL, NULL} -}; - - -LUALIB_API void luaL_openlibs (lua_State *L) { - const luaL_Reg *lib = lualibs; - for (; lib->func; lib++) { - lua_pushcfunction(L, lib->func); - lua_pushstring(L, lib->name); - lua_call(L, 1, 0); - } - - /* Import sluift into global namespace */ - lua_pushglobaltable(L); - lua_getfield(L, -1, "sluift"); - for (lua_pushnil(L); lua_next(L, -2); ) { - lua_pushvalue(L, -2); - lua_pushvalue(L, -2); - lua_settable(L, -6); - lua_pop(L, 1); - } -} diff --git a/Sluift/main.cpp b/Sluift/main.cpp new file mode 100644 index 0000000..fcff2aa --- /dev/null +++ b/Sluift/main.cpp @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <string> +#include <vector> +#include <lua.hpp> +#include <Swiften/Base/foreach.h> +#include <Swiften/Base/Platform.h> +#include <boost/program_options/options_description.hpp> +#include <boost/program_options/variables_map.hpp> +#include <boost/program_options.hpp> +#include <boost/version.hpp> +#include <boost/numeric/conversion/cast.hpp> +#include <Sluift/Console.h> +#include <Sluift/StandardTerminal.h> +#include <Sluift/sluift.h> +#include <Sluift/Lua/LuaUtils.h> +#ifdef HAVE_EDITLINE +#include <Sluift/EditlineTerminal.h> +#endif +#include <Sluift/Version.h> + +using namespace Swift; + +#ifdef SWIFTEN_PLATFORM_WINDOWS +#define EXIT_KEY "Z" +#else +#define EXIT_KEY "D" +#endif + +static const std::string SLUIFT_WELCOME_STRING( + "== Sluift XMPP Console (" SLUIFT_VERSION_STRING ")\nPress Ctrl-" EXIT_KEY " to exit"); + +static const luaL_Reg defaultLibraries[] = { + {"", luaopen_base}, + {LUA_LOADLIBNAME, luaopen_package}, + {LUA_TABLIBNAME, luaopen_table}, + {LUA_IOLIBNAME, luaopen_io}, + {LUA_OSLIBNAME, luaopen_os}, + {LUA_STRLIBNAME, luaopen_string}, + {LUA_MATHLIBNAME, luaopen_math}, + {LUA_DBLIBNAME, luaopen_debug}, + {"sluift", luaopen_sluift}, + {NULL, NULL} +}; + +static void checkResult(lua_State* L, int result) { + if (result && !lua_isnil(L, -1)) { + const char* errorMessage = lua_tostring(L, -1); + throw std::runtime_error(errorMessage ? errorMessage : "Unknown error"); + } +} + +static void initialize(lua_State* L) { + lua_gc(L, LUA_GCSTOP, 0); + for (const luaL_Reg* lib = defaultLibraries; lib->func; lib++) { + lua_pushcfunction(L, lib->func); + lua_pushstring(L, lib->name); + lua_call(L, 1, 0); + } + lua_gc(L, LUA_GCRESTART, 0); +} + +static void runScript(lua_State* L, const std::string& script, const std::vector<std::string>& scriptArguments) { + // Create arguments table + lua_createtable(L, boost::numeric_cast<int>(scriptArguments.size()), 0); + for (size_t i = 0; i < scriptArguments.size(); ++i) { + lua_pushstring(L, scriptArguments[i].c_str()); + lua_rawseti(L, -2, boost::numeric_cast<int>(i+1)); + } + lua_setglobal(L, "arg"); + + // Load file + checkResult(L, luaL_loadfile(L, script.c_str())); + foreach (const std::string& scriptArgument, scriptArguments) { + lua_pushstring(L, scriptArgument.c_str()); + } + checkResult(L, Console::call(L, boost::numeric_cast<int>(scriptArguments.size()), false)); +} + +// void runConsole() { + // contents = contents.replace("LUA_RELEASE", "\"== Sluift XMPP Console (%(version)s) == \\nPress Ctrl-%(key)s to exit\"" % {"version": source[1].get_contents(), "key" : key}) +// } + +int main(int argc, char* argv[]) { + // Parse program options + boost::program_options::options_description visibleOptions("Options"); + visibleOptions.add_options() + ("help,h", "Display this help message") + ("version,v", "Display version information") + ("interactive,i", "Enter interactive mode after executing script") + ; + boost::program_options::options_description hiddenOptions("Hidden Options"); + hiddenOptions.add_options() + ("script", boost::program_options::value< std::string >(), "Script to be executed") + ("script-arguments", boost::program_options::value< std::vector<std::string> >(), "Script arguments") + ; + boost::program_options::options_description options("All Options"); + options.add(visibleOptions).add(hiddenOptions); + + boost::program_options::positional_options_description positional_options; + positional_options.add("script", 1).add("script-arguments", -1); + + boost::program_options::variables_map arguments; + try { + boost::program_options::store( + boost::program_options::command_line_parser(argc, argv) + .options(options) + .positional(positional_options).run(), arguments); + } + catch (const boost::program_options::unknown_option& option) { +#if BOOST_VERSION >= 104200 + std::cout << "Ignoring unknown option " << option.get_option_name() << " but continuing." << std::endl; +#else + std::cout << "Error: " << option.what() << " (continuing)" << std::endl; +#endif + } + catch (const std::exception& e) { + std::cout << "Error: " << e.what() << std::endl; + return -1; + } + boost::program_options::notify(arguments); + + // Help & version + if (arguments.count("help")) { + std::cout << visibleOptions << "\n"; + return 0; + } + else if (arguments.count("version")) { + std::cout << SLUIFT_VERSION_STRING; + return 0; + } + + lua_State* L = luaL_newstate(); + initialize(L); + try { + // Run script + if (arguments.count("script")) { + std::vector<std::string> scriptArguments; + if (arguments.count("script-arguments")) { + scriptArguments = arguments["script-arguments"].as< std::vector<std::string> >(); + } + runScript(L, arguments["script"].as<std::string>(), scriptArguments); + } + + // Run console + if (arguments.count("interactive") || arguments.count("script") == 0) { + std::cout << SLUIFT_WELCOME_STRING << std::endl; +#ifdef HAVE_EDITLINE + EditlineTerminal& terminal = EditlineTerminal::getInstance(); +#else + StandardTerminal terminal; +#endif + Console console(L, &terminal); + console.run(); + } + } + catch (const std::exception& e) { + std::cerr << e.what() << std::endl; + lua_close(L); + return -1; + } + lua_close(L); + return 0; +} diff --git a/Sluift/sluift.h b/Sluift/sluift.h index 2613370..b82e1c4 100644 --- a/Sluift/sluift.h +++ b/Sluift/sluift.h @@ -20,10 +20,6 @@ #include <lua.h> #endif -#if LUA_VERSION_NUM < 502 -#define lua_pushglobaltable(L) lua_pushvalue(L, LUA_GLOBALSINDEX) -#endif - #if defined(__cplusplus) extern "C" #endif diff --git a/Sluift/tokenize.cpp b/Sluift/tokenize.cpp new file mode 100644 index 0000000..b089cdb --- /dev/null +++ b/Sluift/tokenize.cpp @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#include <Sluift/tokenize.h> + +#include <boost/tokenizer.hpp> +#include <cctype> + +using namespace Swift; + +namespace { + struct LuaTokenizeFunctor { + void reset() { + } + + template<typename InputIterator, typename Token> + bool operator()(InputIterator& next, InputIterator& end, Token& result) { + while (next != end && std::isspace(*next)) { + ++next; + } + if (next == end) { + return false; + } + + std::vector<char> token; + char c = *next++; + token.push_back(c); + + // String literal + if (c == '\'' || c == '"') { + char quote = c; + bool inEscape = false; + for (; next != end; ++next) { + c = *next; + token.push_back(c); + if (inEscape) { + inEscape = false; + } + else if (c == '\\') { + inEscape = true; + } + else if (c == quote) { + break; + } + } + if (next != end) { + ++next; + } + } + // Identifier + else if (std::isalpha(c) || c == '_') { + while (next != end && (std::isalpha(*next) || *next == '_' || std::isdigit(*next))) { + token.push_back(*next); + ++next; + } + } + // Digit + else if (std::isdigit(c)) { + while (next != end && !std::isspace(*next)) { + token.push_back(*next); + ++next; + } + } + // Dots + else if (c == '.') { + while (next != end && *next == '.') { + token.push_back(*next); + ++next; + } + } + + result = Token(&token[0], token.size()); + return true; + } + }; +} + + +std::vector<std::string> Lua::tokenize(const std::string& input) { + boost::tokenizer<LuaTokenizeFunctor> tokenizer(input); + return std::vector<std::string>(tokenizer.begin(), tokenizer.end()); +} + diff --git a/Sluift/tokenize.h b/Sluift/tokenize.h new file mode 100644 index 0000000..6f09345 --- /dev/null +++ b/Sluift/tokenize.h @@ -0,0 +1,16 @@ +/* + * Copyright (c) 2013 Remko Tronçon + * Licensed under the GNU General Public License. + * See the COPYING file for more information. + */ + +#pragma once + +#include <vector> +#include <string> + +namespace Swift { + namespace Lua { + std::vector<std::string> tokenize(const std::string&); + } +} |