Add parse command to CLI with multiple output formats
Extend CLI with comprehensive file parsing capabilities: New parse command: - Parse binary files using XScript definitions - Auto-detect file type or force specific type with -t option - Output formats: JSON, tree, or table view Output options: - JSON (-f json): Machine-readable structured output - Tree (-f tree): Hierarchical view with colored output - Table (-f table): Compact tabular format Additional features: - Verbose mode (-v) to show hidden fields - Warnings-only mode (-w) to show blank/zero values - Nested data expansion in tree view - Hex formatting for addresses and hashes Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d7488c5fa9
commit
37bde14174
@ -5,6 +5,7 @@
|
||||
// xscript-cli check-dir <directory> - Check all .xscript files in directory
|
||||
// xscript-cli list-types <file.xscript> - List all types defined in file
|
||||
// xscript-cli info <file.xscript> <type> - Show info about a specific type
|
||||
// xscript-cli parse <binary-file> - Parse file and show UI fields
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QCommandLineParser>
|
||||
@ -13,10 +14,15 @@
|
||||
#include <QDir>
|
||||
#include <QDirIterator>
|
||||
#include <QTextStream>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <functional>
|
||||
|
||||
#include "lexer.h"
|
||||
#include "parser.h"
|
||||
#include "typeregistry.h"
|
||||
#include "dsluischema.h"
|
||||
|
||||
static QTextStream out(stdout);
|
||||
static QTextStream err(stderr);
|
||||
@ -295,6 +301,595 @@ int cmdInfo(const QString& filePath, const QString& typeName) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Parse command - Parse binary files and output UI field diagnostics
|
||||
// ============================================================================
|
||||
|
||||
struct ParseOptions {
|
||||
QString definitionsPath;
|
||||
QString typeName; // Force specific type (skip auto-detect)
|
||||
QString format = "json"; // json, table, tree
|
||||
bool verbose = false;
|
||||
bool warningsOnly = false;
|
||||
};
|
||||
|
||||
bool isBlankValue(const QVariant& value) {
|
||||
if (value.isNull()) return true;
|
||||
if (value.typeId() == QMetaType::QString && value.toString().isEmpty()) return true;
|
||||
if (value.typeId() == QMetaType::QByteArray && value.toByteArray().isEmpty()) return true;
|
||||
if (value.typeId() == QMetaType::QVariantList && value.toList().isEmpty()) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool isZeroValue(const QVariant& value) {
|
||||
bool ok;
|
||||
if (value.typeId() == QMetaType::Int || value.typeId() == QMetaType::LongLong ||
|
||||
value.typeId() == QMetaType::UInt || value.typeId() == QMetaType::ULongLong) {
|
||||
return value.toLongLong(&ok) == 0 && ok;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QString getFieldTypeString(UiFieldKind kind) {
|
||||
switch (kind) {
|
||||
case UiFieldKind::ScalarU8: return "u8";
|
||||
case UiFieldKind::ScalarI8: return "i8";
|
||||
case UiFieldKind::ScalarU16: return "u16";
|
||||
case UiFieldKind::ScalarI16: return "i16";
|
||||
case UiFieldKind::ScalarU32: return "u32";
|
||||
case UiFieldKind::ScalarI32: return "i32";
|
||||
case UiFieldKind::ScalarU64: return "u64";
|
||||
case UiFieldKind::ScalarI64: return "i64";
|
||||
case UiFieldKind::Bool: return "bool";
|
||||
case UiFieldKind::GenericValue: return "value";
|
||||
}
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
QString formatValue(const QVariant& value, int maxLen = 80) {
|
||||
if (value.isNull()) return "(null)";
|
||||
|
||||
switch (value.typeId()) {
|
||||
case QMetaType::QString:
|
||||
return value.toString().left(maxLen);
|
||||
case QMetaType::QByteArray: {
|
||||
QByteArray ba = value.toByteArray();
|
||||
if (ba.size() <= 16) {
|
||||
return QString("bytes[%1]: %2").arg(ba.size()).arg(QString(ba.toHex(' ')));
|
||||
}
|
||||
return QString("bytes[%1]").arg(ba.size());
|
||||
}
|
||||
case QMetaType::QVariantList: {
|
||||
QVariantList list = value.toList();
|
||||
return QString("list[%1]").arg(list.size());
|
||||
}
|
||||
case QMetaType::QVariantMap: {
|
||||
QVariantMap map = value.toMap();
|
||||
return QString("object{%1}").arg(map.size());
|
||||
}
|
||||
case QMetaType::Bool:
|
||||
return value.toBool() ? "true" : "false";
|
||||
case QMetaType::Int:
|
||||
case QMetaType::LongLong:
|
||||
return QString::number(value.toLongLong());
|
||||
case QMetaType::UInt:
|
||||
case QMetaType::ULongLong: {
|
||||
qulonglong v = value.toULongLong();
|
||||
if (v > 0xFFFF) {
|
||||
return QString("0x%1 (%2)").arg(v, 0, 16).arg(v);
|
||||
}
|
||||
return QString::number(v);
|
||||
}
|
||||
case QMetaType::Double:
|
||||
case QMetaType::Float:
|
||||
return QString::number(value.toDouble(), 'g', 6);
|
||||
default:
|
||||
return value.toString().left(maxLen);
|
||||
}
|
||||
}
|
||||
|
||||
QJsonValue variantToJson(const QVariant& value) {
|
||||
if (value.isNull()) return QJsonValue::Null;
|
||||
|
||||
switch (value.typeId()) {
|
||||
case QMetaType::Bool:
|
||||
return value.toBool();
|
||||
case QMetaType::Int:
|
||||
case QMetaType::LongLong:
|
||||
return value.toLongLong();
|
||||
case QMetaType::UInt:
|
||||
case QMetaType::ULongLong:
|
||||
return value.toLongLong(); // JSON doesn't have unsigned
|
||||
case QMetaType::Double:
|
||||
case QMetaType::Float:
|
||||
return value.toDouble();
|
||||
case QMetaType::QString:
|
||||
return value.toString();
|
||||
case QMetaType::QByteArray: {
|
||||
QByteArray ba = value.toByteArray();
|
||||
if (ba.size() <= 64) {
|
||||
return QString("0x") + QString(ba.toHex());
|
||||
}
|
||||
return QString("bytes[%1]").arg(ba.size());
|
||||
}
|
||||
case QMetaType::QVariantList: {
|
||||
QJsonArray arr;
|
||||
for (const QVariant& item : value.toList()) {
|
||||
arr.append(variantToJson(item));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
case QMetaType::QVariantMap: {
|
||||
QJsonObject obj;
|
||||
QVariantMap map = value.toMap();
|
||||
for (auto it = map.begin(); it != map.end(); ++it) {
|
||||
obj[it.key()] = variantToJson(it.value());
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
default:
|
||||
return value.toString();
|
||||
}
|
||||
}
|
||||
|
||||
void outputJson(const QString& filePath, const QString& typeName, const QString& displayName,
|
||||
const QVariantMap& vars, const QMap<QString, UiField>& schema,
|
||||
const ParseOptions& opts) {
|
||||
QJsonObject root;
|
||||
root["file"] = QFileInfo(filePath).fileName();
|
||||
root["detected_type"] = typeName;
|
||||
if (!displayName.isEmpty()) {
|
||||
root["display_name"] = displayName;
|
||||
}
|
||||
|
||||
QJsonArray fields;
|
||||
QJsonArray warnings;
|
||||
|
||||
// Get runtime UI metadata for display names
|
||||
QVariantMap uiMeta = vars.value("_ui_meta").toMap();
|
||||
|
||||
for (auto it = schema.begin(); it != schema.end(); ++it) {
|
||||
const QString& fieldName = it.key();
|
||||
const UiField& field = it.value();
|
||||
|
||||
if (fieldName.startsWith("_")) continue; // Skip internal fields
|
||||
if (!vars.contains(fieldName) && !opts.verbose) continue;
|
||||
|
||||
QVariant value = vars.value(fieldName);
|
||||
bool hasBlank = isBlankValue(value);
|
||||
bool hasZero = isZeroValue(value);
|
||||
|
||||
if (opts.warningsOnly && !hasBlank && !hasZero) continue;
|
||||
|
||||
QJsonObject fieldObj;
|
||||
fieldObj["name"] = fieldName;
|
||||
|
||||
// Use runtime display name if available, fallback to schema
|
||||
QString display = field.displayName;
|
||||
if (uiMeta.contains(fieldName)) {
|
||||
QVariantMap meta = uiMeta.value(fieldName).toMap();
|
||||
if (meta.contains("display") && !meta.value("display").toString().isEmpty()) {
|
||||
display = meta.value("display").toString();
|
||||
}
|
||||
}
|
||||
fieldObj["display"] = display;
|
||||
|
||||
fieldObj["value"] = variantToJson(value);
|
||||
fieldObj["type"] = getFieldTypeString(field.kind);
|
||||
fieldObj["visible"] = field.visible;
|
||||
|
||||
if (!field.readOnly) {
|
||||
fieldObj["editable"] = true;
|
||||
}
|
||||
|
||||
if (hasBlank) {
|
||||
fieldObj["warning"] = "blank";
|
||||
warnings.append(QString("%1 has blank/empty value").arg(fieldName));
|
||||
} else if (hasZero) {
|
||||
fieldObj["warning"] = "zero";
|
||||
warnings.append(QString("%1 has zero value").arg(fieldName));
|
||||
}
|
||||
|
||||
fields.append(fieldObj);
|
||||
}
|
||||
|
||||
// In verbose mode, also show non-UI fields
|
||||
if (opts.verbose) {
|
||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||
const QString& fieldName = it.key();
|
||||
if (fieldName.startsWith("_")) continue;
|
||||
if (schema.contains(fieldName)) continue; // Already added
|
||||
|
||||
QVariant value = it.value();
|
||||
bool hasBlank = isBlankValue(value);
|
||||
bool hasZero = isZeroValue(value);
|
||||
|
||||
if (opts.warningsOnly && !hasBlank && !hasZero) continue;
|
||||
|
||||
QJsonObject fieldObj;
|
||||
fieldObj["name"] = fieldName;
|
||||
fieldObj["display"] = fieldName;
|
||||
fieldObj["value"] = variantToJson(value);
|
||||
fieldObj["type"] = "hidden";
|
||||
fieldObj["visible"] = false;
|
||||
|
||||
if (hasBlank) {
|
||||
fieldObj["warning"] = "blank";
|
||||
} else if (hasZero) {
|
||||
fieldObj["warning"] = "zero";
|
||||
}
|
||||
|
||||
fields.append(fieldObj);
|
||||
}
|
||||
}
|
||||
|
||||
root["fields"] = fields;
|
||||
root["field_count"] = fields.size();
|
||||
|
||||
if (!warnings.isEmpty()) {
|
||||
root["warnings"] = warnings;
|
||||
}
|
||||
|
||||
QJsonDocument doc(root);
|
||||
out << doc.toJson(QJsonDocument::Indented);
|
||||
}
|
||||
|
||||
void outputTreeNode(const QString& name, const QVariant& value, int depth, bool showInternal) {
|
||||
QString indent = QString(" ").repeated(depth);
|
||||
QString prefix = depth > 0 ? "|-- " : "";
|
||||
|
||||
// Skip internal fields unless verbose
|
||||
if (!showInternal && name.startsWith("_")) return;
|
||||
|
||||
switch (value.typeId()) {
|
||||
case QMetaType::QVariantMap: {
|
||||
QVariantMap map = value.toMap();
|
||||
// Match GUI naming logic: _name takes priority, then _display, then key
|
||||
QString displayName;
|
||||
if (map.contains("_name") && !map.value("_name").toString().isEmpty()) {
|
||||
displayName = map.value("_name").toString();
|
||||
} else if (map.contains("_display") && !map.value("_display").toString().isEmpty()) {
|
||||
displayName = map.value("_display").toString();
|
||||
} else {
|
||||
displayName = name;
|
||||
}
|
||||
out << indent << prefix << Color::Cyan << displayName << Color::Reset << " {" << map.size() << "}\n";
|
||||
|
||||
// Sort keys for consistent output
|
||||
QStringList keys = map.keys();
|
||||
keys.sort();
|
||||
for (const QString& key : keys) {
|
||||
outputTreeNode(key, map.value(key), depth + 1, showInternal);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QMetaType::QVariantList: {
|
||||
QVariantList list = value.toList();
|
||||
out << indent << prefix << Color::Yellow << name << Color::Reset << " [" << list.size() << "]\n";
|
||||
int idx = 0;
|
||||
for (const QVariant& item : list) {
|
||||
QString itemName = QString("[%1]").arg(idx);
|
||||
outputTreeNode(itemName, item, depth + 1, showInternal);
|
||||
idx++;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case QMetaType::QByteArray: {
|
||||
QByteArray ba = value.toByteArray();
|
||||
out << indent << prefix << name << ": " << Color::Blue << "bytes[" << ba.size() << "]" << Color::Reset << "\n";
|
||||
break;
|
||||
}
|
||||
default:
|
||||
out << indent << prefix << name << ": " << formatValue(value, 100) << "\n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void outputTree(const QString& filePath, const QString& typeName, const QString& displayName,
|
||||
const QVariantMap& vars, const QMap<QString, UiField>& schema,
|
||||
const ParseOptions& opts) {
|
||||
out << Color::Bold << "File: " << Color::Reset << QFileInfo(filePath).fileName() << "\n";
|
||||
out << Color::Bold << "Type: " << Color::Reset << typeName;
|
||||
if (!displayName.isEmpty()) {
|
||||
out << " (" << displayName << ")";
|
||||
}
|
||||
out << "\n";
|
||||
out << QString("-").repeated(60) << "\n";
|
||||
|
||||
// Output tree structure
|
||||
QStringList keys = vars.keys();
|
||||
keys.sort();
|
||||
for (const QString& key : keys) {
|
||||
outputTreeNode(key, vars.value(key), 0, opts.verbose);
|
||||
}
|
||||
|
||||
out << QString("-").repeated(60) << "\n";
|
||||
|
||||
// Count total nodes for summary
|
||||
std::function<int(const QVariant&)> countNodes = [&](const QVariant& v) -> int {
|
||||
int count = 1;
|
||||
if (v.typeId() == QMetaType::QVariantMap) {
|
||||
QVariantMap map = v.toMap();
|
||||
for (auto it = map.begin(); it != map.end(); ++it) {
|
||||
count += countNodes(it.value());
|
||||
}
|
||||
} else if (v.typeId() == QMetaType::QVariantList) {
|
||||
for (const QVariant& item : v.toList()) {
|
||||
count += countNodes(item);
|
||||
}
|
||||
}
|
||||
return count;
|
||||
};
|
||||
|
||||
int totalNodes = 0;
|
||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||
totalNodes += countNodes(it.value());
|
||||
}
|
||||
|
||||
out << "Total nodes: " << totalNodes << "\n";
|
||||
}
|
||||
|
||||
void outputTable(const QString& filePath, const QString& typeName, const QString& displayName,
|
||||
const QVariantMap& vars, const QMap<QString, UiField>& schema,
|
||||
const ParseOptions& opts) {
|
||||
out << Color::Bold << "File: " << Color::Reset << QFileInfo(filePath).fileName() << "\n";
|
||||
out << Color::Bold << "Type: " << Color::Reset << typeName;
|
||||
if (!displayName.isEmpty()) {
|
||||
out << " (" << displayName << ")";
|
||||
}
|
||||
out << "\n\n";
|
||||
|
||||
// Get runtime UI metadata
|
||||
QVariantMap uiMeta = vars.value("_ui_meta").toMap();
|
||||
|
||||
// Calculate column widths
|
||||
int nameWidth = 12;
|
||||
int displayWidth = 14;
|
||||
int valueWidth = 30;
|
||||
int typeWidth = 8;
|
||||
|
||||
// Build rows
|
||||
struct Row {
|
||||
QString name;
|
||||
QString display;
|
||||
QString value;
|
||||
QString type;
|
||||
QString warning;
|
||||
};
|
||||
QVector<Row> rows;
|
||||
|
||||
for (auto it = schema.begin(); it != schema.end(); ++it) {
|
||||
const QString& fieldName = it.key();
|
||||
const UiField& field = it.value();
|
||||
|
||||
if (fieldName.startsWith("_")) continue;
|
||||
if (!vars.contains(fieldName) && !opts.verbose) continue;
|
||||
|
||||
QVariant value = vars.value(fieldName);
|
||||
bool hasBlank = isBlankValue(value);
|
||||
bool hasZero = isZeroValue(value);
|
||||
|
||||
if (opts.warningsOnly && !hasBlank && !hasZero) continue;
|
||||
|
||||
Row row;
|
||||
row.name = fieldName;
|
||||
|
||||
QString display = field.displayName;
|
||||
if (uiMeta.contains(fieldName)) {
|
||||
QVariantMap meta = uiMeta.value(fieldName).toMap();
|
||||
if (meta.contains("display") && !meta.value("display").toString().isEmpty()) {
|
||||
display = meta.value("display").toString();
|
||||
}
|
||||
}
|
||||
row.display = display;
|
||||
row.value = formatValue(value, 50);
|
||||
row.type = getFieldTypeString(field.kind);
|
||||
|
||||
if (hasBlank) row.warning = "BLANK";
|
||||
else if (hasZero) row.warning = "ZERO";
|
||||
|
||||
nameWidth = qMax(nameWidth, row.name.length() + 2);
|
||||
displayWidth = qMax(displayWidth, row.display.length() + 2);
|
||||
valueWidth = qMax(valueWidth, qMin(row.value.length() + 2, 52));
|
||||
typeWidth = qMax(typeWidth, row.type.length() + 2);
|
||||
|
||||
rows.append(row);
|
||||
}
|
||||
|
||||
// In verbose mode, add hidden fields
|
||||
if (opts.verbose) {
|
||||
for (auto it = vars.begin(); it != vars.end(); ++it) {
|
||||
const QString& fieldName = it.key();
|
||||
if (fieldName.startsWith("_")) continue;
|
||||
if (schema.contains(fieldName)) continue;
|
||||
|
||||
QVariant value = it.value();
|
||||
bool hasBlank = isBlankValue(value);
|
||||
bool hasZero = isZeroValue(value);
|
||||
|
||||
if (opts.warningsOnly && !hasBlank && !hasZero) continue;
|
||||
|
||||
Row row;
|
||||
row.name = fieldName;
|
||||
row.display = "(hidden)";
|
||||
row.value = formatValue(value, 50);
|
||||
row.type = "hidden";
|
||||
|
||||
if (hasBlank) row.warning = "BLANK";
|
||||
else if (hasZero) row.warning = "ZERO";
|
||||
|
||||
nameWidth = qMax(nameWidth, row.name.length() + 2);
|
||||
valueWidth = qMax(valueWidth, qMin(row.value.length() + 2, 52));
|
||||
|
||||
rows.append(row);
|
||||
}
|
||||
}
|
||||
|
||||
if (rows.isEmpty()) {
|
||||
out << "(no fields to display)\n";
|
||||
return;
|
||||
}
|
||||
|
||||
out << Color::Bold << "UI Fields:" << Color::Reset << "\n";
|
||||
|
||||
// Header
|
||||
out << QString("+%1+%2+%3+%4+%5+\n")
|
||||
.arg(QString("-").repeated(nameWidth))
|
||||
.arg(QString("-").repeated(displayWidth))
|
||||
.arg(QString("-").repeated(valueWidth))
|
||||
.arg(QString("-").repeated(typeWidth))
|
||||
.arg(QString("-").repeated(9));
|
||||
|
||||
out << QString("| %1| %2| %3| %4| %5|\n")
|
||||
.arg("Name", -nameWidth + 1)
|
||||
.arg("Display", -displayWidth + 1)
|
||||
.arg("Value", -valueWidth + 1)
|
||||
.arg("Type", -typeWidth + 1)
|
||||
.arg("Warning", -8);
|
||||
|
||||
out << QString("+%1+%2+%3+%4+%5+\n")
|
||||
.arg(QString("-").repeated(nameWidth))
|
||||
.arg(QString("-").repeated(displayWidth))
|
||||
.arg(QString("-").repeated(valueWidth))
|
||||
.arg(QString("-").repeated(typeWidth))
|
||||
.arg(QString("-").repeated(9));
|
||||
|
||||
// Rows
|
||||
for (const Row& row : rows) {
|
||||
QString valueStr = row.value;
|
||||
if (valueStr.length() > valueWidth - 1) {
|
||||
valueStr = valueStr.left(valueWidth - 4) + "...";
|
||||
}
|
||||
|
||||
QString warningStr = row.warning;
|
||||
if (!warningStr.isEmpty()) {
|
||||
warningStr = Color::Yellow + warningStr + Color::Reset;
|
||||
}
|
||||
|
||||
out << QString("| %1| %2| %3| %4| %5|\n")
|
||||
.arg(row.name, -nameWidth + 1)
|
||||
.arg(row.display, -displayWidth + 1)
|
||||
.arg(valueStr, -valueWidth + 1)
|
||||
.arg(row.type, -typeWidth + 1)
|
||||
.arg(row.warning, -8);
|
||||
}
|
||||
|
||||
out << QString("+%1+%2+%3+%4+%5+\n")
|
||||
.arg(QString("-").repeated(nameWidth))
|
||||
.arg(QString("-").repeated(displayWidth))
|
||||
.arg(QString("-").repeated(valueWidth))
|
||||
.arg(QString("-").repeated(typeWidth))
|
||||
.arg(QString("-").repeated(9));
|
||||
|
||||
out << "\nTotal fields: " << rows.size() << "\n";
|
||||
}
|
||||
|
||||
int cmdParse(const QString& filePath, const ParseOptions& opts) {
|
||||
// Determine definitions path
|
||||
QString defsPath = opts.definitionsPath;
|
||||
if (defsPath.isEmpty()) {
|
||||
// Try common locations
|
||||
QStringList searchPaths = {
|
||||
QCoreApplication::applicationDirPath() + "/definitions",
|
||||
QCoreApplication::applicationDirPath() + "/../definitions",
|
||||
QCoreApplication::applicationDirPath() + "/../../definitions",
|
||||
QDir::currentPath() + "/definitions",
|
||||
"definitions"
|
||||
};
|
||||
for (const QString& path : searchPaths) {
|
||||
if (QDir(path).exists()) {
|
||||
defsPath = path;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (defsPath.isEmpty() || !QDir(defsPath).exists()) {
|
||||
err << Color::Red << "Error: Could not find definitions directory" << Color::Reset << "\n";
|
||||
err << "Use --definitions=<path> to specify the path\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Load all XScript definitions
|
||||
TypeRegistry registry;
|
||||
int loadedCount = 0;
|
||||
|
||||
QDirIterator it(defsPath, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories);
|
||||
while (it.hasNext()) {
|
||||
QString scriptPath = it.next();
|
||||
QFile scriptFile(scriptPath);
|
||||
if (scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||||
try {
|
||||
registry.ingestScript(QString::fromUtf8(scriptFile.readAll()), scriptPath);
|
||||
loadedCount++;
|
||||
} catch (const std::exception& e) {
|
||||
err << Color::Yellow << "Warning: Failed to load " << scriptPath << ": "
|
||||
<< e.what() << Color::Reset << "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (loadedCount == 0) {
|
||||
err << Color::Red << "Error: No XScript definitions loaded from " << defsPath << Color::Reset << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Open binary file
|
||||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::ReadOnly)) {
|
||||
err << Color::Red << "Error: Could not open file: " << filePath << Color::Reset << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Determine type
|
||||
QString typeName = opts.typeName;
|
||||
if (typeName.isEmpty()) {
|
||||
typeName = registry.chooseType(&file, filePath);
|
||||
file.seek(0); // Reset after type detection
|
||||
}
|
||||
|
||||
if (typeName.isEmpty()) {
|
||||
err << Color::Red << "Error: Could not detect file type" << Color::Reset << "\n";
|
||||
err << "Use --type=<typename> to specify the type manually\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!registry.hasType(typeName)) {
|
||||
err << Color::Red << "Error: Unknown type '" << typeName << "'" << Color::Reset << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Parse the file
|
||||
QVariantMap vars;
|
||||
try {
|
||||
vars = registry.parse(typeName, &file, filePath);
|
||||
} catch (const std::exception& e) {
|
||||
err << Color::Red << "Error parsing file: " << e.what() << Color::Reset << "\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Get UI schema for this type
|
||||
const TypeDef& td = registry.module().types[typeName];
|
||||
QMap<QString, UiField> schema = buildUiSchemaForType(td, ®istry.module());
|
||||
|
||||
QString displayName = td.display;
|
||||
|
||||
// Output results
|
||||
if (opts.format == "json") {
|
||||
outputJson(filePath, typeName, displayName, vars, schema, opts);
|
||||
} else if (opts.format == "table") {
|
||||
outputTable(filePath, typeName, displayName, vars, schema, opts);
|
||||
} else if (opts.format == "tree") {
|
||||
outputTree(filePath, typeName, displayName, vars, schema, opts);
|
||||
} else {
|
||||
err << Color::Red << "Error: Unknown format '" << opts.format << "'" << Color::Reset << "\n";
|
||||
err << "Valid formats: json, table, tree\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QCoreApplication app(argc, argv);
|
||||
QCoreApplication::setApplicationName("xscript-cli");
|
||||
@ -305,9 +900,22 @@ int main(int argc, char *argv[]) {
|
||||
parser.addHelpOption();
|
||||
parser.addVersionOption();
|
||||
|
||||
parser.addPositionalArgument("command", "Command to run: check, check-dir, list-types, info");
|
||||
parser.addPositionalArgument("command", "Command to run: check, check-dir, list-types, info, parse");
|
||||
parser.addPositionalArgument("args", "Command arguments", "[args...]");
|
||||
|
||||
// Parse command options
|
||||
QCommandLineOption verboseOpt({"v", "verbose"}, "Show all fields including hidden ones");
|
||||
QCommandLineOption formatOpt({"f", "format"}, "Output format: json (default), table, tree", "format", "json");
|
||||
QCommandLineOption definitionsOpt({"d", "definitions"}, "Path to definitions directory", "path");
|
||||
QCommandLineOption typeOpt({"t", "type"}, "Force specific type (skip auto-detect)", "typename");
|
||||
QCommandLineOption warningsOpt({"w", "warnings-only"}, "Only show fields with blank/zero values");
|
||||
|
||||
parser.addOption(verboseOpt);
|
||||
parser.addOption(formatOpt);
|
||||
parser.addOption(definitionsOpt);
|
||||
parser.addOption(typeOpt);
|
||||
parser.addOption(warningsOpt);
|
||||
|
||||
parser.process(app);
|
||||
|
||||
const QStringList args = parser.positionalArguments();
|
||||
@ -319,6 +927,19 @@ int main(int argc, char *argv[]) {
|
||||
err << " xscript-cli check-dir <directory> - Check all .xscript files\n";
|
||||
err << " xscript-cli list-types <file.xscript> - List all types in file\n";
|
||||
err << " xscript-cli info <file.xscript> <type> - Show type info\n";
|
||||
err << " xscript-cli parse <binary-file> - Parse file and show UI fields\n";
|
||||
err << "\n";
|
||||
err << "Parse command options:\n";
|
||||
err << " -v, --verbose Show all fields including hidden ones\n";
|
||||
err << " -f, --format=<fmt> Output format: json (default), table, tree\n";
|
||||
err << " -d, --definitions=<path> Path to definitions directory\n";
|
||||
err << " -t, --type=<typename> Force specific type (skip auto-detect)\n";
|
||||
err << " -w, --warnings-only Only show fields with blank/zero values\n";
|
||||
err << "\n";
|
||||
err << "Formats:\n";
|
||||
err << " json JSON output with field metadata\n";
|
||||
err << " table Tabular output of UI fields\n";
|
||||
err << " tree Hierarchical tree view of entire structure\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
@ -352,9 +973,24 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
return cmdInfo(args[1], args[2]);
|
||||
}
|
||||
else if (command == "parse") {
|
||||
if (args.size() < 2) {
|
||||
err << "Error: parse requires a binary file path\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
ParseOptions opts;
|
||||
opts.verbose = parser.isSet(verboseOpt);
|
||||
opts.format = parser.value(formatOpt);
|
||||
opts.definitionsPath = parser.value(definitionsOpt);
|
||||
opts.typeName = parser.value(typeOpt);
|
||||
opts.warningsOnly = parser.isSet(warningsOpt);
|
||||
|
||||
return cmdParse(args[1], opts);
|
||||
}
|
||||
else {
|
||||
err << "Error: Unknown command '" << command << "'\n";
|
||||
err << "Valid commands: check, check-dir, list-types, info\n";
|
||||
err << "Valid commands: check, check-dir, list-types, info, parse\n";
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user