mysqlshdk/shellcore/shell_core.cc (421 lines of code) (raw):

/* * Copyright (c) 2014, 2025, Oracle and/or its affiliates. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License, version 2.0, * as published by the Free Software Foundation. * * This program is designed to work with certain software (including * but not limited to OpenSSL) that is licensed under separate terms, * as designated in a particular file or component or in included license * documentation. The authors of MySQL hereby grant you an additional * permission to link the program and your derivative works with the * separately licensed software that they have either included with * the program or referenced in the documentation. * * 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, version 2.0, 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 St, Fifth Floor, Boston, MA 02110-1301 USA */ #include "shellcore/shell_core.h" #include <fstream> #include <locale> #ifdef _WIN32 #include <windows.h> #else #include <unistd.h> #endif #include "mysqlshdk/include/shellcore/base_shell.h" #ifdef HAVE_JIT_EXECUTOR #include "mysqlshdk/include/shellcore/shell_polyglot.h" #include "mysqlshdk/scripting/polyglot/utils/polyglot_utils.h" #endif #include "mysqlshdk/include/shellcore/utils_help.h" #include "scripting/lang_base.h" #include "scripting/object_registry.h" #include "shellcore/base_session.h" #include "shellcore/shell_python.h" #include "shellcore/shell_sql.h" #include "utils/base_tokenizer.h" #include "utils/debug.h" #include "utils/utils_general.h" #include "utils/utils_string.h" using std::placeholders::_1; DEBUG_OBJ_ENABLE(Shell_core); REGISTER_HELP_TOPIC(Shell Commands, CATEGORY, commands, Contents, ALL); REGISTER_HELP(COMMANDS_BRIEF, "Provides details about the available built-in shell commands."); REGISTER_HELP(COMMANDS_DETAIL, "The shell commands allow executing specific operations " "including updating the shell configuration."); REGISTER_HELP(COMMANDS_CHILDS_DESC, "The following shell commands are available:"); REGISTER_HELP(COMMANDS_CLOSING, "For help on a specific command use <b>\\?</b> <command>"); REGISTER_HELP(COMMANDS_EXAMPLE, "<b>\\?</b> \\connect"); REGISTER_HELP(COMMANDS_EXAMPLE_DESC, "Displays information about the <b>\\connect</b> command."); namespace shcore { Shell_core::Shell_core(bool interactive) : IShell_core(), m_global_return_code(0), m_interactive(interactive) { DEBUG_OBJ_ALLOC(Shell_core); _mode = Mode::None; _registry = new Object_registry(); } Shell_core::~Shell_core() { DEBUG_OBJ_DEALLOC(Shell_core); _global_dev_session.reset(); delete _registry; // unset all globals from all interpreters for (const auto &g : _globals) { for (const auto &l : _langs) { if (g.second.first.is_set(l.first) && l.second) { l.second->set_global(g.first, shcore::Value()); } } } _globals.clear(); auto remove_language = [this](Mode m) { auto it = _langs.find(m); if (it != _langs.end()) { delete it->second; } }; remove_language(Mode::JavaScript); remove_language(Mode::Python); remove_language(Mode::SQL); } std::string Shell_core::preprocess_input_line(const std::string &s) { return _langs[_mode]->preprocess_input_line(s); } void Shell_core::handle_input(std::string &code, Input_state &state) { _langs[_mode]->handle_input(code, state); } void Shell_core::flush_input(const std::string &code) { _langs[_mode]->flush_input(code); } Input_state Shell_core::input_state() { return _langs[_mode]->input_state(); } std::string Shell_core::get_handled_input() { return _langs[_mode]->get_handled_input(); } void Shell_core::set_argv(const std::vector<std::string> &args) { return _langs[_mode]->set_argv(args); } /* * process_stream will process the content of an opened stream until EOF is * found. * * return values: * - 1 in case of any processing error is found. * - 0 if no processing errors were found. */ int Shell_core::process_stream(std::istream &stream, const std::string &source) { // NOTE: global return code is unused at the moment // return code should be determined at application level on // process_result this global return code may be used again once the // exit() function is in place Input_state state = Input_state::Ok; m_global_return_code = 0; _input_source = source; if (_mode == Shell_core::Mode::SQL) { if (!_langs[_mode]->handle_input_stream(&stream)) m_global_return_code = 1; } else { std::string data; if (&std::cin == &stream) { std::string line; while (!stream.eof()) { line.clear(); shcore::getline(stream, line); data.append(line).append("\n"); } } else { stream.seekg(0, stream.end); std::streamsize fsize = stream.tellg(); stream.seekg(0, stream.beg); data.resize(fsize); stream.read(const_cast<char *>(data.data()), fsize); } // When processing JavaScript files, validates the very first line to start // with #! If that's the case, it is replaced by a comment indicator // if (_mode == IShell_core::Mode::JavaScript && data.size() > 1 && data[0] == '#' && data[1] == '!') data.replace(0, 2, "//"); handle_input(data, state); } return m_global_return_code; } bool Shell_core::switch_mode(Mode mode) { assert(mode != Mode::None); // Updates the shell help mode m_help.set_mode(mode); if (_mode != mode) { Mode backup_mode = _mode; _mode = mode; try { init_mode(_mode); } catch (...) { _mode = backup_mode; throw; } return true; } return false; } void Shell_core::init_mode(Mode mode) { if (_langs.find(mode) == _langs.end()) { switch (mode) { case Mode::None: break; case Mode::SQL: init_sql(); break; case Mode::JavaScript: init_js(); break; case Mode::Python: init_py(); break; } } } void Shell_core::init_sql() { _langs[Mode::SQL] = new Shell_sql(this); } void Shell_core::init_js() { #ifdef HAVE_JS if (_langs.find(Mode::JavaScript) == _langs.end()) { Shell_polyglot *js; _langs[Mode::JavaScript] = js = new Shell_polyglot(this, polyglot::Language::JAVASCRIPT); for (const auto &global : _globals) { if (global.second.first.is_set(Mode::JavaScript)) js->set_global(global.first, global.second.second); } } #endif } void Shell_core::init_py() { #ifdef HAVE_PYTHON Shell_python *py; if (_langs.find(Mode::Python) == _langs.end()) { _langs[Mode::Python] = py = new Shell_python(this); for (std::map<std::string, std::pair<Mode_mask, Value>>::const_iterator iter = _globals.begin(); iter != _globals.end(); ++iter) { if (iter->second.first.is_set(Mode::Python)) py->set_global(iter->first, iter->second.second); } } #endif } void Shell_core::set_global(const std::string &name, const Value &value, Mode_mask mode) { _globals[name] = std::make_pair(mode, value); for (std::map<Mode, Shell_language *>::const_iterator iter = _langs.begin(); iter != _langs.end(); ++iter) { // Only sets the global where applicable if (mode.is_set(iter->first)) { iter->second->set_global(name, value); } } } bool Shell_core::is_global(const std::string &name) { return _globals.find(name) != _globals.end(); } Value Shell_core::get_global(const std::string &name) { return (_globals.count(name) > 0) ? _globals[name].second : Value(); } std::vector<std::string> Shell_core::get_global_objects(Mode mode) { std::vector<std::string> globals; for (auto entry : _globals) { if (entry.second.first.is_set(mode) && entry.second.second.type == shcore::Object) globals.push_back(entry.first); } return globals; } std::vector<std::string> Shell_core::get_all_globals() { std::vector<std::string> globals; for (auto entry : _globals) { if (entry.second.second.type == shcore::Object) globals.push_back(entry.first); } return globals; } void Shell_core::clear_input() { _langs[interactive_mode()]->clear_input(); } bool Shell_core::handle_shell_command(const std::string &line) { if (!_langs[_mode]->command_handler()->process(line)) { return m_command_handler.process(line); } return false; } size_t Shell_core::handle_inline_shell_command(const std::string &line) { size_t skip = _langs[_mode]->command_handler()->process_inline(line); if (skip == 0) { skip = m_command_handler.process_inline(line); } return skip; } /** * Configures the received session as the global development session. * \param session: The session to be set as global. * * If there's a selected schema on the received session, it will be made * available to the scripting interfaces on the global *db* variable */ std::shared_ptr<mysqlsh::ShellBaseSession> Shell_core::set_dev_session( const std::shared_ptr<mysqlsh::ShellBaseSession> &session) { _global_dev_session = session; return _global_dev_session; } /** * Returns the global development session. */ std::shared_ptr<mysqlsh::ShellBaseSession> Shell_core::get_dev_session() { return _global_dev_session; } std::string Shell_core::get_main_delimiter() const { auto it = _langs.find(Mode::SQL); if (it == _langs.end()) return ";"; return static_cast<Shell_sql *>(it->second)->get_main_delimiter(); } void Shell_core::execute_module(const std::string &module_name, const std::vector<std::string> &argv) { _langs[_mode]->execute_module(module_name, argv); } bool Shell_core::load_plugin(Mode mode, const Plugin_definition &plugin) { assert(mode != Mode::None); init_mode(mode); return _langs[mode]->load_plugin(plugin); } //------------------ COMMAND HANDLER FUNCTIONS ------------------// std::vector<std::string> Shell_command_handler::split_command_line( const std::string &command_line) { shcore::BaseTokenizer _tokenizer; static constexpr auto k_quote = "\""; _tokenizer.set_complex_token("escaped-quote", std::vector<std::string>{"\\", k_quote}); _tokenizer.set_complex_token("quote", k_quote); _tokenizer.set_complex_token_callback( "space", [](const std::string &input, size_t &index, std::string &text) -> bool { std::locale locale; while (std::isspace(input[index], locale)) text += input[index++]; return !text.empty(); }); _tokenizer.set_allow_unknown_tokens(true); _tokenizer.set_allow_spaces(true); _tokenizer.set_input(command_line); _tokenizer.process({0, command_line.length()}); std::vector<std::string> ret_val; std::string param; const auto too_many_quotes = [](std::size_t length) { return shcore::Exception::runtime_error( "Too many consecutive quote characters: " + std::to_string(length)); }; const auto missing_space = []() { return shcore::Exception::runtime_error( "Expected space after closing quote character"); }; while (_tokenizer.tokens_available()) { if (_tokenizer.cur_token_type_is("quote")) { // quoted parameter // eat quotes auto quote = _tokenizer.consume_any_token().get_text(); if (quote.length() == 1) { // only one quote character, beginning of a quoted parameter param = quote; // read till the end of string or till next token is a quote while (_tokenizer.tokens_available() && !_tokenizer.cur_token_type_is("quote")) { param += _tokenizer.consume_any_token().get_text(); } if (!_tokenizer.tokens_available()) { // no tokens left, quote was no closed throw shcore::Exception::runtime_error( "Missing closing quotes on command parameter"); } else { // eat quotes quote = _tokenizer.consume_any_token().get_text(); if (quote.length() == 1) { // only one quote character if (!_tokenizer.tokens_available() || _tokenizer.cur_token_type_is("space")) { // string has finished or quoted parameter was followed by space // -> end of quoted parameter param += quote; ret_val.emplace_back(unquote_string(param, k_quote[0])); } else { // quote was not followed by a space, two parameters were not // properly separated throw missing_space(); } } else { // two or more quote characters -> error throw too_many_quotes(quote.length()); } } } else if (quote.length() == 2) { // two quote characters, this has to be an empty parameter if (!_tokenizer.tokens_available() || _tokenizer.cur_token_type_is("space")) { // string has finished or empty parameter was followed by space ret_val.emplace_back(); } else { // no space after double quotes, two parameters were not properly // separated throw missing_space(); } } else { // multiple quote characters -> error throw too_many_quotes(quote.length()); } } else if (_tokenizer.cur_token_type_is("space")) { // space(s) separating parameters _tokenizer.consume_any_token(); } else { // unquoted parameter param.clear(); while (_tokenizer.tokens_available() && !_tokenizer.cur_token_type_is("space")) { if (_tokenizer.cur_token_type_is("quote")) { // quotes are not allowed in an unquoted string throw shcore::Exception::runtime_error( "Unexpected quote token in an unquoted sequence"); } else if (_tokenizer.cur_token_type_is("escaped-quote")) { // escaped quotes need to have the '\' character removed const auto quotes = _tokenizer.consume_any_token().get_text(); param += std::string(quotes.length() / 2, k_quote[0]); } else { param += _tokenizer.consume_any_token().get_text(); } } ret_val.emplace_back(param); } } return ret_val; } bool Shell_command_handler::process(const std::string &command_line) { bool ret_val = false; std::vector<std::string> tokens; if (!_command_dict.empty()) { std::locale locale; // Identifies if the line is a registered command size_t index = 0; while (index < command_line.size() && std::isspace(command_line[index], locale)) index++; size_t start = index; while (index < command_line.size() && !std::isspace(command_line[index], locale)) index++; std::string command = command_line.substr(start, index - start); // Srearch on the registered command list and processes it if it exists Command_registry::iterator item = _command_dict.find(command); if (item != _command_dict.end() && item->second->function) { // Parses the command if (item->second->auto_parse_arguments) tokens = split_command_line(command_line); else tokens.resize(1); // Updates the first element to contain the whole command line tokens[0] = command_line; ret_val = item->second->function(tokens); } } return ret_val; } size_t Shell_command_handler::process_inline(const std::string &command) { auto cmd = _command_dict.find(command.substr(0, 2)); if (cmd != _command_dict.end()) { // currently there are no inline \X commands that accept params, but when // they get added, they should be normalized and still return the number // of consumed chars from the original input bool has_params = false; if (!has_params) { if (process(command.substr(0, 2))) return 2; } else { if (process(command.substr(0, 2) + " " + command.substr(3))) return command.size(); } } return 0; } void Shell_command_handler::add_command(const std::string &triggers, const std::string &help_tag, Shell_command_function function, bool case_sensitive_help, Mode_mask mode, bool auto_parse_arguments) { Shell_command command = {triggers, function, auto_parse_arguments}; _commands.push_back(command); std::vector<std::string> tokens; tokens = split_string(triggers, "|", true); std::vector<std::string>::iterator index = tokens.begin(), end = tokens.end(); // Inserts a mapping for each given token while (index != end) { _command_dict.insert(std::pair<const std::string &, Shell_command *>( *index, &_commands.back())); index++; } if (m_use_help) { // Verifies if the command is already registered to avoid double entry auto topics = Help_registry::get()->search_topics(tokens[0], mode, case_sensitive_help); if (topics.empty()) { std::vector<Help_topic *> new_topics = Help_registry::get()->add_help_topic(tokens[0], shcore::Topic_type::COMMAND, help_tag, "Commands", mode); // If case insensitive, first trigger is already registered if (!case_sensitive_help) tokens.erase(tokens.begin()); for (auto &token : tokens) { for (auto &topic : new_topics) { Help_registry::get()->register_keyword(token, mode, topic, case_sensitive_help); } } // If case sensitive, we need now to remove the first trigger if (case_sensitive_help) tokens.erase(tokens.begin()); if (!tokens.empty()) { std::string alias = "(" + shcore::str_join(tokens, ",") + ")"; Help_registry::get()->add_help(help_tag + "_ALIAS", alias); } } } } std::vector<std::string> Shell_command_handler::get_command_names_matching( const std::string &prefix) const { std::vector<std::string> names; for (auto cmd : _commands) { std::vector<std::string> tokens; tokens = split_string(cmd.triggers, "|", true); if (!tokens.empty()) { if (shcore::str_beginswith(tokens.front(), prefix)) { names.push_back(tokens.front()); } } } return names; } } // namespace shcore