diff --git a/modules/terminal/impl/session_context.h b/modules/terminal/impl/session_context.h index 186c49b7e01d80ef38d1e254cd6897e8aa5c81da..8a94ed8bbd61b2a126f003a1c509a5c52c9ae25e 100644 --- a/modules/terminal/impl/session_context.h +++ b/modules/terminal/impl/session_context.h @@ -35,7 +35,7 @@ struct SessionContext { uint32_t options = 0; std::string curr_input; - size_t cursor = 0; + size_t cursor_pos = 0; Path path; //! 当前路径 std::deque history; //! 历史命令 diff --git a/modules/terminal/impl/terminal.h b/modules/terminal/impl/terminal.h index 445b9a37eb3df80e19b59b2d0fb79a84254314f2..450b47a698ba153c481a00a09c6893d74939ef5a 100644 --- a/modules/terminal/impl/terminal.h +++ b/modules/terminal/impl/terminal.h @@ -54,7 +54,7 @@ class Terminal::Impl { NodeToken createDirNode(const std::string &help); bool deleteNode(NodeToken node_token); NodeToken rootNode() const { return root_token_; } - NodeToken findNode(const std::string &path) const; + NodeToken findNode(const std::string &path_str) const; bool mountNode(const NodeToken &parent, const NodeToken &child, const std::string &name); bool umountNode(const NodeToken &parent, const std::string &name); @@ -90,7 +90,9 @@ class Terminal::Impl { bool executeRunHistoryCmd(SessionContext *s, const Args &args); void executeUserCmd(SessionContext *s, const Args &args); - bool findNode(const std::string &path, Path &node_path) const; + bool findNode(const std::string &path_str, Path &node_path) const; + //! 从指定会话的当前路径寻找path_str对像的结点;如果不指定,则从根目录寻找 + NodeToken findNode(const std::string &path_str, SessionContext *s) const; private: event::Loop *wp_loop_ = nullptr; diff --git a/modules/terminal/impl/terminal_commands.cpp b/modules/terminal/impl/terminal_commands.cpp index 748fcab1162e19d85e1dba5a4087566434ce79ba..56e8e0ee77a27649e0f05913ce3e11382780d136 100644 --- a/modules/terminal/impl/terminal_commands.cpp +++ b/modules/terminal/impl/terminal_commands.cpp @@ -94,7 +94,7 @@ void Terminal::Impl::executeCdCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -102,17 +102,17 @@ void Terminal::Impl::executeCdCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << path_str << "' node has been deleted." << "\r\n"; + oss << "Error: '" << path_str << "' node has been deleted." << "\r\n"; } else if (top_node->type() == NodeType::kDir) { s->path = node_path; } else { - ss << "Error: '" << path_str << "' not directory." << "\r\n"; + oss << "Error: '" << path_str << "' not directory." << "\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) @@ -122,7 +122,7 @@ void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) return; } - stringstream ss; + ostringstream oss; string path_str = args[1]; auto node_path = s->path; @@ -131,15 +131,15 @@ void Terminal::Impl::executeHelpCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node != nullptr) { - ss << top_node->help() << "\r\n"; + oss << top_node->help() << "\r\n"; } else { - ss << "Error: '" << path_str << "' node has been deleted.\r\n"; + oss << "Error: '" << path_str << "' node has been deleted.\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) @@ -148,7 +148,7 @@ void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -156,40 +156,40 @@ void Terminal::Impl::executeLsCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << path_str << "' node has been deleted.\r\n"; + oss << "Error: '" << path_str << "' node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kDir) { auto top_dir_node = static_cast(top_node); vector node_info_vec; top_dir_node->children(node_info_vec); for (auto item : node_info_vec) { - ss << "- " << item.name; + oss << "- " << item.name; auto node = nodes_.at(item.token); if (node == nullptr) { - ss << "(X)"; + oss << "(X)"; } else if (node->type() == NodeType::kDir) - ss << '/'; - ss << "\r\n"; + oss << '/'; + oss << "\r\n"; } - ss << "\r\n"; + oss << "\r\n"; } else { - ss << path_str << " is function" << ".\r\n"; + oss << path_str << " is function" << ".\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeHistoryCmd(SessionContext *s, const Args &) { - stringstream ss; + ostringstream oss; for (size_t i = 0; i < s->history.size(); ++i) { const auto &cmd = s->history.at(i); - ss << setw(2) << i << " " << cmd << "\r\n"; + oss << setw(2) << i << " " << cmd << "\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executeExitCmd(SessionContext *s, const Args &) @@ -212,7 +212,7 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) if (args.size() >= 2) path_str = args[1]; - stringstream ss; + ostringstream oss; auto node_path = s->path; bool is_found = findNode(path_str, node_path); @@ -221,7 +221,7 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << node_path.back().first << " node has been deleted.\r\n"; + oss << node_path.back().first << " node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kDir) { vector> node_token_stack; //!< 遍历栈 string indent_str; //!< 缩进字串 @@ -257,15 +257,15 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) const char *curr_indent_str = is_last_node ? "`-- " : "|-- "; auto &curr_node_info = last_level.front(); - ss << indent_str << curr_indent_str << curr_node_info.name; + oss << indent_str << curr_indent_str << curr_node_info.name; auto curr_node = nodes_.at(curr_node_info.token); if (curr_node == nullptr) { //! 如果已被删除了的结点,显示(X) - ss << "(X)\r\n"; + oss << "(X)\r\n"; } else if (curr_node->type() == NodeType::kFunc) { //! 如果是Func,就打印一下名称就可以了 - ss << "\r\n"; + oss << "\r\n"; } else if (curr_node->type() == NodeType::kDir) { //! 如果是Dir,则需要再深入地遍历其子Node //! 首先需要查重,防止循环路径引起的死循环 @@ -276,9 +276,9 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) } if (is_repeat) - ss << "(R)"; + oss << "(R)"; - ss << "\r\n"; + oss << "\r\n"; if (!is_repeat) { //! 找出该Dir下所有的子Node。 @@ -307,26 +307,26 @@ void Terminal::Impl::executeTreeCmd(SessionContext *s, const Args &args) } } } else { - ss << node_path.back().first << " is a function.\r\n"; + oss << node_path.back().first << " is a function.\r\n"; } } else { - ss << "Error: cannot access '" << path_str << "'.\r\n"; + oss << "Error: cannot access '" << path_str << "'.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } void Terminal::Impl::executePwdCmd(SessionContext *s, const Args &) { - stringstream ss; - ss << '/'; + ostringstream oss; + oss << '/'; for (size_t i = 0; i < s->path.size(); ++i) { - ss << s->path.at(i).first; + oss << s->path.at(i).first; if ((i + 1) != s->path.size()) - ss << '/'; + oss << '/'; } - ss << "\r\n"; - s->wp_conn->send(s->token, ss.str()); + oss << "\r\n"; + s->wp_conn->send(s->token, oss.str()); } bool Terminal::Impl::executeRunHistoryCmd(SessionContext *s, const Args &args) @@ -368,7 +368,7 @@ bool Terminal::Impl::executeRunHistoryCmd(SessionContext *s, const Args &args) void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) { - stringstream ss; + ostringstream oss; const auto &cmd = args[0]; auto node_path = s->path; @@ -378,7 +378,7 @@ void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) auto top_node_token = node_path.empty() ? root_token_ : node_path.back().second; auto top_node = nodes_.at(top_node_token); if (top_node == nullptr) { - ss << "Error: '" << cmd << "' node has been deleted.\r\n"; + oss << "Error: '" << cmd << "' node has been deleted.\r\n"; } else if (top_node->type() == NodeType::kFunc) { auto top_func_node = static_cast(top_node); Session session(s->wp_conn, s->token); @@ -387,10 +387,10 @@ void Terminal::Impl::executeUserCmd(SessionContext *s, const Args &args) s->path = node_path; } } else { - ss << "Error: '" << cmd << "' not found.\r\n"; + oss << "Error: '" << cmd << "' not found.\r\n"; } - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } diff --git a/modules/terminal/impl/terminal_key_events.cpp b/modules/terminal/impl/terminal_key_events.cpp index dc49d1c1fb654be225b2c181ae0ff25098901492..3d00f6e3935f6d2c96171ca2e20fa7b902a47312 100644 --- a/modules/terminal/impl/terminal_key_events.cpp +++ b/modules/terminal/impl/terminal_key_events.cpp @@ -22,6 +22,7 @@ #include #include +#include #include "session_context.h" #include "dir_node.h" @@ -37,24 +38,25 @@ namespace { const string MOVE_LEFT_KEY("\033[D"); const string MOVE_RIGHT_KEY("\033[C"); const size_t HISTORY_MAX_SIZE(20); + const char * SPACE_CHARS = " \t"; } void Terminal::Impl::onChar(SessionContext *s, char ch) { - if (s->cursor == s->curr_input.size()) + if (s->cursor_pos == s->curr_input.size()) s->curr_input.push_back(ch); else - s->curr_input.insert(s->cursor, 1, ch); - s->cursor++; + s->curr_input.insert(s->cursor_pos, 1, ch); + s->cursor_pos++; if (s->options & kEnableEcho) { s->wp_conn->send(s->token, ch); - stringstream ss; - ss << s->curr_input.substr(s->cursor) - << string((s->curr_input.size() - s->cursor), '\b'); + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) + << string((s->curr_input.size() - s->cursor_pos), '\b'); - auto refresh_str = ss.str(); + auto refresh_str = oss.str(); if (!refresh_str.empty()) s->wp_conn->send(s->token, refresh_str); } @@ -74,61 +76,205 @@ void Terminal::Impl::onEnterKey(SessionContext *s) printPrompt(s); s->curr_input.clear(); - s->cursor = 0; + s->cursor_pos = 0; s->history_index = 0; } void Terminal::Impl::onBackspaceKey(SessionContext *s) { - if (s->cursor == 0) + if (s->cursor_pos == 0) return; - if (s->cursor == s->curr_input.size()) + if (s->cursor_pos == s->curr_input.size()) s->curr_input.pop_back(); else - s->curr_input.erase((s->cursor - 1), 1); + s->curr_input.erase((s->cursor_pos - 1), 1); - s->cursor--; + s->cursor_pos--; if (s->options & kEnableEcho) { - stringstream ss; - ss << '\b' << s->curr_input.substr(s->cursor) << ' ' - << string((s->curr_input.size() - s->cursor + 1), '\b'); + ostringstream oss; + oss << '\b' << s->curr_input.substr(s->cursor_pos) << ' ' + << string((s->curr_input.size() - s->cursor_pos + 1), '\b'); - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } void Terminal::Impl::onDeleteKey(SessionContext *s) { - if (s->cursor >= s->curr_input.size()) + if (s->cursor_pos >= s->curr_input.size()) return; - s->curr_input.erase((s->cursor), 1); + s->curr_input.erase((s->cursor_pos), 1); if (s->options & kEnableEcho) { - stringstream ss; - ss << s->curr_input.substr(s->cursor) << ' ' - << string((s->curr_input.size() - s->cursor + 1), '\b'); + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) << ' ' + << string((s->curr_input.size() - s->cursor_pos + 1), '\b'); - s->wp_conn->send(s->token, ss.str()); + s->wp_conn->send(s->token, oss.str()); } } -void Terminal::Impl::onTabKey(SessionContext *) +/** + * 仅实现对Node的补全,不实现参数的补全 + */ +void Terminal::Impl::onTabKey(SessionContext *s) { - //!TODO: 实现补全功能 + //! 如果当前用户没有任何输入,则不进行补全 + if (s->curr_input.empty() || s->cursor_pos == 0) + return; + + //! 找出当前光标下正在输入的命令的起始位置 + size_t start_pos = 0; + { + /** + * 针对单行多指令的情况:"set_a 12; get_ " + * ^ + * 要补全的是get_,而不是set_a + */ + + //! 尝试从光标所在处往前寻找分号。如果是找到,则将start_pos修正为分号后一位 + auto semicolon_pos = s->curr_input.find_last_of(';', s->cursor_pos - 1); + if (semicolon_pos != std::string::npos) + start_pos = semicolon_pos + 1; + + //! 略过左边空格,找到有效字符起始位置 + start_pos = s->curr_input.find_first_not_of(SPACE_CHARS, start_pos); + + /** + * 如果没有找到或找到的位置在光标之后,说明无效,则退出 + * 如:" set_a 12" + * ^ + */ + if (start_pos == std::string::npos || start_pos >= s->cursor_pos) + return; + } + + const auto cmd_str = s->curr_input.substr(start_pos, s->cursor_pos - start_pos); + + if (cmd_str.empty()) + return; + + /** + * 检查从start_pos到s->cursor_pos之间有没有空格 + * 如果有则说明当前用户输入的是参数,不是命令,不进行补全 + * 如:"set_value 12" + * ^ + */ + { + auto space_pos = cmd_str.find_first_of(SPACE_CHARS); + if (space_pos != std::string::npos) + return; + } + + //! 分离路径和节点名 + size_t last_slash_pos = cmd_str.rfind('/'); + string dir_path; + string prefix; + + if (last_slash_pos != string::npos) { + dir_path = cmd_str.substr(0, last_slash_pos); + prefix = cmd_str.substr(last_slash_pos + 1); + } else { + dir_path = "."; + prefix = cmd_str; + } + + //! 查找路径对应的节点 + NodeToken dir_token = findNode(dir_path, s); + if (dir_token.isNull()) + return; + + //! 获取所有匹配的子节点对象 + Node* base_node = nodes_.at(dir_token); + if (base_node == nullptr) + return; + + vector matched_node_name_vec; + if (auto dir_node = dynamic_cast(base_node)) { + vector children_info_vec; + dir_node->children(children_info_vec); + + //! 遍历dir_node下有所有子结点,找出匹配得上的,加入到matched_node_name_vec中 + for (const auto& child_info : children_info_vec) { + if (util::string::IsStartWith(child_info.name, prefix)) + matched_node_name_vec.push_back(child_info.name); + } + } + + //! 如果没有找到匹配项,直接结束 + if (matched_node_name_vec.empty()) + return; + + string completion; //! 补全内容 + + //! 如果只有一个匹配,直接补全 + if (matched_node_name_vec.size() == 1) { + const auto &match_node_name = matched_node_name_vec.back(); + completion = match_node_name.substr(prefix.length()); //! 需要补全的字串 + + //! 如果不需要补全,则结束 + if (completion.empty()) + return; + + //! 检查是否为目录,如果是则添加/ + NodeToken child_token = findNode(dir_path + "/" + match_node_name, s); + if (!child_token.isNull()) { + Node* child_node = nodes_.at(child_token); + if (child_node && child_node->type() == NodeType::kDir) { + completion.push_back('/'); + } + } + + } else { + //! 多个匹配,显示所有可能 + ostringstream oss; + oss << "\r\n"; + for (const auto& match : matched_node_name_vec) + oss << match << "\t"; + oss << "\r\n"; + + if (s->options & kEnableEcho) { + s->wp_conn->send(s->token, oss.str()); + printPrompt(s); + s->wp_conn->send(s->token, s->curr_input); + } + + //! 找出最大共同字串,设置补全内容 + std::string common_prefix = util::string::ExtractCommonPrefix(matched_node_name_vec); + completion = common_prefix.substr(prefix.length()); //! 需要补全的字串 + } + + //! 如果需要补全,则进行补全 + if (!completion.empty()) { + s->curr_input.insert(s->cursor_pos, completion); + s->cursor_pos += completion.length(); + + if (s->options & kEnableEcho) { + s->wp_conn->send(s->token, completion); + + ostringstream oss; + oss << s->curr_input.substr(s->cursor_pos) + << string((s->curr_input.size() - s->cursor_pos), '\b'); + + auto refresh_str = oss.str(); + if (!refresh_str.empty()) + s->wp_conn->send(s->token, refresh_str); + } + } } namespace { void CleanupInput(SessionContext *s) { - while (s->cursor < s->curr_input.size()) { + while (s->cursor_pos < s->curr_input.size()) { s->wp_conn->send(s->token, MOVE_RIGHT_KEY); - s->cursor++; + s->cursor_pos++; } - while (s->cursor--) + while (s->cursor_pos--) s->wp_conn->send(s->token, "\b \b"); } } @@ -142,7 +288,7 @@ void Terminal::Impl::onMoveUpKey(SessionContext *s) s->history_index++; s->curr_input = s->history[s->history.size() - s->history_index]; - s->cursor = s->curr_input.size(); + s->cursor_pos = s->curr_input.size(); s->wp_conn->send(s->token, s->curr_input); } @@ -157,10 +303,10 @@ void Terminal::Impl::onMoveDownKey(SessionContext *s) s->history_index--; if (s->history_index > 0) { s->curr_input = s->history[s->history.size() - s->history_index]; - s->cursor = s->curr_input.size(); + s->cursor_pos = s->curr_input.size(); } else { s->curr_input.clear(); - s->cursor = 0; + s->cursor_pos = 0; } s->wp_conn->send(s->token, s->curr_input); @@ -168,35 +314,35 @@ void Terminal::Impl::onMoveDownKey(SessionContext *s) void Terminal::Impl::onMoveLeftKey(SessionContext *s) { - if (s->cursor == 0) + if (s->cursor_pos == 0) return; - s->cursor--; + s->cursor_pos--; s->wp_conn->send(s->token, MOVE_LEFT_KEY); } void Terminal::Impl::onMoveRightKey(SessionContext *s) { - if (s->cursor >= s->curr_input.size()) + if (s->cursor_pos >= s->curr_input.size()) return; - s->cursor++; + s->cursor_pos++; s->wp_conn->send(s->token, MOVE_RIGHT_KEY); } void Terminal::Impl::onHomeKey(SessionContext *s) { - while (s->cursor != 0) { + while (s->cursor_pos != 0) { s->wp_conn->send(s->token, MOVE_LEFT_KEY); - s->cursor--; + s->cursor_pos--; } } void Terminal::Impl::onEndKey(SessionContext *s) { - while (s->cursor < s->curr_input.size()) { + while (s->cursor_pos < s->curr_input.size()) { s->wp_conn->send(s->token, MOVE_RIGHT_KEY); - s->cursor++; + s->cursor_pos++; } } diff --git a/modules/terminal/impl/terminal_nodes.cpp b/modules/terminal/impl/terminal_nodes.cpp index 03fb807d4f0a94c37636dfd95e51d96661338ed0..67a6fcc9c83cca5779c6e0bee61eb21e0a463b9e 100644 --- a/modules/terminal/impl/terminal_nodes.cpp +++ b/modules/terminal/impl/terminal_nodes.cpp @@ -54,17 +54,10 @@ bool Terminal::Impl::deleteNode(NodeToken node_token) } } +//! 从根结点开始寻找 NodeToken Terminal::Impl::findNode(const string &path_str) const { - Path node_path; - if (findNode(path_str, node_path)) { - if (node_path.empty()) - return root_token_; - else - return node_path.back().second; - } else { - return NodeToken(); - } + return findNode(path_str, nullptr); } bool Terminal::Impl::mountNode(const NodeToken &parent, const NodeToken &child, const string &name) @@ -146,5 +139,22 @@ bool Terminal::Impl::findNode(const string &path_str, Path &node_path) const return true; } +NodeToken Terminal::Impl::findNode(const std::string &path_str, SessionContext *s) const +{ + Path node_path; + + if (s != nullptr) + node_path = s->path; + + if (findNode(path_str, node_path)) { + if (node_path.empty()) + return root_token_; + else + return node_path.back().second; + } else { + return NodeToken(); + } +} + } } diff --git a/modules/terminal/terminal_nodes.h b/modules/terminal/terminal_nodes.h index 1516d93a7059166a596cae6e543414b928a747e3..d2ebcd5c71c9d00214eee747622fd50e4539d974 100644 --- a/modules/terminal/terminal_nodes.h +++ b/modules/terminal/terminal_nodes.h @@ -41,7 +41,7 @@ class TerminalNodes { //! 获取根结点 virtual NodeToken rootNode() const = 0; //! 根据路径查找结点 - virtual NodeToken findNode(const std::string &path) const = 0; + virtual NodeToken findNode(const std::string &path_str) const = 0; //! 将子指定结点挂载到指定父目录结点,并指定名称 virtual bool mountNode(const NodeToken &parent, const NodeToken &child, const std::string &name) = 0; diff --git a/modules/util/string.cpp b/modules/util/string.cpp index c662ea296c0dbd43ca4b9ae6eb4e1d55e76cdedf..0dece2b3db81152456b9bd155ff07e33c74598cf 100644 --- a/modules/util/string.cpp +++ b/modules/util/string.cpp @@ -289,6 +289,26 @@ bool IsEndWith(const std::string &origin_str, const std::string &text) return origin_str.find(text, (origin_str.length() - text.length())) != std::string::npos; } +std::string ExtractCommonPrefix(const std::vector &str_vec) +{ + std::string common_prefix; + auto &first_str = str_vec.front(); + //! 从第一个字串中提取每一个字符,与其它字串对应位置的字符进行比对 + //! 全对应得上,则为公共字符 + for (size_t i = 0; i < first_str.size(); ++i) { + auto ch = first_str[i]; + for (size_t j = 1; j < str_vec.size(); ++j) { + auto &test_str = str_vec[j]; + //! 如果检测字串不够长或者对应不上,则终止 + if (test_str.size() == i || test_str[i] != ch) + return common_prefix; + } + common_prefix.push_back(ch); + } + + return common_prefix; +} + } } } diff --git a/modules/util/string.h b/modules/util/string.h index 6e0eae5224af05422e9c37f7f49aa6d17bb24871..1b593171c18ed3bf70c9859754972638b1cd6e8d 100644 --- a/modules/util/string.h +++ b/modules/util/string.h @@ -172,6 +172,15 @@ bool IsStartWith(const std::string &origin_str, const std::string &text); */ bool IsEndWith(const std::string &origin_str, const std::string &text); +/** + * \brief 提取多个字串的共同前缀 + * + * \param str_vec 字串数组 + * + * \return 共同前缀字串 + */ +std::string ExtractCommonPrefix(const std::vector &str_vec); + } } } diff --git a/modules/util/string_test.cpp b/modules/util/string_test.cpp index 96f16a6bc95e0aeb05bd3330adaf4bb3256db678..1910621e874dfbb30d671fc18d72cde7d6e26329 100644 --- a/modules/util/string_test.cpp +++ b/modules/util/string_test.cpp @@ -296,3 +296,18 @@ TEST(string, IsEndWith) { EXPECT_FALSE(IsEndWith("abc", "abcd")); } +TEST(string, ExtractCommonPrefix) { + { + std::vector str_vec = {"abc123", "abc113", "abd123"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "ab"); + } + { + std::vector str_vec = {"a", "abc113"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "a"); + } + { + std::vector str_vec = {"abc123", "abc1"}; + EXPECT_EQ(ExtractCommonPrefix(str_vec), "abc1"); + } +} + diff --git a/version.mk b/version.mk index 3396e9c4e8e078c39cbdfafaac4083156090b583..3b9045ef7df85e2be1d8eaa2f522a64758f6e2c5 100644 --- a/version.mk +++ b/version.mk @@ -21,4 +21,4 @@ # TBOX版本号 TBOX_VERSION_MAJOR := 1 TBOX_VERSION_MINOR := 12 -TBOX_VERSION_REVISION := 20 +TBOX_VERSION_REVISION := 22