#include "CmdHelper.hpp" namespace Unvirt::CmdHelper { #pragma region CmdSplitter const std::deque& CmdSplitter::GetResult() const { if (!m_ValidResult) throw std::runtime_error("try to get result from an invalid CmdSplitter."); return m_Result; } bool CmdSplitter::Convert(const std::u8string& u8cmd) { // Clear variables m_ValidResult = false; m_Result.clear(); m_Buffer.clear(); m_CurrentChar = u8'\0'; m_State = m_PrevState = StateType::SPACE; // split for (char8_t c : u8cmd) { m_CurrentChar = c; // skip all invalid characters (ascii code unit lower than space char) // thus UTF8 code unit can directly accepted. if (m_CurrentChar < u8' ') continue; switch (m_State) { case StateType::SPACE: ProcSpace(); break; case StateType::SINGLE: ProcSingle(); break; case StateType::DOUBLE: ProcDouble(); break; case StateType::ESCAPE: ProcEscape(); break; case StateType::NORMAL: ProcNormal(); break; } } // final proc bool is_success = false; switch (m_State) { case StateType::SPACE: is_success = true; break; case StateType::NORMAL: // push the last one m_Result.emplace_back(m_Buffer); is_success = true; break; case StateType::SINGLE: case StateType::DOUBLE: case StateType::ESCAPE: // error is_success = false; break; default: throw std::runtime_error("unreachable code."); } // check success if (is_success) { m_ValidResult = true; return true; } else { m_Result.clear(); return false; } } void CmdSplitter::ProcSpace() { switch (m_CurrentChar) { case u8'\'': m_State = StateType::SINGLE; break; case u8'"': m_State = StateType::DOUBLE; break; case u8'\\': m_State = StateType::ESCAPE; m_PrevState = StateType::NORMAL; break; case u8' ': break; // skip blank default: m_Buffer.push_back(m_CurrentChar); m_State = StateType::NORMAL; break; } } void CmdSplitter::ProcSingle() { switch (m_CurrentChar) { case u8'\'': m_State = StateType::NORMAL; break; case u8'"': m_Buffer.push_back('"'); break; case u8'\\': m_State = StateType::ESCAPE; m_PrevState = StateType::SINGLE; break; case u8' ': m_Buffer.push_back(u8' '); break; default: m_Buffer.push_back(m_CurrentChar); break; } } void CmdSplitter::ProcDouble() { switch (m_CurrentChar) { case u8'\'': m_Buffer.push_back(u8'\''); break; case u8'"': m_State = StateType::NORMAL; break; case u8'\\': m_State = StateType::ESCAPE; m_PrevState = StateType::DOUBLE; break; case u8' ': m_Buffer.push_back(u8' '); break; default: m_Buffer.push_back(m_CurrentChar); break; } } void CmdSplitter::ProcEscape() { // add itself m_Buffer.push_back(m_CurrentChar); // restore state m_State = m_PrevState; } void CmdSplitter::ProcNormal() { switch (m_CurrentChar) { case u8'\'': m_Buffer.push_back(u8'\''); break; case u8'"': m_Buffer.push_back(u8'"'); break; case u8'\\': m_State = StateType::ESCAPE; m_PrevState = StateType::NORMAL; break; case u8' ': m_Result.emplace_back(m_Buffer); m_Buffer.clear(); m_State = StateType::SPACE; break; default: m_Buffer.push_back(m_CurrentChar); break; } } #pragma endregion #pragma region Arguments Map ArgumentsMap::ArgumentsMap() : m_Data() {} ArgumentsMap::~ArgumentsMap() {} #pragma endregion #pragma region Help Document HelpDocument::HelpDocument() : m_Stack(), m_Results() {} HelpDocument::~HelpDocument() {} HelpDocument::StackItem::StackItem() : m_Name(), m_Desc() {} HelpDocument::StackItem::StackItem(const std::u8string& name, const std::u8string& desc) : m_Name(name), m_Desc(desc) {} HelpDocument::ResultItem::ResultItem() : m_CmdDesc(), m_ArgDesc() {} HelpDocument::ResultItem::ResultItem(const std::u8string& cmd_desc, const std::deque& arg_desc) : m_CmdDesc(cmd_desc), m_ArgDesc(arg_desc.begin(), arg_desc.end()) {} void HelpDocument::Push(const std::u8string& arg_name, const std::u8string& arg_desc) { m_Stack.emplace_back(StackItem { arg_name, arg_desc }); } void HelpDocument::Pop() { if (m_Stack.empty()) throw std::runtime_error("try pop back on an empty help document."); m_Stack.pop_back(); } void HelpDocument::Terminate(std::u8string& command_desc) { // create new result ResultItem result(command_desc, this->m_Stack); // add into result m_Results.emplace_back(std::move(result)); } void HelpDocument::Print() { for (auto& cmd : m_Results) { // syntax YYCC::ConsoleHelper::WriteLine(u8"Syntax: "); for (const auto& arg : cmd.m_ArgDesc) { YYCC::ConsoleHelper::Format(u8"%s ", arg.m_Name.c_str()); } YYCC::ConsoleHelper::WriteLine(u8""); // command description if (!cmd.m_CmdDesc.empty()) { YYCC::ConsoleHelper::FormatLine(u8"Description: %s", cmd.m_CmdDesc.c_str()); } // argument description YYCC::ConsoleHelper::WriteLine(u8"Arguments:"); for (auto& arg : cmd.m_ArgDesc) { if (!arg.m_Desc.empty()) { YYCC::ConsoleHelper::FormatLine(u8"\t%s: %s", arg.m_Name.c_str(), arg.m_Desc.c_str()); } } // space between each commands YYCC::ConsoleHelper::WriteLine(u8""); } } #pragma endregion namespace Nodes { #pragma region Abstract Node AbstractNode::AbstractNode() : m_Execution(nullptr), m_Comment(), m_Nodes() {} AbstractNode::~AbstractNode() {} AbstractNode& AbstractNode::Executes(FctExecution_t fct, const std::u8string_view& exec_desc) { if (m_Execution != nullptr) throw std::invalid_argument("you should not assign execution multiuple times."); if (fct == nullptr) throw std::invalid_argument("the function passed for executing should not be nullptr."); m_Execution = fct; m_ExecutionDesc = exec_desc; return *this; } AbstractNode& AbstractNode::Comment(const std::u8string_view& comment) { m_Comment = comment; return *this; } void AbstractNode::Help(HelpDocument& doc) { // Push self symbol to help document stack. doc.Push(GetHelpSymbol(), m_Comment); // Check whether this node is terminal. // If it is, terminate it once. if (m_Execution != nullptr) { doc.Terminate(m_ExecutionDesc); } // Then process its children nodes. for (auto& node : m_Nodes) { node->Help(doc); } // Pop self from help document stack doc.Pop(); } bool AbstractNode::Consume(CmdSplitter::Result_t& al, ArgumentsMap& am) { // if no data can consume, return if (al.empty()) return false; // backup current value std::u8string cur_cmd = al.front(); // consume self if (!BeginConsume(cur_cmd, am)) { // fail to consume self. not matched. return return false; } // pop front for processing child nodes. al.pop_front(); #define CONSUME_DEFER \ al.emplace_front(cur_cmd); \ EndConsume(am); if (al.empty()) { // if no more data for parsing. // this is must be a terminal. // check whether we have execution. if (m_Execution == nullptr) { CONSUME_DEFER; return false; } else { m_Execution(am); CONSUME_DEFER; return true; } } else { // still have data to be parsed. try to match them. // iterate node list to find the real terminal // however, we need iterate literal and choice first, the iterate argument. for (auto& node : m_Nodes) { if (node->IsArgument()) continue; if (node->Consume(al, am)) { CONSUME_DEFER; return true; } } for (auto& node : m_Nodes) { if (!node->IsArgument()) continue; if (node->Consume(al, am)) { CONSUME_DEFER; return true; } } // if still nothing to match, return false CONSUME_DEFER; return false; } #undef CONSUME_DEFER } #pragma endregion #pragma region Literal Literal::Literal(const std::u8string_view& words) : AbstractNode(), m_Literal(words), m_ConflictSet { m_Literal } { if (words.empty()) throw std::invalid_argument("The word of literal node should not be empty."); } Literal::~Literal() {} bool Literal::IsArgument() { return false; } const std::set& Literal::GetConflictSet() { return m_ConflictSet; } std::u8string Literal::GetHelpSymbol() { return m_Literal; } bool Literal::BeginConsume(const std::u8string& cur_cmd, ArgumentsMap& am) { return cur_cmd == m_Literal; } void Literal::EndConsume(ArgumentsMap& am) {} #pragma endregion #pragma region Choice Choice::Choice(const std::u8string_view& argname, const std::initializer_list& vocabulary) : AbstractNode(), m_ChoiceName(argname), m_Vocabulary(vocabulary), m_ConflictSet() { // check argument if (argname.empty()) throw std::invalid_argument("Choice argument name should not be empty."); if (m_Vocabulary.size() < 2u) throw std::invalid_argument("Too less vocabulary for choice. At least 2 items."); // init conflict set m_ConflictSet.insert(m_ChoiceName); m_ConflictSet.insert(m_Vocabulary.begin(), m_Vocabulary.end()); } Choice::~Choice() {} bool Choice::IsArgument() { return false; } const std::set& Choice::GetConflictSet() { return m_ConflictSet; } std::u8string Choice::GetHelpSymbol() { return YYCC::StringHelper::Printf(u8"[%s]", YYCC::StringHelper::Join(m_Vocabulary, u8" | ").c_str() ); } bool Choice::BeginConsume(const std::u8string& cur_cmd, ArgumentsMap& am) { for (size_t i = 0; i < m_Vocabulary.size(); ++i) { if (cur_cmd == m_Vocabulary[i]) { am.Add(m_ChoiceName, cur_cmd); return true; } } return false; } void Choice::EndConsume(ArgumentsMap& am) { am.Remove(m_ChoiceName); } #pragma endregion #pragma region Abstract Argument AbstractArgument::AbstractArgument(const std::u8string_view& argname) : AbstractNode(), m_ArgName(argname == nullptr ? "" : argname), m_Accepted(false), m_ParsedData(nullptr) { if (argname == nullptr || m_ArgName.empty()) throw std::invalid_argument("Invalid argument name."); } AbstractArgument::~AbstractArgument() {} NodeType AbstractArgument::GetNodeType() { return NodeType::Argument; } bool AbstractArgument::IsConflictWith(AbstractNode* node) { switch (node->GetNodeType()) { case NodeType::Literal: return false; case NodeType::Choice: return m_ArgName == dynamic_cast(node)->m_ChoiceName; case NodeType::Argument: return m_ArgName == dynamic_cast(node)->m_ArgName; default: throw std::runtime_error("No such node type."); } } std::string AbstractArgument::GetHelpSymbol() { std::string newargname = "<"; newargname.append(m_ArgName); newargname.append(">"); return newargname; } bool AbstractArgument::BeginAccept(const std::string& strl, ArgumentsMap* amap) { m_Accepted = BeginParse(strl); if (m_Accepted) amap->Add(m_ArgName, this); return m_Accepted; } void AbstractArgument::EndAccept(ArgumentsMap* amap) { if (m_Accepted) { amap->Remove(m_ArgName); EndParse(); m_Accepted = false; } } #pragma endregion #pragma region Argument Detail Impl bool IntArgument::BeginParse(const std::string& val) { char* pend = nullptr; errno = 0; int64_t v = std::strtoll(val.c_str(), &pend, 10); if (pend == val.c_str() || errno == ERANGE) return false; // check limit int32_t value = static_cast(v); if (m_IntLimit != nullptr && !m_IntLimit(value)) { return false; } m_ParsedData = new IntArgument::vType(value); return true; } void IntArgument::EndParse() { delete reinterpret_cast(m_ParsedData); m_ParsedData = nullptr; } bool StringArgument::BeginParse(const std::string& strl) { // string always accept every text m_ParsedData = new StringArgument::vType(strl); return true; } void StringArgument::EndParse() { delete reinterpret_cast(m_ParsedData); m_ParsedData = nullptr; } // Copy from Gamepiaynmo/BallanceModLoader std::vector SplitString(const std::string& str, const std::string& de) { size_t lpos, pos = 0; std::vector res; lpos = str.find_first_not_of(de, pos); while (lpos != std::string::npos) { pos = str.find_first_of(de, lpos); res.push_back(str.substr(lpos, pos - lpos)); if (pos == std::string::npos) break; lpos = str.find_first_not_of(de, pos); } if (pos != std::string::npos) res.push_back(""); return res; } bool EncodingArgument::BeginParse(const std::string& strl) { // encoding always accept every text m_ParsedData = new EncodingArgument::vType(SplitString(strl, ",")); return true; } void EncodingArgument::EndParse() { delete reinterpret_cast(m_ParsedData); m_ParsedData = nullptr; } #pragma endregion } #pragma region Command Root CommandRoot::CommandRoot() : AbstractNode() {} CommandRoot::~CommandRoot() {} bool CommandRoot::RootConsume(std::deque& arglist) { // if no data can consume, return if (arglist.empty()) return false; // create a argument map ArgumentsMap amap; // and we only just need iterate all children for (auto& pnode : m_Literals) { if (pnode->Consume(arglist, &amap)) { return true; } } for (auto& pnode : m_Choices) { if (pnode->Consume(arglist, &amap)) { return true; } } for (auto& pnode : m_Args) { if (pnode->Consume(arglist, &amap)) { return true; } } // no matched return false; } HelpDocument* CommandRoot::RootHelp() { HelpDocument* doc = new HelpDocument(); // we only just need iterate all children for (auto& pnode : m_Literals) { pnode->Help(doc); } for (auto& pnode : m_Choices) { pnode->Help(doc); } for (auto& pnode : m_Args) { pnode->Help(doc); } return doc; } #pragma endregion }