// xscript-cli - XScript definition file validator and parser // // Usage: // xscript-cli check - Check syntax and validate references // xscript-cli check-dir - Check all .xscript files in directory // xscript-cli list-types - List all types defined in file // xscript-cli info - Show info about a specific type #include #include #include #include #include #include #include #include "lexer.h" #include "parser.h" #include "typeregistry.h" static QTextStream out(stdout); static QTextStream err(stderr); // Color codes for terminal output namespace Color { const char* Reset = "\033[0m"; const char* Red = "\033[31m"; const char* Green = "\033[32m"; const char* Yellow = "\033[33m"; const char* Blue = "\033[34m"; const char* Cyan = "\033[36m"; const char* Bold = "\033[1m"; } struct CheckResult { QString filePath; QString fileName; bool success = false; QString errorMessage; QStringList typeNames; int warningCount = 0; QStringList warnings; }; CheckResult checkFile(const QString& filePath) { CheckResult result; result.filePath = filePath; result.fileName = QFileInfo(filePath).fileName(); QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { result.errorMessage = "Failed to open file"; return result; } QString content = QString::fromUtf8(file.readAll()); file.close(); try { Lexer lexer(content); Parser parser(std::move(lexer)); Module module = parser.parseModule(); result.success = true; // Collect type names for (auto it = module.types.begin(); it != module.types.end(); ++it) { result.typeNames.append(it.key()); } // Check for potential issues (warnings) for (auto it = module.types.begin(); it != module.types.end(); ++it) { const TypeDef& td = it.value(); // Warn if root type has no criteria if (td.isRoot && td.criteria.isEmpty()) { result.warnings.append(QString("Type '%1' is root but has no criteria block").arg(td.name)); result.warningCount++; } // Warn if type has no body if (td.body.isEmpty()) { result.warnings.append(QString("Type '%1' has an empty body").arg(td.name)); result.warningCount++; } } } catch (const std::exception& e) { result.errorMessage = QString::fromStdString(e.what()); } return result; } int cmdCheck(const QString& filePath) { CheckResult result = checkFile(filePath); if (result.success) { out << Color::Green << "OK" << Color::Reset << " " << result.fileName; if (!result.typeNames.isEmpty()) { out << " (" << result.typeNames.size() << " type(s))"; } out << "\n"; for (const QString& warning : result.warnings) { out << Color::Yellow << " WARNING: " << Color::Reset << warning << "\n"; } return 0; } else { err << Color::Red << "ERROR" << Color::Reset << " " << result.fileName << "\n"; err << " " << result.errorMessage << "\n"; return 1; } } int cmdCheckDir(const QString& dirPath) { QDir dir(dirPath); if (!dir.exists()) { err << Color::Red << "Error: Directory does not exist: " << dirPath << Color::Reset << "\n"; return 1; } QDirIterator it(dirPath, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories); int totalFiles = 0; int successCount = 0; int errorCount = 0; int warningCount = 0; QStringList allTypes; QStringList failedFiles; while (it.hasNext()) { QString filePath = it.next(); totalFiles++; CheckResult result = checkFile(filePath); // Show relative path from the directory QString relativePath = dir.relativeFilePath(filePath); if (result.success) { successCount++; out << Color::Green << "OK" << Color::Reset << " " << relativePath; if (!result.typeNames.isEmpty()) { out << " (" << result.typeNames.size() << " type(s))"; allTypes.append(result.typeNames); } out << "\n"; warningCount += result.warningCount; for (const QString& warning : result.warnings) { out << Color::Yellow << " WARNING: " << Color::Reset << warning << "\n"; } } else { errorCount++; failedFiles.append(relativePath); err << Color::Red << "ERROR" << Color::Reset << " " << relativePath << "\n"; err << " " << result.errorMessage << "\n"; } } // Summary out << "\n" << Color::Bold << "Summary:" << Color::Reset << "\n"; out << " Files checked: " << totalFiles << "\n"; out << " " << Color::Green << "Passed: " << successCount << Color::Reset << "\n"; if (errorCount > 0) { out << " " << Color::Red << "Failed: " << errorCount << Color::Reset << "\n"; } if (warningCount > 0) { out << " " << Color::Yellow << "Warnings: " << warningCount << Color::Reset << "\n"; } out << " Total types: " << allTypes.size() << "\n"; // Validate cross-file references if (!allTypes.isEmpty()) { TypeRegistry registry; int refErrors = 0; // Re-load all files into a single registry QDirIterator it2(dirPath, {"*.xscript"}, QDir::Files, QDirIterator::Subdirectories); while (it2.hasNext()) { QString filePath = it2.next(); QFile file(filePath); if (file.open(QIODevice::ReadOnly | QIODevice::Text)) { try { registry.ingestScript(QString::fromUtf8(file.readAll()), filePath); } catch (...) { // Already reported } } } QStringList refErrorList = registry.validateTypeReferences(); if (!refErrorList.isEmpty()) { out << "\n" << Color::Red << "Reference errors:" << Color::Reset << "\n"; for (const QString& refErr : refErrorList) { out << " " << refErr << "\n"; refErrors++; } } if (refErrors > 0) { errorCount += refErrors; } } return (errorCount > 0) ? 1 : 0; } int cmdListTypes(const QString& filePath) { CheckResult result = checkFile(filePath); if (!result.success) { err << Color::Red << "Error parsing file:" << Color::Reset << "\n"; err << " " << result.errorMessage << "\n"; return 1; } out << Color::Bold << "Types in " << result.fileName << ":" << Color::Reset << "\n"; // Re-parse to get more details QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return 1; } QString content = QString::fromUtf8(file.readAll()); Lexer lexer(content); Parser parser(std::move(lexer)); Module module = parser.parseModule(); for (auto it = module.types.begin(); it != module.types.end(); ++it) { const TypeDef& td = it.value(); out << " " << Color::Cyan << td.name << Color::Reset; QStringList flags; if (td.isRoot) flags.append("root"); if (!td.display.isEmpty()) flags.append(QString("display=\"%1\"").arg(td.display)); if (td.order == ByteOrder::BE) flags.append("BE"); else flags.append("LE"); if (!flags.isEmpty()) { out << " [" << flags.join(", ") << "]"; } out << "\n"; if (!td.criteria.isEmpty()) { out << " criteria: " << td.criteria.size() << " statement(s)\n"; } out << " body: " << td.body.size() << " statement(s)\n"; } return 0; } int cmdInfo(const QString& filePath, const QString& typeName) { QFile file(filePath); if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { err << Color::Red << "Error: Failed to open file" << Color::Reset << "\n"; return 1; } QString content = QString::fromUtf8(file.readAll()); try { Lexer lexer(content); Parser parser(std::move(lexer)); Module module = parser.parseModule(); if (!module.types.contains(typeName)) { err << Color::Red << "Error: Type '" << typeName << "' not found" << Color::Reset << "\n"; err << "Available types:\n"; for (const QString& name : module.types.keys()) { err << " " << name << "\n"; } return 1; } const TypeDef& td = module.types[typeName]; out << Color::Bold << "Type: " << td.name << Color::Reset << "\n"; out << " Root: " << (td.isRoot ? "yes" : "no") << "\n"; out << " Display: " << (td.display.isEmpty() ? "(none)" : td.display) << "\n"; out << " Byte order: " << (td.order == ByteOrder::BE ? "big-endian" : "little-endian") << "\n"; out << " Criteria statements: " << td.criteria.size() << "\n"; out << " Body statements: " << td.body.size() << "\n"; } catch (const std::exception& e) { err << Color::Red << "Error parsing file: " << e.what() << Color::Reset << "\n"; return 1; } return 0; } int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); QCoreApplication::setApplicationName("xscript-cli"); QCoreApplication::setApplicationVersion("1.0.0"); QCommandLineParser parser; parser.setApplicationDescription("XScript definition file validator and parser"); parser.addHelpOption(); parser.addVersionOption(); parser.addPositionalArgument("command", "Command to run: check, check-dir, list-types, info"); parser.addPositionalArgument("args", "Command arguments", "[args...]"); parser.process(app); const QStringList args = parser.positionalArguments(); if (args.isEmpty()) { err << "Error: No command specified\n\n"; err << "Usage:\n"; err << " xscript-cli check - Check syntax and validate\n"; err << " xscript-cli check-dir - Check all .xscript files\n"; err << " xscript-cli list-types - List all types in file\n"; err << " xscript-cli info - Show type info\n"; return 1; } const QString command = args[0]; if (command == "check") { if (args.size() < 2) { err << "Error: check requires a file path\n"; return 1; } return cmdCheck(args[1]); } else if (command == "check-dir") { if (args.size() < 2) { err << "Error: check-dir requires a directory path\n"; return 1; } return cmdCheckDir(args[1]); } else if (command == "list-types") { if (args.size() < 2) { err << "Error: list-types requires a file path\n"; return 1; } return cmdListTypes(args[1]); } else if (command == "info") { if (args.size() < 3) { err << "Error: info requires a file path and type name\n"; return 1; } return cmdInfo(args[1], args[2]); } else { err << "Error: Unknown command '" << command << "'\n"; err << "Valid commands: check, check-dir, list-types, info\n"; return 1; } }