cmdr-cxx
^pre-release^ is a C++17 header-only command-line arguments parser, config manager, and application
framework. As a member of #cmdr series, it provides a
fully-functional Option Store
(Configuration Manager) for your hierarchical configuration data.
See also golang version: cmdr.
-
POSIX-Compliant command-line argument parser
- supports long flag (REQUIRED,
--help
, short flag (-h
), and aliases (--usage
,--info
, ...) - supports multi-level sub-commands
- supports short flag compat:
-vab
==-v -a -b
,-r3ap1zq
==-r 3 -ap 1 -z -q
- supports passthrough flag:
--
will terminate the parsing - supports lots of data types for a flag: bool, int, uint, float, string, array, chrono duration, ...
- allows user-custom data types
- automated help-screen printing (
-hhh
to print the hidden items)
- supports long flag (REQUIRED,
-
Robust Interfaces
-
Hooks or Actions:
- global: pre/post-invoke
- flags: on_hit
- commands: on_hit, pre/post-invoke, invoke
-
Supports non-single-char short flag:
-ap 1
-
Supports for
-D+
,-D-
to enable/disable a bool option -
Supports sortable command/flag groups
-
Supports toggleable flags - just like a radio button group
-
Free style flags arrangements:
$ app main sub4 bug-bug2 zero-sub3 -vqb2r1798b2r 234 --sub4-retry1 913 --bug-bug2-shell-name=fish ~~debug --int 67 -DDD --string 'must-be' --long 789
-
Smart suggestions for wrong command and flags
based on Jaro-Winkler distance. See Snapshot
-
Builtin commands and flags
- Help:
-h
,-?
,--help
,--info
,--usage
, ...help
command:app help server pause
==app server pause --help
.
- Version & Build Info:
--version
/--ver
/-V
,--build-info
/-#
version
/versions
command available.- Simulating version at runtime with
—-version-sim 1.9.1
- Help:
-
~~tree
: lists all commands and sub-commands.~~debug
: print the debugging info--no-color
: disable terminal color in outputting--config <location>
: specify the location of the root config file. [only for yaml-loader]
-
Verbose & Debug:
—verbose
/-v
,—debug
/-D
,—quiet
/-q
-
Supports
-I/usr/include -I=/usr/include
-I /usr/include -I:/usr
option argument specifications Automatically allows those formats (applied to long option too):-I file
,-Ifile
, and-I=files
-I 'file'
,-I'file'
, and-I='files'
-I "file"
,-I"file"
, and-I="files"
-
Envvars overrides:
HELP=1 ./bin/test-app2-c2 server pause
is the equivalent of./bin/test-app2-c2 server pause --help
-
Extensible external loaders:
cli.set_global_on_loading_externals(...);
-
Extending internal actions for special operations auch as printing help screen...
-
-
Hierarchical Data Manager -
Option Store
- various data types supports
- accusing the item with its dotted path key (such as
server.tls.certs.cert-bundle
) - See also Fast Doc section.
- See CHANGELOG
- gcc 10+: passed
- clang 12+: passed
- msvc build tool:
- 17.2.32505.173 (VS2022 or Build Tool) passed
- OLD: 16.7.2, 16.8.5 (VS2019 or Build Tool) passed
- NEW: VS2022 passed
cmdr-cxx
prints an evident, clear, and logical help-screen. Please proceed the more snapshots
at #1 - Gallery.
cmdr-cxx can be installed from homebrew:
brew install hedzr/brew/cmdr-cxx
cmdr-cxx is findable via CMake Modules.
You could install cmdr-cxx manually:
git clone https://github.com/hedzr/cmdr-cxx.git
cd cmdr-cxx
cmake -DCMAKE_VERBOSE_DEBUG=ON -DCMAKE_AUTOMATE_TESTS=OFF -S . -B build/ -G Ninja
# Or:
# cmake -S . -B build/
cmake --build build/
cmake --install build/
# Or:
# cmake --build build/ --target install
#
# Sometimes sudo it:
# sudo cmake --build build/ --target install
# Or:
# cmake --install build/ --prefix ./dist/install --strip
# sudo cp -R ./dist/install/include/* /usr/local/include/
# sudo cp -R ./dist/install/lib/cmake/cmdr11 /usr/local/lib/cmake/
#
# macOS users could install to Homebrew directly without super privilidges:
# cmake --install build/ --prefix $(brew --prefix) --strip
#
rm -rf ./build
cd ..
More cmake commands:
# clean (all targets files, but the immedieted files)
cmake --build build/ --target clean
# clean and build (just relinking all targets without recompiling)
cmake --build build/ --clean-first
# clean deeply
rm -rf build/
# clean deeply since cmake 3.24.0
# (your custom settings from command-line will lost.
# For example, if you ever run `cmake -DCMAKE_VERBOSE_DEBUG=ON -S . -B build',
# and now cmake --fresh -B build/ will ignore `CMAKE_VERBOSE_DEBUG = ON'
# and reconfigure to default state.
# )
cmake --fresh -B build/
# recompiling and relinking (simply passing `-B' to `make')
cmake --build build/ -- -B
# reconfigure
rm ./build/CMakeCache.txt && cmake -DENABLE_AUTOMATE_TESTS=OFF -S . -B build/
# print compiling command before exeuting them
cmake --build build/ -- VERBOSE=1
# Or:
VERBOSE=1 cmake --build build/
# Or:
cmake --build build --verbose
To build cmdr-cxx, please install these components at first:
- yaml-cpp
The typical install command could be brew install yaml-cpp
,
For more information, please refer to the chapter Others.
It can be disabled by
CMDR_NO_3RDPARTY
while building cmdr-cxx:cmake -DCMDR_NO_3RDPARTY:BOOL=ON -DCMAKE_BUILD_TYPE:STRING=Release -S . -B build-release/
Adding cmdr11 with ExternalProject
is possible. A load-cmdr-cxx.cmake
was provided to make integrating cmdr-cxx
easier.
Extract cmake/load-cmdr-cxx.cmake
into your project, load it and use add-cmdr-cxx-to
macro. For example, your cli-app could be:
# define_cxx_executable_project(myapp
# PREFIX myapp
# LIBRARIES ${myapp_libs}
# SOURCES ${myapp_source_files}
# INCLUDE_DIRECTORIES ${myapp_INCDIR}
# )
# enable_version_increaser(myapp-cli myapp my MY_)
project(cmdr-cli-demo)
add_executable(${PROJECT_NAME} cmdr_main.cc)
target_include_directories(${PROJECT_NAME} PRIVATE
$<BUILD_INTERFACE:${CMAKE_GENERATED_DIR}>
)
include(load-cmdr-cxx) # load ME here.
add_cmdr_cxx_to(myapp) # attach cmdr11::cmdr11
It works.
After installed at local cmake repository (Modules), cmdr-cxx
can be integrated as your CMake module. So we might find
and use it:
find_package(cmdr11 REQUIRED)
add_executable(my-app)
target_link_libraries(my-app PRIVATE cmdr11::cmdr11)
set_target_properties(${PROJECT_NAME} PROPERTIES
CXX_STANDARD 17
CXX_STANDARD_REQUIRED ON
CXX_EXTENSIONS ON
)
Or you can download load-cmdr-cxx.cmake and include it:
add_executable(my-app)
include(deps-cmdr11) # put deps-cmdr11.cmake into your cmake module path at first
add_cmdr_cxx_to(my-app)
#include <cmdr11/cmdr11.hh>
#include "version.h" // xVERSION_STRING
int main(int argc, char *argv[]) {
auto &cli = cmdr::cli("app2", xVERSION_STRING, "hedzr",
"Copyright © 2021 by hedzr, All Rights Reserved.",
"A demo app for cmdr-cxx library.",
"$ ~ --help");
try {
using namespace cmdr::opt;
cli += sub_cmd{}("server", "s", "svr")
.description("server operations for listening")
.group("TCP/UDP/Unix");
{
auto &t1 = *cli.last_added_command();
t1 += opt{(int16_t)(8)}("retry", "r")
.description("set the retry times");
t1 += opt{(uint64_t) 2}("count", "c")
.description("set counter value");
t1 += opt{"localhost"}("host", "H", "hostname", "server-name")
.description("hostname or ip address")
.group("TCP")
.placeholder("HOST[:IP]")
.env_vars("HOST");
t1 += opt{(int16_t) 4567}("port", "p")
.description("listening port number")
.group("TCP")
.placeholder("PORT")
.env_vars("PORT", "SERVER_PORT");
t1 += sub_cmd{}("start", "s", "startup", "run")
.description("start the server as a daemon service, or run it at foreground")
.on_invoke([](cmdr::opt::cmd const &c, string_array const &remain_args) -> int {
UNUSED(c, remain_args);
std::cout << c.title() << " invoked.\n";
return 0;
});
auto &s1 = *t1.last_added_command();
s1 += cmdr::opt::opt{}("foreground", "f")
.description("run at fg");
t1 += sub_cmd{}("stop", "t", "shutdown")
.description("stop the daemon service, or stop the server");
t1 += sub_cmd{}("pause", "p")
.description("pause the daemon service");
t1 += sub_cmd{}("resume", "re")
.description("resume the paused daemon service");
t1 += sub_cmd{}("reload", "r")
.description("reload the daemon service");
t1 += sub_cmd{}("hot-reload", "hr")
.description("hot-reload the daemon service without stopping the process");
t1 += sub_cmd{}("status", "st", "info", "details")
.description("display the running status of the daemon service");
}
} catch (std::exception &e) {
std::cerr << "Exception caught for duplicated cmds/args: " << e.what() << '\n';
CMDR_DUMP_STACK_TRACE(e);
}
return cli.run(argc, argv);
}
It is a simple program.
The operator ()
(cli("cmd1.sub-cmd2.sub-sub-cmd")
) could be used for retrieving a command (cmdr::opt::cmd& cc
)
from cli
:
auto &cc = cli("server");
CMDR_ASSERT(cc.valid());
CMDR_ASSERT(cc["count"].valid()); // the flag of 'server'
CMDR_ASSERT(cc["host"].valid());
CMDR_ASSERT(cc("status").valid()); // the sub-command of 'server'
CMDR_ASSERT(cc("start").valid()); // sub-command: 'start'
CMDR_ASSERT(cc("run", true).valid()); // or alias: 'run'
Once cc
is valid, use []
to extract its flags.
The dotted key is allowed. For example: cc["start.port"].valid()
.
CMDR_ASSERT(cli("server.start").valid());
CMDR_ASSERT(cli("server.start.port").valid());
// get flag 'port' of command 'server.start':
CMDR_ASSERT(cc["start.port"].valid());
While a flag given from command-line is matched ok, it holds some hit info. Such as:
auto &cc = cli("server"); // get 'server' command object
CMDR_ASSERT(cc.valid()); // got ok?
CMDR_ASSERT(cc["count"].valid());
CMDR_ASSERT(cc["count"].hit_long()); // if `--count` given
CMDR_ASSERT(cc["count"].hit_long() == false); // if `-c` given
CMDR_ASSERT(cc["count"].hit_count() == 1); // if `--count` given
CMDR_ASSERT(cc["count"].hit_title() == "c"); // if `-c` given
CMDR_ASSERT(cli["verbose"].hit_count() == 3); // if `-vvv` given
// hit_xxx are available for a command too
CMDR_ASSERT(cc.hit_title() == "server"); // if 'server' command given
The value of a flag from command-line will be saved into Option Store
, and extracted by shortcut cmdr::get_for_cli()
. For example:
auto verbose = cmdr::get_for_cli<bool>("verbose");
auto hostname = cmdr::get_for_cli<std::string>("server.host");
In Option Store
, the flag value will be prefixed by "app.cli."
, and get_for_cli wraps transparently.
The normal entries in
Options Store
are prefixed by string"app."
. You could define another one of course.
To extract the normal configuration data, cmdr::set
and cmdr::get
are best choices. They will wrap and unwrap the
prefix app
transparently.
auto verbose = cmdr::get<bool>("cli.verbose");
auto hostname = cmdr::get<std::string>("cli.server.host");
If you wanna extract them directly:
auto verbose = cmdr::get_store().get_raw<bool>("app.cli.verbose");
auto hostname = cmdr::get_store().get_raw<std::string>("app.cli.server.host");
auto verbose = cmdr::get_store().get_raw_p<bool>("app.cli", "verbose");
auto hostname = cmdr::get_store().get_raw_p<std::string>("app.cli", "server.host");
Every entry in Option Store
that we call it a config item. The entries are hierarchical. So we locate it with a dotted
key path string.
A config item is free for data type dynamically. That is saying, you could change the data type of a item at runtime. Such as setting one entry to integer array, from integer originally.
But it is hard for coding while you're working for a c++ program.
cmdr::set("wudao.count", 1);
cmdr::set("wudao.string", "str");
cmdr::set("wudao.float", 3.14f);
cmdr::set("wudao.double", 2.7183);
cmdr::set("wudao.array", std::vector{"a", "b", "c"});
cmdr::set("wudao.bool", false);
std::cout << cmdr::get<int>("wudao.count") << '\n';
auto const &aa = cmdr::get< std::vector<char const*> >("wudao.array");
std::cout << cmdr::string::join(aa, ", ", "[", "]") << '\n';
// Or: maybe you will like to stream out a `variable` with standard format.
cmdr::vars::variable& ab = cmdr::get_app().get("wudao.array");
std::cout << ab << '\n';
cmdr-cxx
provides stream-io on lots of types via cmdr::vars::variable
, take a look for further.
There are some common cross-platform helper classes. Its can be found in these headers:
-
cmdr_defs.hh
-
cmdr_types.hh
-
cmdr_type_checks.hh
-
cmdr_utils.hh
-
cmdr_dbg.hh
-
cmdr_log.gg
-
cmdr_chrono.hh
-
cmdr_if.hh
-
cmdr_ios.hh
-
cmdr_mmap.hh
-
cmdr_os_io_redirect.hh
-
cmdr_path.hh
-
cmdr_process.hh
-
cmdr_pool.hh
-
cmdr_priority_queue.hh
-
cmdr_string.hh
-
cmdr_terminal.hh
cmdr-cxx
provides some debugging features or top view to improve you design at CLI-side.
We've been told that we can bind an action (via on_invoke
) to a (sub-)command:
t1 += sub_cmd{}("start", "s", "startup", "run")
.description("start the server as a daemon service, or run it at foreground")
.on_invoke([](cmdr::opt::cmd const &c, string_array const &remain_args) -> int {
UNUSED(c, remain_args);
std::cout << c.title() << " invoked.\n";
return 0;
});
For those commands without binding to on_invoke
, cmdr-cxx
will invoke a default one, For example:
t1 += sub_cmd{}("pause", "p")
.description("pause the daemon service");
While the end-user is typing and they will got:
❯ ./bin/test-app2-c2 server pause
INVOKING: "pause, p", remains: .
command "pause, p" hit.
Simple naive? Dislike it? The non-hooked action can be customized.
Yes you can:
#if CMDR_TEST_ON_COMMAND_NOT_HOOKED
cli.set_global_on_command_not_hooked([](cmdr::opt::cmd const &, string_array const &) {
cmdr::get_store().dump_full_keys(std::cout);
cmdr::get_store().dump_tree(std::cout);
return 0;
});
#endif
Special flag has the leading sequence chars ~~
.
~~debug
can disable the command action and print the internal hitting information.
❯ ./bin/test-app2-c2 server pause -r 5 -c 3 -p 1357 -vvv ~~debug
command "pause, p" hit.
- 1 hits: "--port=PORT, -p" (hit title: "p", spec:0, long:0, env:0) => 1357
- 1 hits: "--retry, -r" (hit title: "r", spec:0, long:0, env:0) => 5
- 1 hits: "--count, -c" (hit title: "c", spec:0, long:0, env:0) => 3
- 3 hits: "--verbose, -v" (hit title: "v", spec:0, long:0, env:0) => true
- 1 hits: "--debug, -D, --debug-mode" (hit title: "debug", spec:true, long:true, env:false) => true
Another one in Gallary:
Triple D
means --debug --debug --debug
. In ~~debug
mode, triple D
can dump more underlying value structure
inside Option Store
.
The duplicated-flag exception there among others, is expecting because we're in testing.
The values of CLI flags are ignored but ~~cli
can make them raised when dumping. See the snapshot
at #1 - Gallary.
This flag will print the command hierarchical structure:
By default a citation line(s) will be printed at the ends of help screen:
I knew this option is what you want:
auto &cli = cmdr::cli("app2", CMDR_VERSION_STRING, "hedzr",
"Copyright © 2021 by hedzr, All Rights Reserved.",
"A demo app for cmdr-c11 library.",
"$ ~ --help")
// remove "Powered by cmdr-cxx" line
.set_no_cmdr_endings(true)
// customize the last line except cmdr endings
// .set_tail_line("")
.set_no_tail_line(true);
The "Type ...
..." line could be customized by set_tail_line(str)
, so called tail line
,. Or, you can disable
the tail line
by set_no_tail_line(bool)
.
The Powered by ...
line can be disabled by set_no_cmdr_ending
, so-called cmdr-ending
line.
There is a builtin addon yaml-loader
for loading the external config files in the pre-defined directory locations. As
a sample to show you how to write a external loader, yaml-loader
will load and parse the yaml config file and merge it
into Option Store
.
TODO:
conf.d
not processed now.
test-app-c1
demonstrates how to use it:
{
using namespace cmdr::addons::loaders;
cli.set_global_on_loading_externals(yaml_loader{}());
}
The coresponding cmake fragment might be:
#
# For test-app-c1, loading the dependency to yaml-cpp
#
include(loaders/yaml_loader)
add_yaml_loader(test-app2-c1)
This add-on needs a third-part library,
yaml-cpp
, presented.
Inside cmdr-cxx
, there are many optimizable points and some of them in working.
-
enable dim text in terminal
CMDR_DIM=1 ./bin/test-app2-c2 main sub4 bug-bug2
-
--no-color
: do NOT print colorful text with Terminal Escaped Sequences, envvarsPLAIN
orNO_COLOR
available too../bin/test-app2-c2 --no-color PLAIN=1 ./bin/test-app-c2
-
enable very verbose debugging
#define CMDR_ENABLE_VERBOSE_LOG 1 #include <cmdr11/cmdr11.hh>
-
enable unhandled exception handler
cmdr::debug::UnhandledExceptionHookInstaller _ueh{}; // for c++ exceptions cmdr::debug::SigSegVInstaller _ssi{}; // for SIGSEGV ... return cli.run(argc, argv);
-
-hhh
(i.e.--help --help --help
) will print the help screen with those invisible items (the hidden commands and flags). -
Tab-stop position is adjustable based the options automatically
-
The right-side of a line, in the help screen, command/flag decriptions usually, can be wrapped and aligned along the tab-stop width.
-
More...
That's very appreciated!
- cmdr-like programmatical interface.
- See also Short example and Fast Document
- See also test app sources
- A fully functional Hierarchical Configurable Data Management Mechanism (so called
Option Store
) is ready for box. - Uses debug outputting macros:
cmdr_print
,cmdr_debug
,cmdr_trace
(whileCMDR_ENABLE_VERBOSE_LOG
defined), see cmdr_log.hh
gcc 10+: passed
clang 12+: passed
msvc build tool 16.7.2, 16.8.5 (VS2019 or Build Tool) passed
# configure
cmake -DENABLE_AUTOMATE_TESTS=OFF -S . -B build/
# build
cmake --build build/
# install
cmake --build build/ --target install
# sometimes maybe sudo: sudo cmake --build build/ --target install
For msvs build tool, vcpkg should be present, so cmake configure command is:
cmake -DENABLE_AUTOMATE_TESTS=OFF -S . -B build/ -DCMAKE_TOOLCHAIN_FILE=%USERPROFILE%/work/vcpkg/scripts/buildsystems/vcpkg.cmake
If you clone vcvpkg source and bootstrap it at:
%USERPROFILE%/work/vcpkg
.
set VCPKG_DEFAULT_TRIPLET=x64-windows
mkdir %USERPROFILE%/work
cd %USERPROFILE%/work
git clone ...
REM launch `vsbt` build env
SETX PATH "%PATH%;C:\Program Files (x86)\Microsoft Visual Studio\2019\BuildTools\Common7\Tools"
LaunchDevCmd.bat
cd cmdr-cxx
cmake -DENABLE_AUTOMATE_TESTS=OFF -S . -B build/ -DCMAKE_TOOLCHAIN_FILE=%USERPROFILE%/work/vcpkg/scripts/buildsystems/vcpkg.cmake
cmake --build build/
We use ninja for faster building. That's safe to build cmdr-cxx without it.
We use ccache for faster building. That's safe to build cmdr-cxx without it.
BUILD_DOCUMENTATION
=OFFENABLE_TESTS
=OFF
To run all automated tests, or, you're trying to use yaml-loader
add-on, some dependencies need to prepared at first,
by youself, maybe.
If the tests are enabled, Catch2
will be downloaded while cmake configuring and
building automatically. If you have a local cmake-findable Catch2 copy, more attentions would be appreciated.
In our tests, test-app2-c1
and yaml-loader
will request yaml-cpp
is present.
Optional
sudo apt install -y libyaml-cpp-dev
For CentOS or RedHat:
sudo dnf install yaml-cpp yaml-cpp-devel yaml-cpp-static
brew install yaml-cpp
vcpkg install yaml-cpp
NOTE that vcpkg want to inject the control file for cmake building, see also Using vcpkg with CMake
The example executables can be found in ./bin
after built. For example:
# print command tree (with hidden commands)
./bin/cmdr11-cli -hhh ~~tree
You will get them from release page.- TODO: we will build a docker release later.
- Run me from a online CXX IDE.
-
auto & cli = cmdr::get_app()
-
Register actions:
void register_action(opt::Action action, opt::types::on_internal_action const &fn);
In your pre_invoke handler, some actions called
internal actions
could by triggered via the returnedAction
code.The
Action
codes is extensible, followed by aon_internal_action
handler user-customized. -
Hooks
xxx_handlers
ors
(_externals
) means you can specify it multiple times.-
set_global_on_arg_added_handlers
,set_global_on_cmd_added_handlers
-
set_global_on_arg_matched_handlers
,set_global_on_cmd_matched_handlers
-
set_global_on_loading_externals
-
set_global_on_command_not_hooked
cmdr prints some hitting info for a sub-command while no
on_invoke
handler associated with it.Or, you can specify one yours via
set_global_on_command_not_hooked
. -
set_global_on_post_run_handlers
-
set_on_handle_exception_ptr
-
set_global_pre_invoke_handler
,set_global_post_invoke_handler
-
Thanks to JetBrains for donating product licenses to help develop cmdr-cxx
Apache 2.0