mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-05-11 19:43:52 +00:00
feat(editor): add --catalog-by-name entry-name substring search
New utility complements --catalog-find (id-based, single-id lookup) and --catalog-grep (catalog-header label only) by searching every catalog file under a directory tree for entries whose `name` field contains a substring. Reports each hit as [WXXX] file id=N "name" so the operator can find catalog entries by half- remembered names — "find anything called Mark of the Wild" hits all 5 rank entries across WBAB, "find Argent" hits the WTBD tabard, etc. Optional flags: - --magic <WXXX>: limit search to one format family, same convention as --catalog-find. - --ignore-case: lowercase both pattern and haystack before substring match. Returns rc=0 on hits, rc=1 if no entries matched (so the caller can `if --catalog-by-name ... ; then ...; fi`). Skips files with unknown magic and files whose format has no --info-* surface (asset formats like .wom). Permission-denied subdirs skipped via skip_permission_denied directory_options. Closes the search triplet: --catalog-grep catalog-header label --catalog-find entry primary-key id --catalog-by-name entry name substring CLI flag count 1169 -> 1170.
This commit is contained in:
parent
3c8516f8d4
commit
3c69f33465
6 changed files with 298 additions and 1 deletions
281
tools/editor/cli_catalog_by_name.cpp
Normal file
281
tools/editor/cli_catalog_by_name.cpp
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
#include "cli_catalog_by_name.hpp"
|
||||
#include "cli_arg_parse.hpp"
|
||||
#include "cli_format_table.hpp"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cctype>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace wowee {
|
||||
namespace editor {
|
||||
namespace cli {
|
||||
|
||||
namespace {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
std::string shellQuote(const std::string& s) {
|
||||
std::string out;
|
||||
out.reserve(s.size() + 2);
|
||||
out.push_back('\'');
|
||||
for (char c : s) {
|
||||
if (c == '\'') out += "'\"'\"'";
|
||||
else out.push_back(c);
|
||||
}
|
||||
out.push_back('\'');
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string toLower(std::string s) {
|
||||
for (char& c : s) {
|
||||
c = static_cast<char>(
|
||||
std::tolower(static_cast<unsigned char>(c)));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
bool peekMagic(const fs::path& path, char magic[4]) {
|
||||
std::ifstream is(path, std::ios::binary);
|
||||
if (!is) return false;
|
||||
if (!is.read(magic, 4) || is.gcount() != 4) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string runAndCapture(const std::string& cmd, int& outRc) {
|
||||
std::string buf;
|
||||
FILE* pipe = popen(cmd.c_str(), "r");
|
||||
if (!pipe) {
|
||||
outRc = 127;
|
||||
return buf;
|
||||
}
|
||||
char chunk[4096];
|
||||
while (std::fgets(chunk, sizeof(chunk), pipe) != nullptr) {
|
||||
buf += chunk;
|
||||
}
|
||||
int rc = pclose(pipe);
|
||||
#ifdef WEXITSTATUS
|
||||
outRc = (rc != -1) ? WEXITSTATUS(rc) : rc;
|
||||
#else
|
||||
outRc = rc;
|
||||
#endif
|
||||
return buf;
|
||||
}
|
||||
|
||||
// Find the first numeric *Id field in an entry to use as
|
||||
// the displayed id for a hit. Same alphabetical-iteration
|
||||
// caveat as cli_catalog_pluck — we iterate alphabetically
|
||||
// (nlohmann::json default storage), so we have a small
|
||||
// foreign-key filter to skip obvious external refs.
|
||||
// For catalog-by-name this is purely cosmetic (the search
|
||||
// itself is by name), so the filter doesn't need to be
|
||||
// as comprehensive as catalog-pluck.
|
||||
bool isExternalRefField(const std::string& k) {
|
||||
static const char* kExternals[] = {
|
||||
"mapId", "areaId", "spellId", "itemId", "npcId",
|
||||
"creatureId", "factionId", "guildId", "soundId",
|
||||
"movieId", "displayId", "modelId", "iconId",
|
||||
"creatorPlayerId", "emblemId", "animationId",
|
||||
"previousRankId", "nextRankId",
|
||||
};
|
||||
for (const char* ref : kExternals) {
|
||||
if (k == ref) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t findEntryDisplayId(const nlohmann::json& entry) {
|
||||
if (!entry.is_object()) return 0;
|
||||
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
||||
const std::string& k = it.key();
|
||||
if (k.size() >= 2 &&
|
||||
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
||||
it.value().is_number_integer() &&
|
||||
!isExternalRefField(k)) {
|
||||
return it.value().get<uint64_t>();
|
||||
}
|
||||
}
|
||||
for (auto it = entry.begin(); it != entry.end(); ++it) {
|
||||
const std::string& k = it.key();
|
||||
if (k.size() >= 2 &&
|
||||
k.compare(k.size() - 2, 2, "Id") == 0 &&
|
||||
it.value().is_number_integer()) {
|
||||
return it.value().get<uint64_t>();
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct Hit {
|
||||
fs::path path;
|
||||
std::string magic;
|
||||
uint64_t id;
|
||||
std::string entryName;
|
||||
};
|
||||
|
||||
int handleByName(int& i, int argc, char** argv) {
|
||||
if (i + 2 >= argc) {
|
||||
std::fprintf(stderr,
|
||||
"catalog-by-name: usage: --catalog-by-name "
|
||||
"<directory> <name-substring> [--magic <WXXX>] "
|
||||
"[--ignore-case] [--json]\n");
|
||||
return 1;
|
||||
}
|
||||
std::string dir = argv[++i];
|
||||
std::string pattern = argv[++i];
|
||||
bool jsonOut = false;
|
||||
bool ignoreCase = false;
|
||||
std::string magicFilter;
|
||||
// Parse trailing flags in any order.
|
||||
while (i + 1 < argc) {
|
||||
if (std::strcmp(argv[i + 1], "--json") == 0) {
|
||||
++i; jsonOut = true;
|
||||
} else if (std::strcmp(argv[i + 1], "--ignore-case") == 0) {
|
||||
++i; ignoreCase = true;
|
||||
} else if (std::strcmp(argv[i + 1], "--magic") == 0 &&
|
||||
i + 2 < argc) {
|
||||
++i;
|
||||
magicFilter = argv[++i];
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!fs::exists(dir) || !fs::is_directory(dir)) {
|
||||
std::fprintf(stderr,
|
||||
"catalog-by-name: not a directory: %s\n", dir.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::string lcPattern = ignoreCase ? toLower(pattern) : pattern;
|
||||
std::vector<Hit> hits;
|
||||
size_t scanned = 0;
|
||||
|
||||
std::error_code walkEc;
|
||||
fs::recursive_directory_iterator it(
|
||||
dir, fs::directory_options::skip_permission_denied,
|
||||
walkEc);
|
||||
fs::recursive_directory_iterator end;
|
||||
if (walkEc) {
|
||||
std::fprintf(stderr,
|
||||
"catalog-by-name: cannot open directory '%s': %s\n",
|
||||
dir.c_str(), walkEc.message().c_str());
|
||||
return 1;
|
||||
}
|
||||
for (; it != end; it.increment(walkEc)) {
|
||||
if (walkEc) { walkEc.clear(); continue; }
|
||||
const auto& dirent = *it;
|
||||
if (!dirent.is_regular_file(walkEc)) {
|
||||
walkEc.clear(); continue;
|
||||
}
|
||||
char magic[4]{};
|
||||
if (!peekMagic(dirent.path(), magic)) continue;
|
||||
const FormatMagicEntry* fmt = findFormatByMagic(magic);
|
||||
if (!fmt || !fmt->infoFlag) continue;
|
||||
if (!magicFilter.empty()) {
|
||||
std::string m(magic, 4);
|
||||
if (m != magicFilter) continue;
|
||||
}
|
||||
++scanned;
|
||||
std::string base = dirent.path().string();
|
||||
if (fmt->extension && *fmt->extension) {
|
||||
size_t extLen = std::strlen(fmt->extension);
|
||||
if (base.size() >= extLen &&
|
||||
base.compare(base.size() - extLen, extLen,
|
||||
fmt->extension) == 0) {
|
||||
base.resize(base.size() - extLen);
|
||||
}
|
||||
}
|
||||
std::string cmd = shellQuote(argv[0]) + " " +
|
||||
fmt->infoFlag + " " +
|
||||
shellQuote(base) + " --json 2>/dev/null";
|
||||
int rc = 0;
|
||||
std::string out = runAndCapture(cmd, rc);
|
||||
if (rc != 0 || out.empty()) continue;
|
||||
nlohmann::json doc;
|
||||
try { doc = nlohmann::json::parse(out); }
|
||||
catch (...) { continue; }
|
||||
if (!doc.contains("entries") ||
|
||||
!doc["entries"].is_array()) continue;
|
||||
for (const auto& entry : doc["entries"]) {
|
||||
if (!entry.is_object()) continue;
|
||||
if (!entry.contains("name") ||
|
||||
!entry["name"].is_string()) continue;
|
||||
std::string entryName =
|
||||
entry["name"].get<std::string>();
|
||||
std::string haystack = ignoreCase
|
||||
? toLower(entryName) : entryName;
|
||||
if (haystack.find(lcPattern) == std::string::npos)
|
||||
continue;
|
||||
Hit h;
|
||||
h.path = dirent.path();
|
||||
h.magic = std::string(magic, 4);
|
||||
h.id = findEntryDisplayId(entry);
|
||||
h.entryName = entryName;
|
||||
hits.push_back(h);
|
||||
}
|
||||
}
|
||||
|
||||
if (jsonOut) {
|
||||
nlohmann::json out;
|
||||
out["directory"] = dir;
|
||||
out["pattern"] = pattern;
|
||||
out["ignoreCase"] = ignoreCase;
|
||||
if (!magicFilter.empty()) out["magicFilter"] = magicFilter;
|
||||
out["scanned"] = scanned;
|
||||
out["hits"] = nlohmann::json::array();
|
||||
for (const auto& h : hits) {
|
||||
out["hits"].push_back({
|
||||
{"file", h.path.string()},
|
||||
{"magic", h.magic},
|
||||
{"id", h.id},
|
||||
{"name", h.entryName},
|
||||
});
|
||||
}
|
||||
std::printf("%s\n", out.dump(2).c_str());
|
||||
return hits.empty() ? 1 : 0;
|
||||
}
|
||||
|
||||
std::printf("catalog-by-name: searched %zu catalog files "
|
||||
"in '%s' for name~='%s'%s",
|
||||
scanned, dir.c_str(), pattern.c_str(),
|
||||
ignoreCase ? " (case-insensitive)" : "");
|
||||
if (!magicFilter.empty()) {
|
||||
std::printf(" (magic=%s)", magicFilter.c_str());
|
||||
}
|
||||
std::printf("\n");
|
||||
if (hits.empty()) {
|
||||
std::printf(" no hits — no entry name matched the "
|
||||
"pattern in any catalog under this tree\n");
|
||||
return 1;
|
||||
}
|
||||
std::printf(" hits (%zu):\n", hits.size());
|
||||
for (const auto& h : hits) {
|
||||
std::printf(" [%s] %s id=%llu \"%s\"\n",
|
||||
h.magic.c_str(), h.path.string().c_str(),
|
||||
static_cast<unsigned long long>(h.id),
|
||||
h.entryName.c_str());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool handleCatalogByName(int& i, int argc, char** argv, int& outRc) {
|
||||
if (std::strcmp(argv[i], "--catalog-by-name") == 0 &&
|
||||
i + 2 < argc) {
|
||||
outRc = handleByName(i, argc, argv); return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace cli
|
||||
} // namespace editor
|
||||
} // namespace wowee
|
||||
Loading…
Add table
Add a link
Reference in a new issue