refactor(editor): extract CLI introspection handlers into cli_introspect.cpp

Moves five self-discovery handlers (--list-commands,
--info-cli-stats, --info-cli-categories, --info-cli-help,
--gen-completion) out of main.cpp into a new
cli_introspect.{hpp,cpp} module. All five auto-discover
commands by parsing printUsage's stdout via tmpfile capture,
so the surface stays self-describing as new flags are added.

Useful for shell completion scripts that re-exec the binary
at completion time, IDE plugins, and 'is there a flag for X?'
search workflows. main.cpp shrinks by 276 lines (2,298 to
2,022). --validate-cli-help stays inline because it needs
direct access to the static-local kArgRequired array.
This commit is contained in:
Kelsi 2026-05-09 09:31:31 -07:00
parent 96bb1f6d04
commit e117e5aaff
4 changed files with 365 additions and 281 deletions

View file

@ -0,0 +1,337 @@
#include "cli_introspect.hpp"
#include "cli_help.hpp"
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cctype>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <map>
#include <set>
#include <string>
#include <vector>
namespace wowee {
namespace editor {
namespace cli {
namespace {
int handleListCommands(int& i, int argc, char** argv) {
// Capture printUsage's stdout and grep for '--flag' tokens at
// the start of each line. This auto-tracks the help text as
// commands are added — no parallel list to maintain. Result
// is a sorted, deduped, one-per-line list of recognized flags.
FILE* old = stdout;
// Temp file lets us read printUsage's output back. fmemopen
// would be cleaner but isn't available on Windows; tmpfile is
// portable.
FILE* tmp = std::tmpfile();
if (!tmp) { std::fprintf(stderr, "list-commands: tmpfile failed\n"); return 1; }
stdout = tmp;
wowee::editor::cli::printUsage(argv[0]);
stdout = old;
std::fseek(tmp, 0, SEEK_SET);
std::set<std::string> commands;
char line[512];
while (std::fgets(line, sizeof(line), tmp)) {
// Match leading whitespace then '--' then [a-z-]+
const char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (p[0] != '-' || p[1] != '-') continue;
std::string flag;
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
*p == '-' || *p == '_')) {
flag += *p++;
}
if (flag.size() > 2) commands.insert(flag);
}
std::fclose(tmp);
// Always include the meta-flags that printUsage describes
// alongside others (-h/-v aliases) since the regex above only
// captures double-dash forms.
commands.insert("--help");
commands.insert("--version");
for (const auto& c : commands) std::printf("%s\n", c.c_str());
return 0;
}
int handleInfoCliStats(int& i, int argc, char** argv) {
// Meta-stats on the CLI surface: total command count + per-
// category breakdown by prefix verb (--info-*, --validate-*,
// --diff-*, etc.). Useful for tracking growth over time and
// spotting category imbalances.
bool jsonOut = (i + 1 < argc &&
std::strcmp(argv[i + 1], "--json") == 0);
if (jsonOut) i++;
// Re-use --list-commands' parser. Capture printUsage stdout.
FILE* old = stdout;
FILE* tmp = std::tmpfile();
if (!tmp) { std::fprintf(stderr, "info-cli-stats: tmpfile failed\n"); return 1; }
stdout = tmp;
wowee::editor::cli::printUsage(argv[0]);
stdout = old;
std::fseek(tmp, 0, SEEK_SET);
std::set<std::string> commands;
char line[512];
while (std::fgets(line, sizeof(line), tmp)) {
const char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (p[0] != '-' || p[1] != '-') continue;
std::string flag;
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
*p == '-' || *p == '_')) { flag += *p++; }
if (flag.size() > 2) commands.insert(flag);
}
std::fclose(tmp);
commands.insert("--help");
commands.insert("--version");
// Bucket by category — verb is the second token after '--',
// up to the next dash. So '--info-zone-tree' -> 'info'.
std::map<std::string, int> byCategory;
int maxLen = 0;
for (const auto& c : commands) {
if (static_cast<int>(c.size()) > maxLen) maxLen = static_cast<int>(c.size());
size_t verbStart = 2; // skip '--'
size_t verbEnd = c.find('-', verbStart);
std::string verb = (verbEnd == std::string::npos)
? c.substr(verbStart)
: c.substr(verbStart, verbEnd - verbStart);
byCategory[verb]++;
}
if (jsonOut) {
nlohmann::json j;
j["totalCommands"] = commands.size();
j["maxFlagLength"] = maxLen;
nlohmann::json cats = nlohmann::json::object();
for (const auto& [v, c] : byCategory) cats[v] = c;
j["byCategory"] = cats;
std::printf("%s\n", j.dump(2).c_str());
return 0;
}
std::printf("CLI surface stats\n");
std::printf(" total commands : %zu\n", commands.size());
std::printf(" longest flag : %d chars\n", maxLen);
std::printf("\n Categories (by verb prefix, sorted by count):\n");
// Sort by count descending for the table.
std::vector<std::pair<std::string, int>> sorted(
byCategory.begin(), byCategory.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) {
return a.second > b.second;
});
for (const auto& [verb, count] : sorted) {
std::printf(" --%-12s %4d\n", verb.c_str(), count);
}
return 0;
}
int handleInfoCliCategories(int& i, int argc, char** argv) {
// Discovery view of every CLI flag grouped by verb prefix.
// Where --info-cli-stats just counts per category, this
// lists every command in each category — handy for "I
// know I want to gen something but what shapes/textures
// are available?"
FILE* old = stdout;
FILE* tmp = std::tmpfile();
if (!tmp) {
std::fprintf(stderr, "info-cli-categories: tmpfile failed\n");
return 1;
}
stdout = tmp;
wowee::editor::cli::printUsage(argv[0]);
stdout = old;
std::fseek(tmp, 0, SEEK_SET);
std::set<std::string> commands;
char line[512];
while (std::fgets(line, sizeof(line), tmp)) {
const char* p = line;
while (*p == ' ' || *p == '\t') ++p;
if (p[0] != '-' || p[1] != '-') continue;
std::string flag;
while (*p && (std::isalnum(static_cast<unsigned char>(*p)) ||
*p == '-' || *p == '_')) { flag += *p++; }
if (flag.size() > 2) commands.insert(flag);
}
std::fclose(tmp);
commands.insert("--help");
commands.insert("--version");
std::map<std::string, std::vector<std::string>> byCategory;
for (const auto& c : commands) {
size_t verbStart = 2;
size_t verbEnd = c.find('-', verbStart);
std::string verb = (verbEnd == std::string::npos)
? c.substr(verbStart)
: c.substr(verbStart, verbEnd - verbStart);
byCategory[verb].push_back(c);
}
std::printf("CLI commands by category (%zu total):\n\n",
commands.size());
// Sort categories by count descending, commands within
// each alphabetically.
std::vector<std::pair<std::string, std::vector<std::string>>> sorted(
byCategory.begin(), byCategory.end());
std::sort(sorted.begin(), sorted.end(),
[](const auto& a, const auto& b) {
if (a.second.size() != b.second.size())
return a.second.size() > b.second.size();
return a.first < b.first;
});
for (const auto& [verb, cmds] : sorted) {
std::printf("--%s (%zu):\n", verb.c_str(), cmds.size());
for (const auto& c : cmds) {
std::printf(" %s\n", c.c_str());
}
std::printf("\n");
}
return 0;
}
int handleInfoCliHelp(int& i, int argc, char** argv) {
// Substring search through the help text. With 130+ commands,
// 'is there a thing for X?' is a common ask — this answers it
// without making the user scroll the full --help output:
//
// wowee_editor --info-cli-help quest
// wowee_editor --info-cli-help validate
// wowee_editor --info-cli-help glb
std::string pattern = argv[++i];
// Lowercase the pattern for case-insensitive match.
std::string patLower = pattern;
for (auto& c : patLower) c = std::tolower(static_cast<unsigned char>(c));
// Capture printUsage stdout, walk line-by-line, print every
// line containing the pattern (case-insensitive). Continuation
// lines (the indented description on the line after a flag)
// are emitted along with the flag line for context.
FILE* old = stdout;
FILE* tmp = std::tmpfile();
if (!tmp) {
std::fprintf(stderr, "info-cli-help: tmpfile failed\n"); return 1;
}
stdout = tmp;
wowee::editor::cli::printUsage(argv[0]);
stdout = old;
std::fseek(tmp, 0, SEEK_SET);
std::vector<std::string> lines;
char buf[1024];
while (std::fgets(buf, sizeof(buf), tmp)) {
std::string s = buf;
if (!s.empty() && s.back() == '\n') s.pop_back();
lines.push_back(std::move(s));
}
std::fclose(tmp);
int matches = 0;
for (size_t k = 0; k < lines.size(); ++k) {
std::string lower = lines[k];
for (auto& c : lower) c = std::tolower(static_cast<unsigned char>(c));
if (lower.find(patLower) == std::string::npos) continue;
std::printf("%s\n", lines[k].c_str());
// Look ahead for a continuation line (indented and not
// starting with '--'). Print it for context.
if (k + 1 < lines.size()) {
const auto& next = lines[k + 1];
if (!next.empty() && next[0] == ' ' &&
next.find("--") == std::string::npos) {
std::printf("%s\n", next.c_str());
}
}
matches++;
}
if (matches == 0) {
std::fprintf(stderr, "info-cli-help: no matches for '%s'\n",
pattern.c_str());
return 1;
}
std::fprintf(stderr, "\n%d line(s) matched '%s'\n", matches, pattern.c_str());
return 0;
}
int handleGenCompletion(int& i, int argc, char** argv) {
// Emit a bash or zsh completion script. Re-execs the editor's
// own --list-commands at completion time so newly-added flags
// light up automatically without regenerating the script.
std::string shell = argv[++i];
if (shell != "bash" && shell != "zsh") {
std::fprintf(stderr,
"gen-completion: shell must be 'bash' or 'zsh', got '%s'\n",
shell.c_str());
return 1;
}
// Use argv[0] as the binary name in the completion so it
// works whether the user installed it as 'wowee_editor' or
// a custom alias. Strip directory components for the
// completion-name registration (bash 'complete -F' expects
// a basename).
std::string self = argv[0];
auto slash = self.find_last_of('/');
std::string baseName = (slash != std::string::npos)
? self.substr(slash + 1)
: self;
if (shell == "bash") {
std::printf(
"# wowee_editor bash completion — source from ~/.bashrc:\n"
"# source <(%s --gen-completion bash)\n"
"_wowee_editor_complete() {\n"
" local cur prev cmds\n"
" COMPREPLY=()\n"
" cur=\"${COMP_WORDS[COMP_CWORD]}\"\n"
" prev=\"${COMP_WORDS[COMP_CWORD-1]}\"\n"
" # Cache the command list per shell session.\n"
" if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n"
" _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n"
" fi\n"
" if [[ \"$cur\" == --* ]]; then\n"
" COMPREPLY=( $(compgen -W \"$_WOWEE_EDITOR_CMDS\" -- \"$cur\") )\n"
" return 0\n"
" fi\n"
" # Default: complete file paths for arg slots.\n"
" COMPREPLY=( $(compgen -f -- \"$cur\") )\n"
"}\n"
"complete -F _wowee_editor_complete %s\n",
self.c_str(), self.c_str(), baseName.c_str());
} else {
// zsh — simpler descriptor-based completion.
std::printf(
"# wowee_editor zsh completion — source from ~/.zshrc:\n"
"# source <(%s --gen-completion zsh)\n"
"_wowee_editor_complete() {\n"
" local -a cmds\n"
" if [[ -z \"$_WOWEE_EDITOR_CMDS\" ]]; then\n"
" export _WOWEE_EDITOR_CMDS=$(%s --list-commands 2>/dev/null)\n"
" fi\n"
" cmds=( ${(f)_WOWEE_EDITOR_CMDS} )\n"
" _arguments \"*: :($cmds)\"\n"
"}\n"
"compdef _wowee_editor_complete %s\n",
self.c_str(), self.c_str(), baseName.c_str());
}
return 0;
}
} // namespace
bool handleIntrospect(int& i, int argc, char** argv, int& outRc) {
if (std::strcmp(argv[i], "--list-commands") == 0) {
outRc = handleListCommands(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-cli-stats") == 0) {
outRc = handleInfoCliStats(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-cli-categories") == 0) {
outRc = handleInfoCliCategories(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--info-cli-help") == 0 && i + 1 < argc) {
outRc = handleInfoCliHelp(i, argc, argv); return true;
}
if (std::strcmp(argv[i], "--gen-completion") == 0 && i + 1 < argc) {
outRc = handleGenCompletion(i, argc, argv); return true;
}
return false;
}
} // namespace cli
} // namespace editor
} // namespace wowee