mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-10 02:53:51 +00:00
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:
parent
96bb1f6d04
commit
e117e5aaff
4 changed files with 365 additions and 281 deletions
|
|
@ -1355,6 +1355,7 @@ add_executable(wowee_editor
|
||||||
tools/editor/cli_deps.cpp
|
tools/editor/cli_deps.cpp
|
||||||
tools/editor/cli_for_each.cpp
|
tools/editor/cli_for_each.cpp
|
||||||
tools/editor/cli_check.cpp
|
tools/editor/cli_check.cpp
|
||||||
|
tools/editor/cli_introspect.cpp
|
||||||
tools/editor/editor_app.cpp
|
tools/editor/editor_app.cpp
|
||||||
tools/editor/editor_camera.cpp
|
tools/editor/editor_camera.cpp
|
||||||
tools/editor/editor_viewport.cpp
|
tools/editor/editor_viewport.cpp
|
||||||
|
|
|
||||||
337
tools/editor/cli_introspect.cpp
Normal file
337
tools/editor/cli_introspect.cpp
Normal 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
|
||||||
23
tools/editor/cli_introspect.hpp
Normal file
23
tools/editor/cli_introspect.hpp
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace wowee {
|
||||||
|
namespace editor {
|
||||||
|
namespace cli {
|
||||||
|
|
||||||
|
// Dispatch the CLI introspection / discoverability handlers —
|
||||||
|
// auto-discover commands by parsing printUsage's output so the
|
||||||
|
// surface stays self-describing as new flags are added. Useful
|
||||||
|
// for shell completion, IDE plugins, and 'is there a flag for X?'
|
||||||
|
// search workflows.
|
||||||
|
// --list-commands flat sorted/deduped flag list
|
||||||
|
// --info-cli-stats per-category counts (--info-* / --validate-*)
|
||||||
|
// --info-cli-categories per-category command listing
|
||||||
|
// --info-cli-help <q> substring search through help text
|
||||||
|
// --gen-completion <bash|zsh> re-exec'd at completion time
|
||||||
|
//
|
||||||
|
// Returns true if matched; outRc holds the exit code.
|
||||||
|
bool handleIntrospect(int& i, int argc, char** argv, int& outRc);
|
||||||
|
|
||||||
|
} // namespace cli
|
||||||
|
} // namespace editor
|
||||||
|
} // namespace wowee
|
||||||
|
|
@ -56,6 +56,7 @@
|
||||||
#include "cli_deps.hpp"
|
#include "cli_deps.hpp"
|
||||||
#include "cli_for_each.hpp"
|
#include "cli_for_each.hpp"
|
||||||
#include "cli_check.hpp"
|
#include "cli_check.hpp"
|
||||||
|
#include "cli_introspect.hpp"
|
||||||
#include "content_pack.hpp"
|
#include "content_pack.hpp"
|
||||||
#include "npc_spawner.hpp"
|
#include "npc_spawner.hpp"
|
||||||
#include "object_placer.hpp"
|
#include "object_placer.hpp"
|
||||||
|
|
@ -565,6 +566,9 @@ int main(int argc, char* argv[]) {
|
||||||
if (wowee::editor::cli::handleCheck(i, argc, argv, outRc)) {
|
if (wowee::editor::cli::handleCheck(i, argc, argv, outRc)) {
|
||||||
return outRc;
|
return outRc;
|
||||||
}
|
}
|
||||||
|
if (wowee::editor::cli::handleIntrospect(i, argc, argv, outRc)) {
|
||||||
|
return outRc;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
if (std::strcmp(argv[i], "--data") == 0 && i + 1 < argc) {
|
||||||
dataPath = argv[++i];
|
dataPath = argv[++i];
|
||||||
|
|
@ -1944,227 +1948,6 @@ int main(int argc, char* argv[]) {
|
||||||
std::printf("Open formats: WOT/WHM/WOM/WOB/WOC/WCP + PNG/JSON (all novel)\n");
|
std::printf("Open formats: WOT/WHM/WOM/WOB/WOC/WCP + PNG/JSON (all novel)\n");
|
||||||
std::printf("By Kelsi Davis\n");
|
std::printf("By Kelsi Davis\n");
|
||||||
return 0;
|
return 0;
|
||||||
} else if (std::strcmp(argv[i], "--list-commands") == 0) {
|
|
||||||
// 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;
|
|
||||||
} else if (std::strcmp(argv[i], "--info-cli-stats") == 0) {
|
|
||||||
// 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;
|
|
||||||
} else if (std::strcmp(argv[i], "--info-cli-categories") == 0) {
|
|
||||||
// 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;
|
|
||||||
} else if (std::strcmp(argv[i], "--info-cli-help") == 0 && i + 1 < argc) {
|
|
||||||
// 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;
|
|
||||||
} else if (std::strcmp(argv[i], "--validate-cli-help") == 0) {
|
} else if (std::strcmp(argv[i], "--validate-cli-help") == 0) {
|
||||||
// Self-check: every flag we declare in kArgRequired (the list
|
// Self-check: every flag we declare in kArgRequired (the list
|
||||||
// of commands needing positional args) must appear in the
|
// of commands needing positional args) must appear in the
|
||||||
|
|
@ -2210,66 +1993,6 @@ int main(int argc, char* argv[]) {
|
||||||
std::printf(" FAILED — %zu flag(s) missing from help text:\n", missing.size());
|
std::printf(" FAILED — %zu flag(s) missing from help text:\n", missing.size());
|
||||||
for (const auto& m : missing) std::printf(" - %s\n", m.c_str());
|
for (const auto& m : missing) std::printf(" - %s\n", m.c_str());
|
||||||
return 1;
|
return 1;
|
||||||
} else if (std::strcmp(argv[i], "--gen-completion") == 0 && i + 1 < argc) {
|
|
||||||
// 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;
|
|
||||||
} else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
|
} else if (std::strcmp(argv[i], "--help") == 0 || std::strcmp(argv[i], "-h") == 0) {
|
||||||
wowee::editor::cli::printUsage(argv[0]);
|
wowee::editor::cli::printUsage(argv[0]);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue