Building a Command-Line Interface for the Duckies App
Book: The C++ Programmer’s Mindset
Author: Sam Morley
ISBN: 978-1-83588-842-1
Chapter 8 is where the rubber duckies project gets a real executable named duckies. Morley builds a minimal but extensible CLI around Boost program_options, spdlog, and OpenMP. Even if you are the only user, he argues, --help pays for itself the first time you forget what flags exist.
Previous: Outlining the Challenge | Next: Reading Data from Different Formats
Project setup
CMake finds OpenMP, Boost program_options, and spdlog. The duckies target starts as a single main.cpp and grows in later chapters. vcpkg manifest is provided for dependency management on any platform.
Morley’s revised main is deliberately boring:
setup_signals();
const auto args = parse_config(argc, argv);
setup_logging(args);
setup_threading(args);
run_and_report(args);
Four phases: config, logging, threading, work. Separation of concerns so adding a new flag does not mean rewriting everything. Chapter 7’s four-function main evolved into this because the original skipped logging, threading, and output format control.
Parsing with Boost program_options
Options defined:
-h/--helpand--version(early exit with synopsis + usage strings)-v/--verbose(logging level)-j/--jobs(thread count, following GNU make convention)- Positional
pathsfor input files (unlimited_arguments)
Environment variables work too via DUCKIES_ prefix (DUCKIES_VERBOSE, DUCKIES_JOBS, etc.). Morley’s point: do not reinvent getenv parsing when the library already does it. Boost can also read config files, though this project does not use that path.
Parse failures print the exception, synopsis, and full options, then exit(EXIT_FAILURE). Missing file paths after parsing? Print synopsis and options, exit failure. Help and version exit success.
Abseil’s program options exist, but Morley picks Boost because it is widely packaged and CMake-friendly.
Logging with spdlog
Default: warnings and errors only to stderr with pattern [YYYY-MM-DD HH:MM:SS] [L] message. Verbose mode drops to info level. A test log line at info level proves the filter works (you will not see it unless -v is set).
Recoverable problems use spdlog::warn (unexpected file format, missing file). spdlog::error for serious issues. spdlog::critical at the top-level catch before exit.
Header-only spdlog keeps linking simple. Console sink uses colored stderr via stderr_color_mt.
Threading with OpenMP
setup_threading caps threads sensibly. Default: 2 threads (conservative). If -j is set, use min(requested, omp_get_max_threads()). Morley explicitly sets his own default rather than trusting OMP_NUM_THREADS. On a 400-core server, uncontrolled OpenMP defaults are a bad idea. Users will not read docs; make flags obvious in --help.
Error handling
Two error classes: recoverable (bad file format, skip and log) and unrecoverable (something exploded deep in the stack). The handle_errors template wraps lambdas:
template <typename F>
void handle_errors(F&& func) noexcept {
try { func(); }
catch (const std::exception& exc) {
spdlog::critical(exc.what());
std::exit(EXIT_FAILURE);
}
}
Marked noexcept because the handler itself must not throw. Lambdas capture by reference for easy use inside run_and_report. Unrecoverable errors propagate up; only the outer shell catches and exits cleanly with a proper code.
Signal handling
Basic SIGINT handler for graceful shutdown on Unix. First Ctrl-C sets an atomic flag checked via check_interrupts() in the main loop. Second Ctrl-C calls std::exit(1) immediately.
Critical detail: do not print from inside the signal handler. operator<< is not reentrant. The handler only increments signal_count; the main thread prints the graceful shutdown message.
Windows signal support is weaker. Morley keeps the handler minimal for portability. Not required for correctness, but a quality-of-life feature for long runs.
Output formatting
Results are cluster centroids as lat/long pairs printed to stdout (logs go to stderr, Unix style). Morley uses C++20 std::format:
std::cout << std::format("{: 7.3f} {: 8.3f}\n", lat, lon);
Three decimal places gives roughly hundred-meter resolution at the equator. ISO 6709 puts latitude first. Aligned columns for humans, still machine-parseable for pipes. The space-before-minus format keeps columns neat when values go negative.
run_and_report is stubbed with an empty results vector for now. Clustering fills it in Chapter 11.
My take
Nothing here is flashy, and that is the point. Good CLI design is predictable flags, helpful errors, and logging you can turn on when things go wrong. The four-phase main is a template worth copying for any batch processing tool.
If you have been hand-rolling argv parsing, Boost program_options plus this structure is a solid upgrade. The namespace po = boost::program_options alias and static usage strings are small touches that keep main.cpp readable as options grow.
Chapter 9 plugs file readers into run_and_report. Until then, you can build and run with --help to verify the skeleton works.
Previous: Outlining the Challenge | Next: Reading Data from Different Formats