Skip to content
/ cmdr-cxx Public

cmdr cxx version, a C++17 header-only command-line parser with hierarchical config data manager here

License

Notifications You must be signed in to change notification settings

hedzr/cmdr-cxx

Repository files navigation

cmdr-cxx {#mainpage}

CMake Build Matrix GitHub tag (latest SemVer)

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.

cover

Features

  • 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)
  • 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
    • ~~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.

Status

CXX 17/20 Compilers

  • 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

Snapshots

cmdr-cxx prints an evident, clear, and logical help-screen. Please proceed the more snapshots at #1 - Gallery.

Bonus

Usages

Local Deployment

Homebrew

cmdr-cxx can be installed from homebrew:

brew install hedzr/brew/cmdr-cxx

CMake Standard

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
dependencies

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/

CMake ExternalProject

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.

Integrate to your cmake script

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)

Short example

#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.

Fast Document

Lookup a command and its flags

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());

Extract the matched information of a flag

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");

Set the value of a config item

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::vars::variable

cmdr-cxx provides stream-io on lots of types via cmdr::vars::variable, take a look for further.

Bundled Utilities or Helpers

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

Features to improve your app arch

cmdr-cxx provides some debugging features or top view to improve you design at CLI-side.

Default Action

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.

User-custom non-hooked action

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

~~debug

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:

image-20210217161825139

-DDD

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.

image-20210211184120423

~~debug --cli -DDD

The values of CLI flags are ignored but ~~cli can make them raised when dumping. See the snapshot at #1 - Gallary.

~~tree

This flag will print the command hierarchical structure:

image-20210222130046734

Remove the cmdr-cxx tail line

By default a citation line(s) will be printed at the ends of help screen:

image-20210215100547030

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.

External Loaders

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.

Specials

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, envvars PLAIN or NO_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...

Use cmdr-cxx As A New App Skeletion

That's very appreciated!

PROs

  • cmdr-like programmatical interface.
  • 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 (while CMDR_ENABLE_VERBOSE_LOG defined), see cmdr_log.hh

Contributions

Build

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.

Windows Server 2019 Core & VSBT

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/

ninja, [Optional]

We use ninja for faster building. That's safe to build cmdr-cxx without it.

ccache, [Optional]

We use ccache for faster building. That's safe to build cmdr-cxx without it.

Other Options

  1. BUILD_DOCUMENTATION=OFF
  2. ENABLE_TESTS=OFF

Prerequisites

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.

Catch2

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.

Others

In our tests, test-app2-c1 and yaml-loader will request yaml-cpp is present.

Optional

Linux
sudo apt install -y libyaml-cpp-dev

For CentOS or RedHat: sudo dnf install yaml-cpp yaml-cpp-devel yaml-cpp-static

macOS
brew install yaml-cpp
Windows
vcpkg install yaml-cpp

NOTE that vcpkg want to inject the control file for cmake building, see also Using vcpkg with CMake

Run the examples

The example executables can be found in ./bin after built. For example:

# print command tree (with hidden commands)
./bin/cmdr11-cli -hhh ~~tree
  1. You will get them from release page.
  2. TODO: we will build a docker release later.
  3. Run me from a online CXX IDE.

Hooks in cmdr-cxx

  1. auto & cli = cmdr::get_app()

  2. 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 returned Action code.

    The Action codes is extensible, followed by a on_internal_action handler user-customized.

  3. Hooks

    xxx_handlers or s(_externals) means you can specify it multiple times.

    1. set_global_on_arg_added_handlers, set_global_on_cmd_added_handlers

    2. set_global_on_arg_matched_handlers, set_global_on_cmd_matched_handlers

    3. set_global_on_loading_externals

    4. 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.

    5. set_global_on_post_run_handlers

    6. set_on_handle_exception_ptr

    7. set_global_pre_invoke_handler, set_global_post_invoke_handler

Thanks to JODL

Thanks to JetBrains for donating product licenses to help develop cmdr-cxx
jetbrains

LICENSE

Apache 2.0