feat: cli flag parser

and -h / --help flag
This commit is contained in:
2026-06-02 01:17:51 +03:00
parent 7aaee83654
commit b34b0e2794
+180 -8
View File
@@ -10,13 +10,14 @@
#include <unordered_map> #include <unordered_map>
#include <cctype> #include <cctype>
#include <string_view> #include <string_view>
#include <functional>
#include <sys/utsname.h> #include <sys/utsname.h>
#include <sys/stat.h> #include <sys/stat.h>
#include <ctime> #include <ctime>
#include <clocale> #include <clocale>
#include <cwchar> #include <cwchar>
// Config parsing (vendored) // TOML++ (Config parsing)
#include <toml++/toml.hpp> #include <toml++/toml.hpp>
using std::string, std::cout, std::vector; using std::string, std::cout, std::vector;
@@ -79,8 +80,6 @@ struct EnumHash {
} }
}; };
// Built-in configuration defaults. These should preserve the current behavior
// when no config file is present (or when it's invalid).
struct Config { struct Config {
vector<Module> modules; vector<Module> modules;
bool logoEnabled = true; bool logoEnabled = true;
@@ -125,6 +124,157 @@ static const ModuleSpec* findModuleSpec(Module m) {
return nullptr; return nullptr;
} }
enum class FlagValue {
None,
Required,
};
struct FlagSpec {
std::string_view longName;
char shortName = '\0';
FlagValue value = FlagValue::None;
std::string_view valueName;
std::string_view help;
std::function<void(std::string_view)> onValue;
std::function<void()> onFlag;
};
struct ParsedCli {
bool showHelp = false;
vector<string> positionals;
vector<string> warnings;
};
static const FlagSpec* findLongFlag(const vector<FlagSpec>& specs, std::string_view name) {
for (const auto& s : specs) {
if (s.longName == name) return &s;
}
return nullptr;
}
static const FlagSpec* findShortFlag(const vector<FlagSpec>& specs, char name) {
for (const auto& s : specs) {
if (s.shortName == name && name != '\0') return &s;
}
return nullptr;
}
static ParsedCli parseCli(int argc, char** argv, const vector<FlagSpec>& specs) {
ParsedCli out;
bool onlyPositionals = false;
for (int i = 1; i < argc; ++i) {
std::string_view arg = argv[i] ? argv[i] : "";
if (arg.empty()) continue;
if (!onlyPositionals && arg == "--") {
onlyPositionals = true;
continue;
}
if (!onlyPositionals && arg.rfind("--", 0) == 0) {
std::string_view name = arg.substr(2);
std::string_view value;
const size_t eq = name.find('=');
if (eq != std::string_view::npos) {
value = name.substr(eq + 1);
name = name.substr(0, eq);
}
const FlagSpec* spec = findLongFlag(specs, name);
if (!spec) {
out.warnings.push_back("fetchit: warning: unknown option '--" + string(name) + "'; ignoring");
continue;
}
if (spec->value == FlagValue::None) {
if (!value.empty()) {
out.warnings.push_back("fetchit: warning: option '--" + string(name)
+ "' does not take a value; ignoring value");
}
if (spec->onFlag) spec->onFlag();
} else {
if (value.empty()) {
if (i + 1 >= argc) {
out.warnings.push_back("fetchit: warning: option '--" + string(name)
+ "' requires a value; ignoring");
continue;
}
value = argv[++i];
}
if (spec->onValue) spec->onValue(value);
}
continue;
}
if (!onlyPositionals && arg.size() >= 2 && arg[0] == '-' && arg[1] != '-') {
std::string_view group = arg.substr(1);
bool consumedValue = false;
for (size_t j = 0; j < group.size(); ++j) {
const char ch = group[j];
const FlagSpec* spec = findShortFlag(specs, ch);
if (!spec) {
out.warnings.push_back(string("fetchit: warning: unknown option '-") + ch + "'; ignoring");
continue;
}
if (spec->value == FlagValue::None) {
if (spec->onFlag) spec->onFlag();
continue;
}
std::string_view value = group.substr(j + 1);
if (value.empty()) {
if (i + 1 >= argc) {
out.warnings.push_back(string("fetchit: warning: option '-") + ch
+ "' requires a value; ignoring");
break;
}
value = argv[++i];
}
if (spec->onValue) spec->onValue(value);
consumedValue = true;
break;
}
(void)consumedValue;
continue;
}
out.positionals.push_back(string(arg));
}
return out;
}
static void printHelp(const char* argv0, const vector<FlagSpec>& specs) {
const char* prog = (argv0 && *argv0) ? argv0 : "fetchit";
std::cout << "Usage: " << prog << " [options]\n\n";
std::cout << "Options:\n";
for (const auto& s : specs) {
std::cout << " ";
bool any = false;
if (s.shortName != '\0') {
std::cout << "-" << s.shortName;
any = true;
}
if (!s.longName.empty()) {
if (any) std::cout << ", ";
std::cout << "--" << s.longName;
any = true;
}
if (s.value == FlagValue::Required) {
std::cout << " <" << (s.valueName.empty() ? "value" : s.valueName) << ">";
}
std::cout << "\n";
if (!s.help.empty()) {
std::cout << " " << s.help << "\n";
}
}
}
Config defaultConfig() { Config defaultConfig() {
Config cfg; Config cfg;
@@ -151,7 +301,6 @@ static bool getenvNonEmpty(const char* name, string& out) {
static fs::path defaultConfigPath() { static fs::path defaultConfigPath() {
// XDG base dir spec: $XDG_CONFIG_HOME, fallback to ~/.config. // XDG base dir spec: $XDG_CONFIG_HOME, fallback to ~/.config.
// We keep it as a single file so users can drop in one config.
string xdg; string xdg;
if (getenvNonEmpty("XDG_CONFIG_HOME", xdg)) { if (getenvNonEmpty("XDG_CONFIG_HOME", xdg)) {
return fs::path(xdg) / "fetchit" / "config.toml"; return fs::path(xdg) / "fetchit" / "config.toml";
@@ -184,7 +333,6 @@ static string toLowerAscii(string s) {
static bool parseColorName(const string& name, Color& out) { static bool parseColorName(const string& name, Color& out) {
const string n = toLowerAscii(name); const string n = toLowerAscii(name);
// Keep this small and aligned with Color enum values used by fetchit.
if (n == "red") { if (n == "red") {
out = Color::Red; out = Color::Red;
return true; return true;
@@ -230,7 +378,6 @@ static Config loadConfigOrDefault(vector<string>& warnings) {
const fs::path configPath = findExistingConfigPath(); const fs::path configPath = findExistingConfigPath();
if (configPath.empty()) return defaultConfig(); if (configPath.empty()) return defaultConfig();
// Start from defaults and apply overrides.
Config cfg = defaultConfig(); Config cfg = defaultConfig();
toml::table tbl; toml::table tbl;
@@ -243,7 +390,6 @@ static Config loadConfigOrDefault(vector<string>& warnings) {
return defaultConfig(); return defaultConfig();
} }
// modules = ["distro", ...]
if (auto modulesArr = tbl["modules"].as_array()) { if (auto modulesArr = tbl["modules"].as_array()) {
vector<Module> modules; vector<Module> modules;
modules.reserve(modulesArr->size()); modules.reserve(modulesArr->size());
@@ -364,9 +510,35 @@ vector<gpuId> getGpuIds() {
return gpus; return gpus;
} }
int main () { int main (int argc, char** argv) {
std::setlocale(LC_ALL, ""); std::setlocale(LC_ALL, "");
ParsedCli cliState;
const vector<FlagSpec> flagSpecs = {
FlagSpec{
.longName = "help",
.shortName = 'h',
.value = FlagValue::None,
.valueName = {},
.help = "Show this help message",
.onValue = {},
.onFlag = [&]() { cliState.showHelp = true; },
},
};
ParsedCli cli = parseCli(argc, argv, flagSpecs);
cliState.warnings.insert(cliState.warnings.end(), cli.warnings.begin(), cli.warnings.end());
cliState.positionals = std::move(cli.positionals);
if (cliState.showHelp) {
printHelp(argv && argv[0] ? argv[0] : "fetchit", flagSpecs);
return 0;
}
for (const auto& w : cliState.warnings) {
std::cerr << w << "\n";
}
vector<string> configWarnings; vector<string> configWarnings;
const Config config = loadConfigOrDefault(configWarnings); const Config config = loadConfigOrDefault(configWarnings);
for (const auto& w : configWarnings) { for (const auto& w : configWarnings) {