395 lines
9.8 KiB
C++
395 lines
9.8 KiB
C++
/*
|
|
This is a streamlined version of CLI11.hpp, stripping out the functionality not
|
|
used by this program.
|
|
*/
|
|
|
|
#pragma once
|
|
#include <functional>
|
|
#include <iostream>
|
|
#include <map>
|
|
#include <sstream>
|
|
#include <stdexcept>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// Forward declaration
|
|
class ParseError : public std::runtime_error {
|
|
public:
|
|
ParseError(const std::string &msg) : std::runtime_error(msg) {}
|
|
};
|
|
|
|
class App {
|
|
private:
|
|
class Option {
|
|
public:
|
|
std::string name;
|
|
std::string description;
|
|
std::string *value_ptr = nullptr;
|
|
bool is_required = false;
|
|
|
|
Option *required() {
|
|
is_required = true;
|
|
return this;
|
|
}
|
|
};
|
|
|
|
class Subcommand {
|
|
public:
|
|
std::string name;
|
|
std::string description;
|
|
std::vector<std::string> aliases;
|
|
std::function<void()> callback_fn;
|
|
std::map<std::string, Option> options;
|
|
std::map<std::string, Subcommand> subcommands;
|
|
bool requires_subcommand = false;
|
|
|
|
Subcommand() = default;
|
|
|
|
Subcommand(const std::string &n, const std::string &desc)
|
|
: name(n), description(desc) {}
|
|
|
|
Subcommand *alias(const std::string &alias_name) {
|
|
aliases.push_back(alias_name);
|
|
return this;
|
|
}
|
|
|
|
Subcommand *callback(std::function<void()> cb) {
|
|
callback_fn = std::move(cb);
|
|
return this;
|
|
}
|
|
|
|
void require_subcommand() { requires_subcommand = true; }
|
|
|
|
Option *add_option(const std::string &opt_name, std::string &value,
|
|
const std::string &opt_description) {
|
|
auto &opt = options[opt_name];
|
|
opt.name = opt_name;
|
|
opt.description = opt_description;
|
|
opt.value_ptr = &value;
|
|
return &opt;
|
|
}
|
|
|
|
Subcommand *add_subcommand(const std::string &cmd_name,
|
|
const std::string &cmd_description) {
|
|
auto &cmd = subcommands[cmd_name];
|
|
cmd.name = cmd_name;
|
|
cmd.description = cmd_description;
|
|
return &cmd;
|
|
}
|
|
|
|
std::string help() const {
|
|
std::ostringstream ss;
|
|
ss << "Usage: " << name;
|
|
|
|
// Show options in usage
|
|
for (const auto &[opt_name, opt] : options) {
|
|
ss << " ";
|
|
if (opt.is_required) {
|
|
ss << "<" << opt_name << ">";
|
|
} else {
|
|
ss << "[" << opt_name << "]";
|
|
}
|
|
}
|
|
ss << "\n\n";
|
|
|
|
if (!description.empty()) {
|
|
ss << description << "\n\n";
|
|
}
|
|
|
|
// Show options detail
|
|
if (!options.empty()) {
|
|
ss << "Arguments:\n";
|
|
for (const auto &[opt_name, opt] : options) {
|
|
ss << " " << opt_name;
|
|
if (opt.is_required) {
|
|
ss << " (required)";
|
|
}
|
|
if (!opt.description.empty()) {
|
|
ss << ": " << opt.description;
|
|
}
|
|
ss << "\n";
|
|
}
|
|
ss << "\n";
|
|
}
|
|
|
|
// Show subcommands if any
|
|
if (!subcommands.empty()) {
|
|
ss << "Subcommands:\n";
|
|
for (const auto &[name, cmd] : subcommands) {
|
|
ss << " " << name;
|
|
if (!cmd.aliases.empty()) {
|
|
ss << " (";
|
|
for (size_t i = 0; i < cmd.aliases.size(); ++i) {
|
|
if (i > 0)
|
|
ss << ", ";
|
|
ss << cmd.aliases[i];
|
|
}
|
|
ss << ")";
|
|
}
|
|
ss << ": " << cmd.description << "\n";
|
|
}
|
|
}
|
|
|
|
return ss.str();
|
|
}
|
|
};
|
|
|
|
std::string name;
|
|
std::string description;
|
|
std::string footer_text;
|
|
std::map<std::string, Option> options;
|
|
std::map<std::string, Subcommand> subcommands;
|
|
bool fallthrough_flag = false;
|
|
|
|
public:
|
|
App(const std::string &desc = "") : description(desc) {}
|
|
|
|
Subcommand *add_subcommand(const std::string &name,
|
|
const std::string &description) {
|
|
auto &cmd = subcommands[name];
|
|
cmd.name = name;
|
|
cmd.description = description;
|
|
return &cmd;
|
|
}
|
|
|
|
App *fallthrough(bool value = true) {
|
|
fallthrough_flag = value;
|
|
return this;
|
|
}
|
|
|
|
void footer(const std::string &footer) { footer_text = footer; }
|
|
|
|
Subcommand *get_subcommand(const std::string &name) {
|
|
// Try direct name match
|
|
auto it = subcommands.find(name);
|
|
if (it != subcommands.end()) {
|
|
return &it->second;
|
|
}
|
|
|
|
// Try aliases
|
|
for (auto &[_, cmd] : subcommands) {
|
|
if (std::find(cmd.aliases.begin(), cmd.aliases.end(), name) !=
|
|
cmd.aliases.end()) {
|
|
return &cmd;
|
|
}
|
|
}
|
|
|
|
throw std::runtime_error("Subcommand not found");
|
|
}
|
|
|
|
void parse(const std::string &input) {
|
|
std::vector<std::string> args;
|
|
std::istringstream iss(input);
|
|
std::string arg;
|
|
while (iss >> arg) {
|
|
args.push_back(arg);
|
|
}
|
|
|
|
if (args.empty())
|
|
return;
|
|
|
|
// Handle help flags
|
|
if (args[0] == "-h" || args[0] == "--help") {
|
|
std::cout << help() << std::endl;
|
|
return;
|
|
}
|
|
|
|
Subcommand *cmd = nullptr;
|
|
|
|
// Try direct name match
|
|
auto it = subcommands.find(args[0]);
|
|
if (it != subcommands.end()) {
|
|
cmd = &it->second;
|
|
} else {
|
|
// Try aliases
|
|
for (auto &[_, subcmd] : subcommands) {
|
|
if (std::find(subcmd.aliases.begin(), subcmd.aliases.end(), args[0]) !=
|
|
subcmd.aliases.end()) {
|
|
cmd = &subcmd;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!cmd) {
|
|
std::cout << "Unknown command: " << args[0] << "\n\n";
|
|
std::cout << help() << std::endl;
|
|
throw ParseError("Unknown command: " + args[0]);
|
|
}
|
|
|
|
// Remove the command name from args
|
|
args.erase(args.begin());
|
|
|
|
// Handle subcommand help
|
|
if (!args.empty() && (args[0] == "-h" || args[0] == "--help")) {
|
|
std::cout << cmd->help() << std::endl;
|
|
return;
|
|
}
|
|
|
|
// Check if subcommand is required but missing
|
|
if (cmd->requires_subcommand && (args.empty() || args[0][0] == '-')) {
|
|
std::cout << "Error: Subcommand required\n\n";
|
|
std::cout << cmd->help() << std::endl;
|
|
throw ParseError("Subcommand required");
|
|
}
|
|
|
|
// If command has subcommands, try to parse them
|
|
if (!cmd->subcommands.empty() && !args.empty()) {
|
|
Subcommand *subcmd = nullptr;
|
|
|
|
// Try direct name match for subcommand
|
|
auto subit = cmd->subcommands.find(args[0]);
|
|
if (subit != cmd->subcommands.end()) {
|
|
subcmd = &subit->second;
|
|
} else {
|
|
// Try aliases for subcommand
|
|
for (auto &[_, sub] : cmd->subcommands) {
|
|
if (std::find(sub.aliases.begin(), sub.aliases.end(), args[0]) !=
|
|
sub.aliases.end()) {
|
|
subcmd = ⊂
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!subcmd) {
|
|
std::cout << "Unknown subcommand: " << args[0] << "\n\n";
|
|
std::cout << cmd->help() << std::endl;
|
|
throw ParseError("Unknown subcommand: " + args[0]);
|
|
}
|
|
|
|
// Remove the subcommand name from args
|
|
args.erase(args.begin());
|
|
|
|
// Check for required options in subcommand
|
|
bool missing_required = false;
|
|
for (const auto &[_, opt] : subcmd->options) {
|
|
if (opt.is_required && args.empty()) {
|
|
missing_required = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (missing_required) {
|
|
std::cout << "Error: Missing required arguments\n\n";
|
|
std::cout << subcmd->help() << std::endl;
|
|
throw ParseError("Missing required arguments");
|
|
}
|
|
|
|
// Process subcommand options
|
|
if (!args.empty() && !subcmd->options.empty()) {
|
|
auto opt_it = subcmd->options.begin();
|
|
for (const auto &arg : args) {
|
|
if (opt_it != subcmd->options.end()) {
|
|
if (opt_it->second.value_ptr) {
|
|
*opt_it->second.value_ptr = arg;
|
|
}
|
|
++opt_it;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (subcmd->callback_fn) {
|
|
subcmd->callback_fn();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Check for required options
|
|
bool missing_required = false;
|
|
for (const auto &[_, opt] : cmd->options) {
|
|
if (opt.is_required && args.empty()) {
|
|
missing_required = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (missing_required) {
|
|
std::cout << "Error: Missing required arguments\n\n";
|
|
std::cout << cmd->help() << std::endl;
|
|
throw ParseError("Missing required arguments");
|
|
}
|
|
|
|
// If there are arguments, assign them to options in order
|
|
if (!args.empty() && !cmd->options.empty()) {
|
|
auto opt_it = cmd->options.begin();
|
|
for (const auto &arg : args) {
|
|
if (opt_it != cmd->options.end()) {
|
|
if (opt_it->second.value_ptr) {
|
|
*opt_it->second.value_ptr = arg;
|
|
}
|
|
++opt_it;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (cmd->callback_fn) {
|
|
cmd->callback_fn();
|
|
}
|
|
}
|
|
|
|
std::string help() const {
|
|
std::ostringstream ss;
|
|
ss << description << "\n\n";
|
|
|
|
// Show options in usage
|
|
for (const auto &[opt_name, opt] : options) {
|
|
ss << " ";
|
|
if (opt.is_required) {
|
|
ss << "<" << opt_name << ">";
|
|
} else {
|
|
ss << "[" << opt_name << "]";
|
|
}
|
|
}
|
|
ss << "\n\n";
|
|
|
|
if (!description.empty()) {
|
|
ss << description << "\n\n";
|
|
}
|
|
|
|
// Show options detail
|
|
if (!options.empty()) {
|
|
ss << "Arguments:\n";
|
|
for (const auto &[opt_name, opt] : options) {
|
|
ss << " " << opt_name;
|
|
if (opt.is_required) {
|
|
ss << " (required)";
|
|
}
|
|
if (!opt.description.empty()) {
|
|
ss << ": " << opt.description;
|
|
}
|
|
ss << "\n";
|
|
}
|
|
ss << "\n";
|
|
}
|
|
|
|
// Show commands
|
|
if (!subcommands.empty()) {
|
|
ss << "Commands:\n";
|
|
for (const auto &[name, cmd] : subcommands) {
|
|
ss << " " << name;
|
|
if (!cmd.aliases.empty()) {
|
|
ss << " (";
|
|
for (size_t i = 0; i < cmd.aliases.size(); ++i) {
|
|
if (i > 0)
|
|
ss << ", ";
|
|
ss << cmd.aliases[i];
|
|
}
|
|
ss << ")";
|
|
}
|
|
ss << ": " << cmd.description << "\n";
|
|
}
|
|
}
|
|
|
|
if (!footer_text.empty()) {
|
|
ss << "\n" << footer_text << "\n";
|
|
}
|
|
|
|
return ss.str();
|
|
}
|
|
};
|
|
|
|
namespace CLI {
|
|
using App = ::App;
|
|
using ParseError = ::ParseError;
|
|
} // namespace CLI
|