Domain Track
Difficulty 2/5CLI Tools in C++
C++ for command-line tools — argument parsing, terminal output, progress bars, readline, piped I/O, exit codes, and signal handling.
TL;DR
Building CLI tools in C++ means: parse arguments (use a library), detect terminal capabilities, handle signals gracefully, and follow Unix conventions (exit codes, stdin/stdout/stderr, pipes).
Argument Parsing
Manual (Simple Tools)
cpp
#include <span>
#include <string_view>
struct Options {
std::string input;
std::string output;
bool verbose = false;
int jobs = 1;
};
Options parse_args(int argc, char* argv[]) {
Options opts;
auto args = std::span{argv + 1, static_cast<size_t>(argc - 1)};
for (size_t i = 0; i < args.size(); ++i) {
std::string_view arg = args[i];
if (arg == "-v" || arg == "--verbose") {
opts.verbose = true;
} else if (arg == "-j" || arg == "--jobs") {
if (++i >= args.size()) throw std::runtime_error("--jobs requires a value");
std::from_chars(args[i], args[i] + std::strlen(args[i]), opts.jobs);
} else if (arg == "-o" || arg == "--output") {
if (++i >= args.size()) throw std::runtime_error("--output requires a value");
opts.output = args[i];
} else if (arg.starts_with('-')) {
throw std::runtime_error("unknown option: " + std::string{arg});
} else {
opts.input = std::string{arg};
}
}
return opts;
}CLI11 (Popular Library)
cpp
// With CLI11 (header-only):
#include <CLI/CLI.hpp>
int main(int argc, char* argv[]) {
CLI::App app{"My Tool", "mytool"};
std::string input, output;
bool verbose = false;
int jobs = 1;
app.add_option("input", input, "Input file")->required();
app.add_option("-o,--output", output, "Output file")->required();
app.add_flag("-v,--verbose", verbose, "Verbose output");
app.add_option("-j,--jobs", jobs, "Parallel jobs")->default_val(1);
CLI11_PARSE(app, argc, argv); // handles --help, errors, etc.
if (verbose) std::println("input: {}", input);
return 0;
}Terminal Detection and ANSI Colors
cpp
#include <unistd.h>
bool is_tty() { return isatty(STDOUT_FILENO); }
namespace color {
const char* reset = "\033[0m";
const char* red = "\033[31m";
const char* green = "\033[32m";
const char* yellow = "\033[33m";
const char* blue = "\033[34m";
const char* bold = "\033[1m";
const char* dim = "\033[2m";
}
void print_status(bool ok, std::string_view msg) {
if (is_tty()) {
// Colorized output for terminal
std::println("{}[{}]{} {}",
ok ? color::green : color::red,
ok ? "OK" : "FAIL",
color::reset, msg);
} else {
// Plain output for pipes/files
std::println("[{}] {}", ok ? "OK" : "FAIL", msg);
}
}Progress Bar
cpp
class ProgressBar {
size_t total_;
size_t current_ = 0;
int width_ = 40;
public:
explicit ProgressBar(size_t total) : total_{total} {}
void update(size_t n = 1) {
current_ = std::min(current_ + n, total_);
render();
}
void render() const {
if (!isatty(STDOUT_FILENO)) return;
double pct = total_ > 0 ? static_cast<double>(current_) / total_ : 0.0;
int filled = static_cast<int>(pct * width_);
std::print("\r[");
for (int i = 0; i < width_; ++i)
std::print("{}", i < filled ? '#' : '.');
std::print("] {:.0f}% ({}/{})", pct * 100.0, current_, total_);
std::cout.flush();
if (current_ == total_) std::println("");
}
};
ProgressBar pb{100};
for (int i = 0; i < 100; ++i) {
process_item(i);
pb.update();
}Piped I/O
cpp
// Detect if stdin has data (piped)
bool has_piped_input() { return !isatty(STDIN_FILENO); }
// Process lines from stdin
void process_stdin() {
for (std::string line; std::getline(std::cin, line); ) {
process_line(line);
}
}
// Unix tool pattern: file args or stdin
int main(int argc, char* argv[]) {
if (argc > 1) {
// Process files
for (int i = 1; i < argc; ++i) {
std::ifstream f{argv[i]};
if (!f) { std::println(stderr, "cannot open: {}", argv[i]); continue; }
for (std::string line; std::getline(f, line); )
process_line(line);
}
} else {
// Fall back to stdin
process_stdin();
}
}Exit Codes (Unix Conventions)
cpp
// Standard exit codes
#include <cstdlib>
// 0: success
return EXIT_SUCCESS; // or return 0;
// 1: general error
return EXIT_FAILURE; // or return 1;
// 2: wrong usage (bad arguments)
if (argc < 2) {
std::println(stderr, "usage: {} <file>", argv[0]);
return 2;
}
// 127: command not found (used by shells)
// 128+N: terminated by signal N
// exit() vs return from main: both call atexit() handlers
// Quick_exit (C++11): no destructors, no atexit
std::quick_exit(1); // emergency exitSignal Handling
cpp
#include <signal.h>
#include <atomic>
std::atomic<bool> g_interrupted{false};
extern "C" void on_signal(int) {
g_interrupted.store(true, std::memory_order_relaxed);
}
void setup_signals() {
struct sigaction sa{};
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
sigaction(SIGINT, &sa, nullptr); // Ctrl+C
sigaction(SIGTERM, &sa, nullptr); // kill
}
int main() {
setup_signals();
for (size_t i = 0; !g_interrupted.load(); ++i) {
process_item(i);
}
if (g_interrupted) std::println(stderr, "\ninterrupted");
return g_interrupted ? 130 : 0; // 128 + SIGINT(2) = 130 convention
}Configuration Files
cpp
// Read a simple KEY=VALUE config file
std::unordered_map<std::string, std::string> read_config(const std::filesystem::path& p) {
std::unordered_map<std::string, std::string> cfg;
std::ifstream f{p};
for (std::string line; std::getline(f, line); ) {
// Strip comments and blank lines
if (auto pos = line.find('#'); pos != std::string::npos)
line.resize(pos);
if (line.find_first_not_of(" \t\r\n") == std::string::npos)
continue;
// Parse KEY=VALUE
auto eq = line.find('=');
if (eq == std::string::npos) continue;
auto key = line.substr(0, eq);
auto val = line.substr(eq + 1);
cfg[key] = val;
}
return cfg;
}
// Priority: defaults → config file → environment → CLI args
auto config = read_config(find_config_path());
if (auto* env = std::getenv("MY_OUTPUT")) config["output"] = env;
// then override from CLI args...