How to add an interactive shell (remote, too) to a C++ application by using my open-source C++14 library:
https://github.com/daniele77/cli
In the slide deck you can learn how to use it, how does it work, and find some thoughts about C++ design and patterns used by the library.
Add an interactive command line to your C++ application
1. Add an interactive command
line to your applications
Daniele Pallastrelli, daniele77.github.io
13.01.2022, Italian C++
2. My typical projects
• Run for a long period of time
• No classic interaction with the user (i.e., no GUI)
• Run in a server, custom board, almost never desktop applications
• They're not CPU bound applications
3. Why an interactive console in your apps?
• Need to have some sort of console to interact with my
applications (e.g., embedded systems running around the clock)
to:
• monitor,
• configure,
• manage the system
• E.g., CISCO routers
• Traditional systems: SSH connections + logs
4. Why an interactive console in your apps?
• Debugging
• Poke around internal state
• Dump internal structures
• Change the log level at runtime
• Change the working mode
• Enable / disable modules
• Load / unload plugins
• Early stage of development
5. Reinventing the wheel?
Existing solutions in open source domain:
• Linux only
• They're applications where you hook external programs to commands
• No remote sessions
• Few in C++
• None of them in "modern" C++
6. Enter the shell (cli?)
• My own library in C++14
• Production code quality
• Used in several industrial projects
• Demo time
• C++14
• Cross-platform (Linux and windows tested)
• Menus and submenus
• Command history (navigation with arrow keys)
• Autocompletion (with TAB key)
• Async interface
• Colors
7. Something missing…
• Good when you start the app from a console (e.g. desktop
applications or development stage)
• What about processes that run in background (e.g., embedded,
servers & c)?
12. Features summary
• C++14
• Header only
• Cross-platform (linux and windows)
• Menus and submenus
• Remote sessions (telnet)
• Persistent history (navigation with arrow keys)
• Autocompletion (with TAB key)
• Async interface
• Colors
13. auto rootMenu = make_unique<Menu>("cli");
rootMenu->Insert(
"hello",
[](std::ostream& out){ out << "Hello, worldn"; },
"Print hello world" );
rootMenu->Insert(
"hello_everysession",
[](std::ostream&){ Cli::cout() << "Hello, everybody" << std::endl; },
"Print hello everybody on all open sessions" );
rootMenu->Insert(
"reverse", {"string_to_revert"},
[](std::ostream& out, const string& arg)
{
string copy(arg);
std::reverse(copy.begin(), copy.end());
out << copy << "n";
},
"Print the reverse string" );
14. rootMenu->Insert(
"add", {"first_term", "second_term"},
[](std::ostream& out, int x, int y)
{
out << x << " + " << y << " = " << (x+y) << "n";
},
"Print the sum of the two numbers" );
rootMenu->Insert(
"sort", {"list of strings separated by space"},
[](std::ostream& out, std::vector<std::string> data)
{
std::sort(data.begin(), data.end());
out << "sorted list: ";
std::copy(data.begin(), data.end(), std::ostream_iterator<std::string>(out, " "));
out << "n";
},
"Alphabetically sort a list of words" );
15. auto subMenu = make_unique<Menu>("sub");
subMenu->Insert(
"demo",
[](std::ostream& out){ out << "This is a sample!n"; },
"Print a demo string" );
rootMenu->Insert( std::move(subMenu) );
16. // create a cli with the given root menu and a persistent storage
Cli cli( std::move(rootMenu), std::make_unique<FileHistoryStorage>(".cli") );
// global exit action
cli.ExitAction( [](auto& out){ out << "Goodbye and thanks for all the fish.n"; } );
// std exception custom handler
cli.StdExceptionHandler(
[](std::ostream& out, const std::string& cmd, const std::exception& e)
{
out << "Exception caught in cli handler: "
<< e.what()
<< " handling command: "
<< cmd
<< ".n";
}
);
20. Core idea
• Command list
• The user enters a string: command and parameters
• Iteration over the command list:
• Command name
• Parameter number and type
• The handler is called with the typed parameters
22. class Command {};
class Menu : public Command
{
private:
Menu* parent{ nullptr };
std::vector<Command*> cmds;
};
template <typename F, typename ... Args>
class VariadicFunctionCommand : public Command {};
23. Menu is-a Command, because when you start the CLI, every Menu shows as a command
you can digit at the prompt (e.g., if you define a Menu "foo", you get the command "foo" in
the Cli to enter the submenu).
32. Interlude – How do you get the type of a
lambda argument?
template <typename F>
void foo(F f)
{
// what's the type of "f" first parameter?
// something like:
// using T = F::first_parameter_type
)
foo( [](int){} );
33. Interlude – How do you get the type of a
lambda argument?
template<typename F, typename Ret, typename A, typename... Rest>
A helper(Ret (F::*)(A, Rest...) const);
template <typename F>
void foo(F f)
{
using T = decltype( helper(&F::operator()) );
}
foo( [](int){} );
34. class Menu : public Command
{
public:
template <typename F>
CmdHandler Insert(const string& cmdName, F f)
{
return CreateCmd(cmdName, f, &F::operator());
}
private:
template <typename F, typename R, typename ... Args>
CmdHandler CreateCmd(const string& cmdName, F& f, R (F::*)(Args...) const)
{
auto cmd = make_unique< VariadicFunctionCommand< F, Args ... > >(cmdName, f);
// insert cmd into this menu commands
...
}
};
Works with lambdas
and std::function
35. class Menu : public Command
{
public:
...
template <typename R, typename ... Args>
CmdHandler Insert(const std::string& cmdName, R (*f)(Args...))
{
using F = R (*)(Args...);
auto cmd = make_unique<VariadicFunctionCommand<F, Args ...>>(cmdName, f);
// insert cmd into this menu commands
...
}
...
};
Overload for free-
functions
42. The Proactor solution
Split every application service into:
• Long-duration operations. execute asynchronously
• Completion handlers. processes the results of the associated
asynchronous operations (potentially invoking additional
asynchronous operations).
48. Concurrency in my projects
• asio for asynchonous I/O (timers, too)
• asio::io_context available
• When I/O is ready, asio puts handler in the asio::io_context
• If more threads are needed (e.g., time consuming computations or
blocking I/O), the result is put in asio::io_context
• => everything runs in (my) single thread of execution
49. Back to the library
• Input coming from:
• Keyboard, using blocking primitives (std::getchar() and _getch())
• Sockets
• Commands callbacks (in which thread?)
• => proactor (asio::io_context)
50. Consequences
• The user must instantiate a boost::asio::io_context object
• The whole library depends on boost::asio
51. Concurrency summary (v. 1.0)
• The library depends on boost::asio, even when you don't need the
telnet server
• Library users ask to use standalone asio instead of boost::asio
• The truth is that the whole library depends on boost::asio because:
• Telnet server needs it
• Keyboard handler needs an event handler
• The two must use the same event manager, i.e. boost::asio::io_context
52. Release 2.0 – Goals:
• Optionally use standalone asio instead of boost::asio for the telnet
server
• Remove all dependencies if the telnet server is not needed
53. Release 2.0 – The solution
• New abstraction: "Scheduler"
• schedules async event
• The library provides three kind of schedulers:
• StandaloneAsioScheduler (based on asio::io_context)
• BoostAsioScheduler (based on boost::asio::io_context)
• LoopScheduler (hand made sync queue)
54. Release 2.0 – The solution
• Cli library can use all the schedulers, but if you need the telnet server
you must use *AsioScheduler
• Bottom line:
• If ( you need telnet server OR your app already uses [boost::]asio::io_context
=> StandaloneAsioScheduler or BoostAsioScheduler
• Else
=> LoopScheduler (no external dependencies)
61. Intentional architecture VS emergent design
"Many times, thinking things out in advance saved us serious development headaches later on. ... [on
making a particular specification change] ... Making this change in the spec took an hour or two. If we
had made this change in code, it would have added weeks to the schedule. I can’t tell you how
strongly I believe in Big Design Up Front, which the proponents of Extreme Programming consider
anathema. I have consistently saved time and made better products by using BDUF and I’m proud to
use it, no matter what the XP fanatics claim. They’re just wrong on this point and I can’t be any clearer
than that."
-- Joel Spolsky "The project Aardwark Spec" – Joel On Software
62. Take away
• Use the right technique
• Use Cli library (when you need it)
• Use Proactor pattern (when you need it)
• Use your brain (always)