/******************************************************************************* * CLI - A simple command line interface. * Copyright (C) 2016-2021 Daniele Pallastrelli * * Boost Software License - Version 1.0 - August 17th, 2003 * * Permission is hereby granted, free of charge, to any person or organization * obtaining a copy of the software and accompanying documentation covered by * this license (the "Software") to use, reproduce, display, distribute, * execute, and transmit the Software, and to prepare derivative works of the * Software, and to permit third-parties to whom the Software is furnished to * do so, all subject to the following: * * The copyright notices in the Software and this entire statement, including * the above license grant, this restriction and the following disclaimer, * must be included in all copies of the Software, in whole or in part, and * all derivative works of the Software, unless such copies or derivative * works are solely in the form of machine-executable object code generated by * a source language processor. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. ******************************************************************************/ #ifndef CLI_CLI_H #define CLI_CLI_H #include #include #include #include #include #include #include // std::isspace #include #include "terminalprofile.h" #include "detail/history.h" #include "detail/split.h" #include "detail/fromstring.h" #include "historystorage.h" #include "volatilehistorystorage.h" #include #include namespace cli { // ******************************************************************** template < typename T > struct TypeDesc { static const char* Name() { return ""; } }; template <> struct TypeDesc< char > { static const char* Name() { return ""; } }; template <> struct TypeDesc< unsigned char > { static const char* Name() { return ""; } }; template <> struct TypeDesc< signed char > { static const char* Name() { return ""; } }; template <> struct TypeDesc< short > { static const char* Name() { return ""; } }; template <> struct TypeDesc< unsigned short > { static const char* Name() { return ""; } }; template <> struct TypeDesc< int > { static const char* Name() { return ""; } }; template <> struct TypeDesc< unsigned int > { static const char* Name() { return ""; } }; template <> struct TypeDesc< long > { static const char* Name() { return ""; } }; template <> struct TypeDesc< unsigned long > { static const char* Name() { return ""; } }; template <> struct TypeDesc< long long > { static const char* Name() { return ""; } }; template <> struct TypeDesc< unsigned long long > { static const char* Name() { return ""; } }; template <> struct TypeDesc< float > { static const char* Name() { return ""; } }; template <> struct TypeDesc< double > { static const char* Name() { return ""; } }; template <> struct TypeDesc< long double > { static const char* Name() { return ""; } }; template <> struct TypeDesc< bool > { static const char* Name() { return ""; } }; template <> struct TypeDesc< std::string > { static const char* Name() { return ""; } }; template <> struct TypeDesc< std::vector > { static const char* Name() { return ""; } }; // ******************************************************************** // forward declarations class Menu; class CliSession; class Cli { // inner class to provide a global output stream class OutStream { public: template OutStream& operator << (const T& msg) { for (auto out: ostreams) *out << msg; return *this; } // this is the type of std::cout using CoutType = std::basic_ostream >; // this is the function signature of std::endl using StandardEndLine = CoutType &(*)(CoutType &); // takes << std::endl OutStream& operator << (StandardEndLine manip) { for (auto out: ostreams) manip(*out); return *this; } private: friend class Cli; void Register(std::ostream& o) { ostreams.push_back(&o); } void UnRegister(std::ostream& o) { ostreams.erase(std::remove(ostreams.begin(), ostreams.end(), &o), ostreams.end()); } std::vector ostreams; }; // end inner class public: ~Cli() = default; // disable value semantics Cli(const Cli&) = delete; Cli& operator = (const Cli&) = delete; // enable move semantics Cli(Cli&&) = default; Cli& operator = (Cli&&) = default; /// \deprecated Use the @c Cli::Cli(std::unique_ptr,std::unique_ptr) /// overload version and the method @c Cli::ExitAction instead [[deprecated("Use the other overload of Cli constructor and the method Cli::ExitAction instead")]] explicit Cli( std::unique_ptr&& _rootMenu, std::function< void(std::ostream&)> _exitAction, std::unique_ptr&& historyStorage = std::make_unique() ) : globalHistoryStorage(std::move(historyStorage)), rootMenu(std::move(_rootMenu)), exitAction(std::move(_exitAction)) { } /** * @brief Construct a new Cli object having a given root menu that contains the first level commands available. * * @param _rootMenu is the @c Menu containing the first level commands available to the user. * @param historyStorage is the policy for the storage of the cli commands history. You must pass an istance of * a class derived from @c HistoryStorage. The library provides these policies: * - @c VolatileHistoryStorage * - @c FileHistoryStorage it's a persistent history. I.e., the command history is preserved after your application * is restarted. * * However, you can develop your own, just derive a class from @c HistoryStorage . */ Cli(std::unique_ptr _rootMenu, std::unique_ptr historyStorage = std::make_unique()) : globalHistoryStorage(std::move(historyStorage)), rootMenu(std::move(_rootMenu)), exitAction{} { } /** * @brief Add a global exit action that is called every time a session (local or remote) gets the "exit" command. * * @param action the function to be called when a session exits, taking a @c std::ostream& parameter to write on that session console. */ void ExitAction(const std::function< void(std::ostream&)>& action) { exitAction = action; } /** * @brief Add an handler that will be called when a @c std::exception (or derived) is thrown inside a command handler. * If an exception handler is not set, the exception will be logget on the session output stream. * * @param handler the function to be called when an exception is thrown, taking a @c std::ostream& parameter to write on that session console * and the exception thrown. */ void StdExceptionHandler(const std::function< void(std::ostream&, const std::string& cmd, const std::exception&) >& handler) { exceptionHandler = handler; } /** * @brief Get a global out stream object that can be used to print on every session currently connected (local and remote) * * @return OutStream& the reference to the global out stream writing on every session console. */ static OutStream& cout() { static OutStream s; return s; } private: friend class CliSession; Menu* RootMenu() { return rootMenu.get(); } void ExitAction( std::ostream& out ) { if ( exitAction ) exitAction( out ); } void StdExceptionHandler(std::ostream& out, const std::string& cmd, const std::exception& e) { if (exceptionHandler) exceptionHandler(out, cmd, e); else out << e.what() << '\n'; } static void Register(std::ostream& o) { cout().Register(o); } static void UnRegister(std::ostream& o) { cout().UnRegister(o); } void StoreCommands(const std::vector& cmds) { globalHistoryStorage->Store(cmds); } std::vector GetCommands() const { return globalHistoryStorage->Commands(); } private: std::unique_ptr globalHistoryStorage; std::unique_ptr rootMenu; // just to keep it alive std::function exitAction; std::function exceptionHandler; }; // ******************************************************************** class Command { public: explicit Command(std::string _name) : name(std::move(_name)), enabled(true) {} virtual ~Command() noexcept = default; // disable copy and move semantics Command(const Command&) = delete; Command(Command&&) = delete; Command& operator=(const Command&) = delete; Command& operator=(Command&&) = delete; virtual void Enable() { enabled = true; } virtual void Disable() { enabled = false; } virtual bool Exec(const std::vector& cmdLine, CliSession& session) = 0; virtual void Help(std::ostream& out) const = 0; // Returns the collection of completions relatives to this command. // For simple commands, provides a base implementation that use the name of the command // for aggregate commands (i.e., Menu), the function is redefined to give the menu command // and the subcommand recursively virtual std::vector GetCompletionRecursive(const std::string& line) const { if (!enabled) return {}; if (name.rfind(line, 0) == 0) return {name}; // name starts_with line return {}; } protected: const std::string& Name() const { return name; } bool IsEnabled() const { return enabled; } private: const std::string name; bool enabled; }; // ******************************************************************** // free utility function to get completions from a list of commands and the current line inline std::vector GetCompletions( const std::shared_ptr>>& cmds, const std::string& currentLine) { std::vector result; std::for_each(cmds->begin(), cmds->end(), [¤tLine,&result](const auto& cmd) { auto c = cmd->GetCompletionRecursive(currentLine); result.insert( result.end(), std::make_move_iterator(c.begin()), std::make_move_iterator(c.end()) ); } ); return result; } // ******************************************************************** // forward declaration class CmdHandler; class UserSession : public std::unordered_map { public: explicit UserSession(std::ostream& _out) noexcept : out(_out) {} std::ostream& out; TerminalProfile terminalProfile; virtual ~UserSession() noexcept = default; // disable value semantics UserSession(const UserSession&) = delete; UserSession& operator = (const UserSession&) = delete; // disable move semantics UserSession(UserSession&&) = delete; UserSession& operator = (UserSession&&) = delete; }; class CliSession : public UserSession { public: CliSession(Cli& _cli, std::ostream& _out, std::size_t historySize = 100); virtual ~CliSession() noexcept { Cli::UnRegister(out); } // disable value semantics CliSession(const CliSession&) = delete; CliSession& operator = (const CliSession&) = delete; // disable move semantics CliSession(CliSession&&) = delete; CliSession& operator = (CliSession&&) = delete; void Feed( const std::string& cmd ); void Prompt(); void Current(Menu* menu) { current = menu; } void Help() const; void Exit() { exitAction(out); cli.ExitAction(out); auto cmds = history.GetCommands(); cli.StoreCommands(cmds); } void ExitAction(const std::function& action) { exitAction = action; } void ShowHistory() const { history.Show(out); } std::string PreviousCmd(const std::string& line) { return history.Previous(line); } std::string NextCmd() { return history.Next(); } std::vector GetCompletions(std::string currentLine) const; private: Cli& cli; Menu* current; std::unique_ptr globalScopeMenu; std::function< void(std::ostream&)> exitAction = []( std::ostream& ){}; detail::History history; }; // ******************************************************************** class CmdHandler { public: using CmdVec = std::vector>; CmdHandler() = default; CmdHandler(std::weak_ptr c, std::weak_ptr v) : descriptor(c, v) {} void Enable() { descriptor.Enable(); } void Disable() { descriptor.Disable(); } void Remove() { descriptor.Remove(); } private: struct Descriptor { Descriptor() = default; Descriptor(std::weak_ptr c, std::weak_ptr v) : cmd(std::move(c)), cmds(std::move(v)) {} void Enable() { if (auto c = cmd.lock()) c->Enable(); } void Disable() { if(auto c = cmd.lock()) c->Disable(); } void Remove() { auto scmd = cmd.lock(); auto scmds = cmds.lock(); if (scmd && scmds) { auto i = std::find_if( scmds->begin(), scmds->end(), [&](const auto& c){ return c.get() == scmd.get(); } ); if (i != scmds->end()) scmds->erase(i); } } std::weak_ptr cmd; std::weak_ptr cmds; }; Descriptor descriptor; }; // ******************************************************************** class Menu : public Command { public: // disable value and move semantics Menu(const Menu&) = delete; Menu& operator = (const Menu&) = delete; Menu(Menu&&) = delete; Menu& operator = (Menu&&) = delete; Menu() : Command({}), parent(nullptr), description(), cmds(std::make_shared()) {} explicit Menu(const std::string& _name, std::string desc = "(menu)") : Command(_name), parent(nullptr), description(std::move(desc)), cmds(std::make_shared()) {} template CmdHandler Insert(const std::string& cmdName, F f, const std::string& help = "", const std::vector& parDesc={}) { // dispatch to private Insert methods return Insert(cmdName, help, parDesc, f, &F::operator()); } template CmdHandler Insert(const std::string& cmdName, const std::vector& parDesc, F f, const std::string& help = "") { // dispatch to private Insert methods return Insert(cmdName, help, parDesc, f, &F::operator()); } CmdHandler Insert(std::unique_ptr&& cmd) { std::shared_ptr scmd(std::move(cmd)); CmdHandler c(scmd, cmds); cmds->push_back(scmd); return c; } CmdHandler Insert(std::unique_ptr&& menu) { std::shared_ptr smenu(std::move(menu)); CmdHandler c(smenu, cmds); smenu->parent = this; cmds->push_back(smenu); return c; } bool Exec(const std::vector& cmdLine, CliSession& session) override { if (!IsEnabled()) return false; if (cmdLine[0] == Name()) { if (cmdLine.size() == 1) { session.Current(this); return true; } else { // check also for subcommands std::vector subCmdLine(cmdLine.begin()+1, cmdLine.end()); for (auto& cmd: *cmds) if (cmd->Exec( subCmdLine, session )) return true; } } return false; } bool ScanCmds(const std::vector& cmdLine, CliSession& session) { if (!IsEnabled()) return false; for (auto& cmd: *cmds) if (cmd->Exec(cmdLine, session)) return true; return (parent && parent->Exec(cmdLine, session)); } std::string Prompt() const { return Name(); } void MainHelp(std::ostream& out) { if (!IsEnabled()) return; for (const auto& cmd: *cmds) cmd->Help(out); if (parent != nullptr) parent->Help(out); } void Help(std::ostream& out) const override { if (!IsEnabled()) return; out << " - " << Name() << "\n\t" << description << "\n"; } // returns: // - the completions of this menu command // - the recursive completions of subcommands // - the recursive completions of parent menu std::vector GetCompletions(const std::string& currentLine) const { auto result = cli::GetCompletions(cmds, currentLine); if (parent != nullptr) { auto c = parent->GetCompletionRecursive(currentLine); result.insert(result.end(), std::make_move_iterator(c.begin()), std::make_move_iterator(c.end())); } return result; } // returns: // - the completion of this menu command // - the recursive completions of the subcommands std::vector GetCompletionRecursive(const std::string& line) const override { if (line.rfind(Name(), 0) == 0) // line starts_with Name() { auto rest = line; rest.erase(0, Name().size()); // trim_left(rest); rest.erase(rest.begin(), std::find_if(rest.begin(), rest.end(), [](int ch) { return !std::isspace(ch); })); std::vector result; for (const auto& cmd: *cmds) { auto cs = cmd->GetCompletionRecursive(rest); for (const auto& c: cs) result.push_back(Name() + ' ' + c); // concat submenu with command } return result; } return Command::GetCompletionRecursive(line); } private: template CmdHandler Insert(const std::string& name, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, Args...) const); template CmdHandler Insert(const std::string& name, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, const std::vector&) const); template CmdHandler Insert(const std::string& name, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, std::vector) const); Menu* parent{ nullptr }; const std::string description; // using shared_ptr instead of unique_ptr to get a weak_ptr // for the CmdHandler::Descriptor using Cmds = std::vector>; std::shared_ptr cmds; }; // ******************************************************************** template struct Select; template struct Select { template static void Exec(const F& f, InputIt first, InputIt last) { assert( first != last ); assert( std::distance(first, last) == 1+sizeof...(Args) ); const P p = detail::from_string::type>(*first); auto g = [&](auto ... pars){ f(p, pars...); }; Select::Exec(g, std::next(first), last); } }; template struct Select { template static void Exec(const F& f, InputIt first, InputIt last) { // silence the unused warning in release mode when assert is disabled static_cast(first); static_cast(last); assert(first == last); f(); } }; template struct PrintDesc; template struct PrintDesc { static void Dump(std::ostream& out) { out << " " << TypeDesc< typename std::decay

