Command-Line Programs in C++
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
C++ has no standard argument parser, no standard way to ask whether a stream is a terminal and no standard colour API. What the language does provide, since C17 and C23, is a portable path type, a type-safe formatting library and a print function; everything else comes from the platform or from a library. This page builds the shelf command-line tool of the Bookshelf domain around that split: a third-party library for the command line, the standard library for output and paths, and a small amount of platform code where the standard stops.
The page was written in October 2026 against these versions:
-
C++23 with
std::print,std::printlnandstd::format, andstd::filesystemfrom C++17. The standard library snippets were compiled and run with Apple clang 21 on macOS in C++23 mode. They were not compiled with MSVC or GCC. -
CLI11 v2.7.2, cxxopts v3.3.1 and Boost.Program_options from Boost 1.92. The CLI11, cxxopts and Boost snippets were not compiled while this page was written, because the libraries were not installed; they follow the documentation of each project, which is cited after every example.
-
fmt, the library the standard formatting facilities were standardized from, and CMake, vcpkg and Conan 2 for the build. The
CMakeLists.txt,vcpkg.jsonandconanfile.txtfiles were not run either.
The running example is shelf list, shelf add and shelf loan. How the process starts, what argv is and how
signals and file descriptors work are covered in POSIX Processes,
Signals & I/O and Windows Console & Processes; the language
itself is covered in the C++ Reference, and the C version of this page is
Command-Line Programs in C. Design rules for the command line (flags,
streams, exit codes, help text) are in CLI Design Fundamentals
and are not repeated here.
What the Official Documentation Provides, and What It Does Not
-
The C++ standard defines
main,std::exit, the formatting and print functions andstd::filesystem. It does not define argument parsing, terminal detection, colour, signals beyond a minimal<csignal>, or an environment for the current user’s configuration directory. -
None of the three argument parsers is part of any standard. Each project documents itself: CLI11 has a book, cxxopts has a README, and Boost.Program_options has a reference and tutorial. They do not publish benchmarks or a comparison, so the table below is assembled from their documentation and contains only features each project documents.
-
The Boost.Program_options documentation does not describe subcommands. The pattern shown later is built from documented pieces and is a convention, not a feature.
-
There is no official guidance from the compiler vendors on colour or terminal detection.
NO_COLORis a community convention described at https://no-color.org; see Escape Sequences & Terminal Capabilities for what is and is not standardized.
Arguments Arrive as argc and argv
A C++ program starts in main, whose two portable forms are int main() and int main(int argc, char *argv[]).
The array holds argc pointers to null-terminated strings, argv[0] is implementation-defined (usually the program
name) and argv[argc] is a null pointer. Everything that follows is built on that array.
-
Linux and macOS: the strings are the raw bytes the kernel passed, usually UTF-8 but not guaranteed. The shell has already expanded globs and removed quotes, so quoting the title or not gives the same
argv. -
Windows: the process receives one command-line string, and the C runtime splits it into
argvusing the rules of the compiler’s runtime. The strings are narrow and use the process’s active code page (not UTF-8) unless the executable carries the UTF-8 manifest.int wmain(int argc, wchar_t *argv[])is Microsoft’s wide-character entry point. The splitting rules, the manifest and the pitfalls are in Windows Console & Processes.
Parsing the array by hand is fine for one flag. Beyond that, the conventions (-v and --verbose, --name=value,
combined short flags such as -abc, -- to end options, generated --help) are better left to a library.
Three Parsers Compared
CLI11, cxxopts and Boost.Program_options are the three libraries most C++ command-line tools choose from. They differ less in syntax than in what they take on: CLI11 models a whole command tree, cxxopts parses one flat list of options, and Boost.Program_options models options from several sources and leaves the command structure to the program.
| CLI11 | cxxopts | Boost.Program_options | |
|---|---|---|---|
Distribution |
Header-only; a single-header form is also provided |
Header-only (one header, |
A compiled library that must be built and linked |
Licence |
BSD 3-Clause |
MIT |
Boost Software License 1.0 |
Subcommands |
Yes: |
No; one flat set of options |
Not described in the documentation; built by the program (see below) |
Validation |
Built-in validators ( |
Type conversion only ( |
Type conversion, |
Config files |
Yes: |
No |
Yes: |
Environment variables |
Yes: |
No |
Yes: |
Generated help |
Yes, including subcommand help and |
Yes: |
Yes: print the |
Wide strings |
Narrow strings; see the CLI11 book for Windows Unicode handling |
Narrow strings |
Wide-character variants such as |
Source precedence |
Set per option; see the precedence rules in the CLI11 book |
Not applicable |
The first source stored wins, so store the command line first |
The sources of the table are the three projects' own documents, cited after the examples. Where a cell says "no" it means the documentation does not describe the feature, and a program can always add it by hand.
The flow is a starting point. A tool with a handful of flags is served by cxxopts; a tool shaped like git or shelf
(list, add, loan) is what CLI11 is designed for.
CLI11
CLI11 describes the command line as a tree of CLI::App objects. The root app is the program, each
add_subcommand call adds a child, and add_option and add_flag bind a command-line option directly to a
variable. CLI11_PARSE(app, argc, argv) is a macro that parses, prints help or the error message and returns the exit
code from main when parsing fails or --help was requested.
// src/main.cpp -- CLI11 v2.7.2 (not compiled while this page was written)
#include <CLI/CLI.hpp>
#include <algorithm>
#include <cctype>
#include <cstddef>
#include <print>
#include <string>
namespace {
// A custom validator: returns an empty string on success, a message on failure.
// It may also rewrite the value, here to strip hyphens.
const CLI::Validator isbn13{
[](std::string& value) -> std::string {
std::string digits;
for (char c : value) {
if (c != '-') digits += c;
}
const bool numeric = std::all_of(digits.begin(), digits.end(),
[](unsigned char c) { return std::isdigit(c) != 0; });
if (digits.size() != 13 || !numeric) return "an ISBN-13 has 13 digits";
int sum = 0;
for (std::size_t i = 0; i < 13; ++i) {
sum += (digits[i] - '0') * (i % 2 == 0 ? 1 : 3);
}
if (sum % 10 != 0) return "the ISBN-13 check digit is wrong";
value = digits;
return {};
},
"ISBN-13", "isbn13"};
} // namespace
int main(int argc, char** argv) {
CLI::App app{"Bookshelf catalogue", "shelf"};
app.set_version_flag("--version", "shelf 1.0.0");
app.set_config("--config", "shelf.toml", "Read options from a TOML file");
app.require_subcommand(1);
// shelf list [--author NAME] [--limit N] [--json]
auto* list = app.add_subcommand("list", "List books");
std::string author;
int limit = 20;
bool json = false;
list->add_option("-a,--author", author, "Only books by this author");
list->add_option("-n,--limit", limit, "Maximum number of rows")
->check(CLI::Range(1, 1000))
->envname("SHELF_LIMIT")
->capture_default_str();
list->add_flag("--json", json, "Print JSON instead of a table");
// shelf add TITLE --isbn ISBN
auto* add = app.add_subcommand("add", "Add a book to the catalogue");
std::string title;
std::string isbn;
add->add_option("title", title, "Title of the book")->required();
add->add_option("--isbn", isbn, "ISBN-13, hyphens allowed")->required()->check(isbn13);
// shelf loan ISBN --to MEMBER
auto* loan = app.add_subcommand("loan", "Lend a book to a member");
std::string loan_isbn;
std::string member;
loan->add_option("isbn", loan_isbn, "ISBN-13 of the book")->required()->check(isbn13);
loan->add_option("--to", member, "Member who borrows the book")->required();
CLI11_PARSE(app, argc, argv);
if (list->parsed()) {
std::println("list: author='{}' limit={} json={}", author, limit, json);
} else if (add->parsed()) {
std::println("add: '{}' ({})", title, isbn);
} else if (loan->parsed()) {
std::println("loan: {} to {}", loan_isbn, member);
}
return 0;
}
Source: CLI11 book (chapters on options, subcommands, validators, config files and
CLI11_PARSE).
A matching configuration file sets defaults without any code. CLI11 reads the file named by --config, or
shelf.toml when --config is absent and the file exists; the book describes how a subcommand’s options map to a
section named after it.
# shelf.toml
[list]
limit = 50
A validation failure, an unknown option or a missing required value makes CLI11_PARSE print a message and return a
non-zero status; --help and --version print and return 0. The statuses for each class of parse error are defined
by CLI11’s CLI::ExitCodes enumeration, so scripts that need to tell them apart should read the book rather than
hard-code numbers.
Features worth knowing about:
-
->envname("SHELF_LIMIT")lets an environment variable supply a value, which is the usual home for secrets and CI settings (see Configuration & Authentication for precedence). -
->excludes()and->needs()express relations between options.->transform()and->each()run user code on values. -
app.allow_extras()keeps unrecognised arguments for the program instead of failing, which is how a wrapper passes the rest of the command line to another process. -
CLI11 can also be used without
CLI11_PARSE: callapp.parse(argc, argv)inside atryblock, catchCLI::ParseErrorand returnapp.exit(e). Do that when the program must run cleanup or log before exiting.
cxxopts
cxxopts is a single header. It has no commands: shelf list would be the program shelf-list, or the program looks at
the first argument itself and hands the rest to a parser per command. It supports the usual long and short forms,
--name=value, grouped short flags, default and implicit values and positional arguments.
// list.cpp -- cxxopts v3.3.1 (not compiled while this page was written)
#include <cxxopts.hpp>
#include <exception>
#include <iostream>
#include <print>
#include <string>
int main(int argc, char** argv) {
cxxopts::Options options("shelf list", "List the books in the catalogue");
options.add_options()
("a,author", "Only books by this author", cxxopts::value<std::string>())
("n,limit", "Maximum number of rows", cxxopts::value<int>()->default_value("20"))
("json", "Print JSON instead of a table")
("h,help", "Print usage");
try {
const auto result = options.parse(argc, argv);
if (result.count("help") > 0) {
std::cout << options.help() << '\n';
return 0;
}
const int limit = result["limit"].as<int>();
if (limit < 1 || limit > 1000) {
std::println(stderr, "shelf: --limit must be between 1 and 1000");
return 2;
}
const std::string author = result.count("author") > 0 ? result["author"].as<std::string>() : "";
std::println("list: author='{}' limit={} json={}", author, limit, result.count("json") > 0);
return 0;
} catch (const std::exception& e) {
std::println(stderr, "shelf: {}", e.what());
return 2;
}
}
Source: cxxopts README (quick start, positional arguments, default and implicit values).
The parser throws on a missing value, an unknown option or a failed conversion; catching std::exception works for
every version of the library. The range check is the program’s own, which is the price of the smaller feature set.
Boost.Program_options
Boost.Program_options separates three steps: describe the options in an options_description, parse a source
into a collection of options, and store them into a variables_map. Several sources can be parsed and stored into the
same map, and a call to notify finally copies the values into variables and raises errors for missing required
options. The library is compiled, so the build must link Boost::program_options.
// list.cpp -- Boost 1.92 Program_options (not compiled while this page was written)
#include <boost/program_options.hpp>
#include <iostream>
#include <print>
#include <string>
namespace po = boost::program_options;
int main(int argc, char** argv) {
po::options_description desc("Options");
desc.add_options()
("help,h", "print usage")
("author,a", po::value<std::string>(), "only books by this author")
("limit,n", po::value<int>()->default_value(20), "maximum number of rows")
("json", po::bool_switch(), "print JSON instead of a table");
po::variables_map vm;
try {
// The first source stored wins: command line, then environment (SHELF_LIMIT).
po::store(po::parse_command_line(argc, argv, desc), vm);
po::store(po::parse_environment(desc, "SHELF_"), vm);
po::notify(vm);
} catch (const po::error& e) {
std::println(stderr, "shelf: {}", e.what());
return 2;
}
if (vm.count("help") > 0) {
std::cout << desc << '\n';
return 0;
}
const std::string author = vm.count("author") > 0 ? vm["author"].as<std::string>() : "";
std::println("list: author='{}' limit={} json={}", author, vm["limit"].as<int>(), vm["json"].as<bool>());
return 0;
}
Source: Boost.Program_options documentation
(tutorial, parse_environment, parse_config_file, positional_options_description).
Because the documentation has no subcommand feature, programs that need one use the pieces it does document: parse
with a first, loose description that stops at the command word, take the leftover tokens with
po::collect_unrecognized(parsed.options, po::include_positional) and parse them again with the description of that
command. This works, but it is an application pattern; if more than two commands are involved, CLI11 is the shorter
route.
Output with std::print and fmt
C++23 adds std::print and std::println to <print>, built on the std::format machinery of C++20. They write
to stdout by default or to any FILE*, so diagnostics go to stderr with std::println(stderr, ...). Compared with
iostreams they are type-checked at compile time, locale-independent unless asked, and a whole message is one call
instead of a chain of << operators.
#include <format>
#include <print>
#include <string>
int main() {
std::println("{:<28} {:<18} {:>4}", "Title", "Author", "Year"); // aligned columns
std::println("{:<28} {:<18} {:>4}", "Dune", "Frank Herbert", 1965);
std::println(stderr, "shelf: {} books skipped", 2); // diagnostics on stderr
const std::string line = std::format("{:>6.1f}%", 99.5); // a string, not printed
std::println("{}", line);
}
Source: std::print and std::format.
Points that matter for command-line programs:
-
Unicode on Windows. The standard describes
std::printas writing UTF-8 text and, when the stream is a terminal, using the platform’s native Unicode API. Check the behaviour of the standard library in use on a legacy console, and see Windows Console & Processes for the UTF-8 manifest and code page settings. -
Compiler support.
<print>needs a recent standard library. Check the compiler support table on cppreference before depending on it; where it is missing, fmt providesfmt::printwith the same format strings. -
fmt extras. fmt is where
std::formatcame from and still offers more:fmt::printto anyFILE*, text styles in<fmt/color.h>such asfmt::fg(fmt::color::red)andfmt::emphasis::bold, compile-time checked format strings and ranges formatting. Text styles write escape sequences unconditionally, so the program decides whether to use them (see below).
Source: fmt documentation.
Portable Paths with std::filesystem
std::filesystem::path hides the separator (/ on Linux and macOS, \ on Windows, where / is also accepted), the
character type (char on POSIX and wchar_t on Windows) and the root syntax. The / operator joins components, so
fs::path{"data"} / "books.csv" is correct on every system. exists, file_size, create_directories,
weakly_canonical and the directory iterators cover most of what a command-line tool needs.
Two details bite in practice:
-
A path is not printable.
std::format("{}", path)does not compile before C++26: with the Apple clang 21 standard library, formatting astd::filesystem::pathfails with a deleted default constructor offormatter<std::filesystem::path, char>. Convert it first. -
path::string()is not UTF-8 on Windows. It converts to the active code page, so a path such asBiblioteca\Cien años.csvcan lose characters.generic_u8string()returns astd::u8stringin C++20 and later; copying its bytes into astd::stringgives UTF-8 text on every platform, as the helperutf8in the next section does.
Source: Filesystem library and std::filesystem::path.
Where the configuration file lives is not a std::filesystem question: the standard has no concept of a per-user
configuration directory. The XDG directories on Linux, ~/Library/Application Support on macOS and %APPDATA% on
Windows are covered in Cross-Platform Portability.
Exit Codes and Exceptions at the Top of main
Returning from main runs the destructors of its local objects, flushes the standard streams and reports the value as
the exit status; calling std::exit does not destroy the locals of main, and an exception that escapes main ends
in std::terminate. For a tool that other programs call, an abnormal termination is the worst result: no message, an
exit status that means "crashed", and an implementation-defined choice of whether the stack is unwound at all. The fix
is a single function that does the work and a main that catches everything and maps each failure to a documented
status.
On POSIX only the low eight bits of the status reach the parent (exit(256) looks like 0), and the shell sets $?.
On Windows the status is a 32-bit value read through %ERRORLEVEL% or $LASTEXITCODE. Staying between 0 and 125 is
portable; the values above are reserved by shells for signals and "command not found". The set of codes below is the
tool’s own contract; sysexits.h exists on Linux and macOS but not on Windows, so a portable program defines its own.
The complete, runnable program below ties the pieces of this page together: an exit-code enumeration, a UsageError,
paths printed as UTF-8, colour decided from the environment and the stream, and the top-level handlers.
// shelf_main.cpp -- build: c++ -std=c++23 shelf_main.cpp -o shelf
#include <cstdio>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <format>
#include <print>
#include <stdexcept>
#include <string>
#include <string_view>
#ifdef _WIN32
# include <io.h>
# define SHELF_ISATTY(f) _isatty(_fileno(f))
#else
# include <unistd.h>
# define SHELF_ISATTY(f) isatty(fileno(f))
#endif
namespace fs = std::filesystem;
enum ExitCode : int { ok = 0, failure = 1, usage = 2, not_found = 3 };
struct UsageError : std::runtime_error {
using std::runtime_error::runtime_error;
};
static bool use_colour(std::FILE* stream) {
if (const char* v = std::getenv("NO_COLOR"); v != nullptr && *v != '\0') return false;
if (const char* v = std::getenv("FORCE_COLOR"); v != nullptr && *v != '\0') return true;
if (const char* t = std::getenv("TERM"); t != nullptr && std::string_view{t} == "dumb") return false;
return SHELF_ISATTY(stream) != 0;
}
// path::string() uses the ANSI code page on Windows; go through UTF-8 instead.
static std::string utf8(const fs::path& p) {
const auto u = p.generic_u8string();
return {u.begin(), u.end()};
}
static int run(int argc, char** argv) {
if (argc < 2) throw UsageError{"missing command (try: shelf list)"};
const std::string_view command = argv[1];
if (command != "list") throw UsageError{std::format("unknown command '{}'", command)};
const fs::path catalogue = argc > 2 ? fs::path{argv[2]} : fs::path{"data"} / "books.csv";
if (!fs::exists(catalogue)) {
std::println(stderr, "shelf: no such catalogue: {}", utf8(catalogue));
return not_found;
}
const bool colour = use_colour(stdout);
const std::string_view heading = colour ? "\x1b[1m" : "";
const std::string_view reset = colour ? "\x1b[0m" : "";
std::println("{}Catalogue{} {} ({} bytes)", heading, reset, utf8(catalogue),
fs::file_size(catalogue));
return ok;
}
int main(int argc, char** argv) {
try {
return run(argc, argv);
} catch (const UsageError& e) {
std::println(stderr, "shelf: {}", e.what());
return usage;
} catch (const fs::filesystem_error& e) {
std::println(stderr, "shelf: {}: {}", utf8(e.path1()), e.code().message());
return failure;
} catch (const std::exception& e) {
std::println(stderr, "shelf: internal error: {}", e.what());
return failure;
} catch (...) {
std::println(stderr, "shelf: unknown internal error");
return failure;
}
}
Source: std::exit, std::terminate and std::filesystem::filesystem_error.
This program was compiled with -Wall -Wextra and run on macOS. A session with the catalogue in data/books.csv
shows the exit statuses, the colour decision and the diagnostics going to stderr:
$ ./shelf list
Catalogue data/books.csv (4 bytes)
$ FORCE_COLOR=1 ./shelf list | cat -v
^[[1mCatalogue^[[0m data/books.csv (4 bytes)
$ ./shelf list | cat -v
Catalogue data/books.csv (4 bytes)
$ ./shelf zap; echo "status=$?"
shelf: unknown command 'zap'
status=2
$ ./shelf list nope; echo "status=$?"
shelf: no such catalogue: nope
status=3
The first command runs in a terminal, so the heading is bold (the escape sequence is not visible in the output). The
third is a pipe, so isatty returns false and no escape sequence is written.
Colour and TTY Detection
Neither the language nor the standard library knows what a terminal is, so the check is a platform call on the
descriptor behind the stream: isatty(fileno(stdout)) from <unistd.h> on Linux and macOS, _isatty(_fileno(stdout))
from <io.h> on Windows (the program above hides the difference in one macro). The result is per stream:
shelf list > out.txt has a terminal on stderr but not on stdout, and each stream is decided separately.
The decision order used by use_colour is the common one, shared with the other languages of this section:
-
NO_COLORset to any non-empty value: no colour. -
FORCE_COLORset to a non-empty value: colour, even into a pipe. -
TERM=dumb: no colour. -
Otherwise: colour only if the stream is a terminal.
Add a --color=auto|always|never option for the user to override all of this. NO_COLOR and FORCE_COLOR are
community conventions rather than standards (FORCE_COLOR in particular has no single specification), which is why the
option matters. The reasoning behind the order, and what to do for accessible output, is in
TTY, Colour & Interactivity.
Windows needs one more step: the console only interprets escape sequences after the program enables
ENABLE_VIRTUAL_TERMINAL_PROCESSING with SetConsoleMode, unless the host is Windows Terminal or another terminal that
already does. fmt and the standard library do not do that for the program. The call, and the cases where it is not
needed, are in Windows Console & Processes.
Source: NO_COLOR and Console virtual terminal sequences.
Building with CMake, vcpkg and Conan
CMake describes the build; vcpkg and Conan supply the libraries. The recipe is the same for all three parsers: declare the dependency, find it and link its imported target. The general CMake and package-manager workflow is introduced in C++ Build & Tooling; this section adds what is specific to a command-line program.
# CMakeLists.txt
cmake_minimum_required(VERSION 3.25)
project(shelf VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
find_package(CLI11 CONFIG REQUIRED)
# cxxopts: find_package(cxxopts CONFIG REQUIRED) -> cxxopts::cxxopts
# Boost: find_package(Boost REQUIRED COMPONENTS program_options) -> Boost::program_options
add_executable(shelf src/main.cpp)
target_link_libraries(shelf PRIVATE CLI11::CLI11)
install(TARGETS shelf RUNTIME DESTINATION bin)
With vcpkg in manifest mode the dependency list lives next to the code, and the toolchain file makes
find_package find it:
{
"name": "shelf",
"version": "1.0.0",
"dependencies": ["cli11", "fmt"]
}
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake"
cmake --build build
With Conan 2 the equivalent is a conanfile.txt and a generated toolchain:
[requires]
cli11/[>=2.7 <3]
[generators]
CMakeDeps
CMakeToolchain
conan install . --output-folder=build --build=missing -s build_type=Release
cmake -B build -S . -DCMAKE_BUILD_TYPE=Release -DCMAKE_TOOLCHAIN_FILE=build/conan_toolchain.cmake
cmake --build build
Source: CMake documentation, vcpkg documentation and Conan 2 documentation.
Port and package names (cli11, cxxopts, boost-program-options, fmt) and the version in the Conan file depend on
the package index at the time, so search the vcpkg and Conan catalogues for the current names before copying them.
Visual Studio and Xcode generators are multi-configuration, so there -DCMAKE_BUILD_TYPE is ignored and the
configuration is chosen with cmake --build build --config Release. How to turn the result into a signed, relocatable
binary for each platform is the subject of
Native Binaries & Cross-Compilation.
Windows, macOS and Linux at a Glance
| Topic | Windows | macOS | Linux |
|---|---|---|---|
|
Active code page, unless the UTF-8 manifest is embedded; |
Raw bytes, normally UTF-8 |
Raw bytes, normally UTF-8 |
|
|
|
|
Colour escape sequences |
Need |
Terminal.app, iTerm2 and others interpret them |
Almost all terminals; |
|
Active code page: use |
UTF-8 |
UTF-8 |
Exit status |
32-bit, |
Low 8 bits, |
Low 8 bits, |
Standard library |
MSVC STL, selected by the compiler version |
libc++ from Xcode |
libstdc (GCC) or libc (Clang) |
Console subsystem |
|
Not applicable |
Not applicable |
Pitfalls
-
Parsing
argvby hand and then growing. A hand-written loop is fine for one flag. When the second command appears, switch to a library before the code is entangled with the parsing. -
Throwing out of
mainor a static initializer. The status is whatever the runtime picks, and some stacks are not unwound. Catch at the top and return a documented status. -
Printing a
std::filesystem::pathorpath::string()on Windows. Use a UTF-8 conversion such as theutf8helper above. -
Unconditional escape sequences. A text style from fmt, or a literal
\x1b[1mstring, written into a pipe or a file ends up in the user’s data. Gate colour onuse_colourfor the stream being written. -
Mixing
std::coutandstd::printon the same stream without care. They share the underlying C stream by default, but code that disables synchronization withstd::ios::sync_with_stdio(false)must not mix the two. -
Storing the config file before the command line in Boost.Program_options. The first stored value wins, so the order of
storecalls is the precedence order. -
Trusting a validator for security. Range and ISBN checks give good error messages; they do not make the value safe to put in a shell command or an escape sequence. Printing user-supplied text to a terminal has its own risks, described in Escape Sequences & Terminal Capabilities.
-
Reading
SIGPIPEas an error.shelf list | head -1closes the pipe early and, on POSIX, the default action terminates the program. Whether to ignore it and handle the failed write is covered in POSIX Processes, Signals & I/O.
Full-Screen Interfaces
Everything on this page writes lines and exits. A tool that takes over the screen, such as the shelf tui catalogue
browser with its list, details pane and search box, needs a different library and a different structure; for C++
that is FTXUI (and ncurses or PDCursesMod), covered in
C & C++ TUIs: ncurses, PDCurses & FTXUI. The concepts behind
them are in TUI Architecture & Concepts.