::type >::Name(); PrintDesc::Dump(out); } }; template <> struct PrintDesc<> { static void Dump(std::ostream& /*out*/) {} }; // ******************************************* template class VariadicFunctionCommand : public Command { public: // disable value semantics VariadicFunctionCommand(const VariadicFunctionCommand&) = delete; VariadicFunctionCommand& operator = (const VariadicFunctionCommand&) = delete; VariadicFunctionCommand( const std::string& _name, F fun, std::string desc, std::vector parDesc ) : Command(_name), func(std::move(fun)), description(std::move(desc)), parameterDesc(std::move(parDesc)) { } bool Exec(const std::vector< std::string >& cmdLine, CliSession& session) override { if (!IsEnabled()) return false; const std::size_t paramSize = sizeof...(Args); if (cmdLine.size() != paramSize+1) return false; if (Name() == cmdLine[0]) { try { auto g = [&](auto ... pars){ func( session, pars... ); }; Select::Exec(g, std::next(cmdLine.begin()), cmdLine.end()); } catch (std::bad_cast&) { return false; } return true; } return false; } void Help(std::ostream& out) const override { if (!IsEnabled()) return; out << " - " << Name(); if (parameterDesc.empty()) PrintDesc::Dump(out); for (auto& s: parameterDesc) out << " <" << s << '>'; out << "\n\t" << description << "\n"; } private: const F func; const std::string description; const std::vector parameterDesc; }; template class FreeformCommand : public Command { public: // disable value semantics FreeformCommand(const FreeformCommand&) = delete; FreeformCommand& operator = (const FreeformCommand&) = delete; FreeformCommand( const std::string& _name, F fun, std::string desc, std::vector parDesc ) : Command(_name), func(std::move(fun)), description(std::move(desc)), parameterDesc(std::move(parDesc)) { } bool Exec(const std::vector< std::string >& cmdLine, CliSession& session) override { if (!IsEnabled()) return false; assert(!cmdLine.empty()); if (Name() == cmdLine[0]) { func(session, std::vector(std::next(cmdLine.begin()), cmdLine.end())); return true; } return false; } void Help(std::ostream& out) const override { if (!IsEnabled()) return; out << " - " << Name(); if (parameterDesc.empty()) PrintDesc>::Dump(out); for (auto& s: parameterDesc) out << " <" << s << '>'; out << "\n\t" << description << "\n"; } private: const F func; const std::string description; const std::vector parameterDesc; }; // ******************************************************************** // CliSession implementation inline CliSession::CliSession(Cli& _cli, std::ostream& _out, std::size_t historySize) : UserSession(_out), cli(_cli), current(cli.RootMenu()), globalScopeMenu(std::make_unique< Menu >()), history(historySize) { history.LoadCommands(cli.GetCommands()); Cli::Register(out); globalScopeMenu->Insert( "help", [this](UserSession&){ Help(); }, "This help message" ); globalScopeMenu->Insert( "exit", [this](UserSession&){ Exit(); }, "Quit the session" ); #ifdef CLI_HISTORY_CMD globalScopeMenu->Insert( "history", [this](UserSession&){ ShowHistory(); }, "Show the history" ); #endif } inline void CliSession::Feed(const std::string& cmd) { std::vector strs; detail::split(strs, cmd); if (strs.empty()) return; // just hit enter history.NewCommand(cmd); // add anyway to history try { // global cmds check bool found = globalScopeMenu->ScanCmds(strs, *this); // root menu recursive cmds check if (!found) found = current->ScanCmds(strs, *this); if (!found) // error msg if not found out << "wrong command: " << cmd << '\n'; } catch(const std::exception& e) { cli.StdExceptionHandler(out, cmd, e); } catch(...) { out << "Cli. Unknown exception caught handling command line \"" << cmd << "\"\n"; } } inline void CliSession::Prompt() { out << terminalProfile.beforePrompt << current->Prompt() << terminalProfile.afterPrompt << "> " << std::flush; } inline void CliSession::Help() const { out << "Commands available:\n"; globalScopeMenu->MainHelp(out); current->MainHelp( out ); } inline std::vector CliSession::GetCompletions(std::string currentLine) const { // trim_left(currentLine); currentLine.erase(currentLine.begin(), std::find_if(currentLine.begin(), currentLine.end(), [](int ch) { return !std::isspace(ch); })); auto v1 = globalScopeMenu->GetCompletions(currentLine); auto v3 = current->GetCompletions(currentLine); v1.insert(v1.end(), std::make_move_iterator(v3.begin()), std::make_move_iterator(v3.end())); // removes duplicates (std::unique requires a sorted container) std::sort(v1.begin(), v1.end()); auto ip = std::unique(v1.begin(), v1.end()); v1.resize(static_cast(std::distance(v1.begin(), ip))); return v1; } // Menu implementation template CmdHandler Menu::Insert(const std::string& cmdName, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, Args...) const ) { return Insert(std::make_unique>(cmdName, f, help, parDesc)); } template CmdHandler Menu::Insert(const std::string& cmdName, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, const std::vector& args) const ) { return Insert(std::make_unique>(cmdName, f, help, parDesc)); } template CmdHandler Menu::Insert(const std::string& cmdName, const std::string& help, const std::vector& parDesc, F& f, R (F::*)(UserSession& s, std::vector args) const ) { return Insert(std::make_unique>(cmdName, f, help, parDesc)); } } // namespace cli #endif // CLI_CLI_H