#include "StarFile.hpp" #include "json_tool.hpp" #include "editor_gui.hpp" // Tool for scripting and mass-editing of JSON+Comments files without affecting // formatting. using namespace Star; FormattedJson GenericInputFormat::toJson(String const& input) const { return FormattedJson::parse(input); } String GenericInputFormat::fromJson(FormattedJson const& json) const { return json.repr(); } FormattedJson GenericInputFormat::getDefault() const { return FormattedJson::ofType(Json::Type::Null); } FormattedJson CommaSeparatedStrings::toJson(String const& input) const { if (input.trim() == "") return FormattedJson::ofType(Json::Type::Array); StringList strings = input.split(','); JsonArray array = strings.transformed(bind(&String::trim, _1, "")).transformed(construct()); return Json(array); } String CommaSeparatedStrings::fromJson(FormattedJson const& json) const { StringList strings = json.toJson().toArray().transformed(bind(&Json::toString, _1)); return strings.join(", "); } FormattedJson CommaSeparatedStrings::getDefault() const { return FormattedJson::ofType(Json::Type::Array); } FormattedJson StringInputFormat::toJson(String const& input) const { return FormattedJson(Json(input)); } String StringInputFormat::fromJson(FormattedJson const& json) const { return json.toJson().toString(); } FormattedJson StringInputFormat::getDefault() const { return FormattedJson(Json("")); } String Star::reprWithLineEnding(FormattedJson const& json) { // Append a newline, preserving the newline style of the file, e.g windows or // unix. String repr = json.repr(); if (repr.contains("\r")) return strf("{}\r\n", repr); return strf("{}\n", repr); } void OutputOnSeparateLines::out(FormattedJson const& json) { coutf("{}", reprWithLineEnding(json)); } void OutputOnSeparateLines::flush() {} void ArrayOutput::out(FormattedJson const& json) { if (!m_unique || !m_results.contains(json)) m_results.append(json); } void ArrayOutput::flush() { FormattedJson array = FormattedJson::ofType(Json::Type::Array); for (FormattedJson const& result : m_results) { array = array.append(result); } coutf("{}", reprWithLineEnding(array)); } FormattedJson Star::addOrSet(bool add, JsonPath::PathPtr path, FormattedJson const& input, InsertLocation insertLocation, FormattedJson const& value) { JsonPath::EmptyPathOp emptyPathOp = [&value, add](FormattedJson const& document) { if (!add || document.type() == Json::Type::Null) return value; throw JsonException("Cannot add a value to the entire document, it is not empty."); }; JsonPath::ObjectOp objectOp = [&value, &insertLocation]( FormattedJson const& object, String const& key) { if (insertLocation.is()) return object.prepend(key, value); if (insertLocation.is()) return object.append(key, value); if (insertLocation.is()) return object.insertBefore(key, value, insertLocation.get().key); if (insertLocation.is()) return object.insertAfter(key, value, insertLocation.get().key); return object.set(key, value); }; JsonPath::ArrayOp arrayOp = [&value, add](FormattedJson const& array, Maybe i) { if (i.isValid()) { if (add) return array.insert(*i, value); return array.set(*i, value); } return array.append(value); }; return path->apply(input, emptyPathOp, objectOp, arrayOp); } void forEachFileRecursive(String const& directory, function func) { for (pair entry : File::dirList(directory)) { String filename = File::relativeTo(directory, entry.first); if (entry.second) forEachFileRecursive(filename, func); else func(filename); } } StringList Star::findFiles(FindInput const& findArgs) { StringList matches; forEachFileRecursive(findArgs.directory, [&findArgs, &matches](String const& filename) { if (filename.endsWith(findArgs.filenameSuffix)) matches.append(filename); }); return matches; } void forEachChild(FormattedJson const& parent, function func) { if (parent.isType(Json::Type::Object)) { for (String const& key : parent.toJson().toObject().keys()) { func(parent.get(key)); } } else if (parent.isType(Json::Type::Array)) { for (size_t i = 0; i < parent.size(); ++i) { func(parent.get(i)); } } else { throw JsonPath::TraversalException::format( "Cannot get the children of Json type {}, must be either Array or Object", parent.typeName()); } } bool process(function output, Command const& command, Options const& options, FormattedJson const& input) { if (command.is()) { GetCommand const& getCmd = command.get(); try { FormattedJson value = getCmd.path->get(input); if (getCmd.children) { forEachChild(value, output); } else { output(value); } } catch (JsonPath::TraversalException const& e) { if (!getCmd.opt) throw e; } } else if (command.is()) { SetCommand const& setCmd = command.get(); output(addOrSet(false, setCmd.path, input, options.insertLocation, setCmd.value)); } else if (command.is()) { AddCommand const& addCmd = command.get(); output(addOrSet(true, addCmd.path, input, options.insertLocation, addCmd.value)); } else if (command.is()) { output(command.get().path->remove(input)); } else { starAssert(command.empty()); output(input); } return true; } bool process( function output, Command const& command, Options const& options, String const& input) { FormattedJson inJson = FormattedJson::parse(input); return process(output, command, options, inJson); } bool process( function output, Command const& command, Options const& options, Input const& input) { if (input.is()) { return process(output, command, options, input.get().json); } bool success = true; StringList files; if (input.is()) { files = StringList{input.get().filename}; } else { files = findFiles(input.get()); } for (String const& file : files) { if (options.inPlace) { output = [&file](FormattedJson const& json) { File::writeFile(reprWithLineEnding(json), file); }; } success &= process(output, command, options, File::readFileString(file)); } return success; } JsonPath::PathPtr parsePath(String const& path) { if (path.beginsWith("/")) return make_shared(path); return make_shared(path); } pair parseGetPath(String path) { // --get and --opt have a special syntax for getting the child values of // the value at the given path. These end with *, e.g.: // /foo/bar/* // foo.bar.* // foo.bar[*] bool children = false; if (path.endsWith("/*") || path.endsWith(".*")) { path = path.substr(0, path.size() - 2); children = true; } else if (path.endsWith("[*]")) { path = path.substr(0, path.size() - 3); children = true; } return make_pair(parsePath(path), children); } Maybe parseArgs(int argc, char** argv) { // Skip the program name Deque args = Deque(argv + 1, argv + argc); ParsedArgs parsed; // Parse option arguments while (!args.empty()) { String const& arg = args.takeFirst(); if (arg == "--get" || arg == "--opt") { // Retrieve values at a given path in the Json document if (!parsed.command.empty() || args.empty()) return {}; JsonPath::PathPtr path; bool children = false; tie(path, children) = parseGetPath(args.takeFirst()); parsed.command = GetCommand{path, arg == "--opt", children}; } else if (arg == "--set") { // Set the value at the given path in the Json document if (!parsed.command.empty() || args.size() < 2) return {}; JsonPath::PathPtr path = parsePath(args.takeFirst()); FormattedJson value = FormattedJson::parse(args.takeFirst()); parsed.command = SetCommand{path, value}; } else if (arg == "--add") { // Add (insert) a path to a Json document if (!parsed.command.empty() || args.size() < 2) return {}; JsonPath::PathPtr path = parsePath(args.takeFirst()); FormattedJson value = FormattedJson::parse(args.takeFirst()); parsed.command = AddCommand{path, value}; } else if (arg == "--remove") { // Remove a path from a Json document if (!parsed.command.empty() || args.empty()) return {}; parsed.command = RemoveCommand{parsePath(args.takeFirst())}; } else if (arg == "--edit") { // Interactive bullk Json editor if (!parsed.command.empty() || args.empty()) return {}; parsed.command = EditCommand{parsePath(args.takeFirst())}; } else if (arg == "--editor-image") { if (args.empty()) return {}; parsed.options.editorImages.append(parsePath(args.takeFirst())); } else if (arg == "--input") { // Configure the input syntax for --edit if (args.empty()) return {}; String format = args.takeFirst(); if (format == "json" || format == "generic") parsed.options.editFormat = make_shared(); else if (format == "css" || format == "csv") parsed.options.editFormat = make_shared(); else if (format == "string") parsed.options.editFormat = make_shared(); else return {}; } else if (arg == "--array") { // Output multiple results as a single array parsed.options.output = make_shared(false); } else if (arg == "--array-unique") { // Output multiple results as a single array, with duplicate results // removed parsed.options.output = make_shared(true); } else if (arg == "--help") { return {}; } else if (arg == "-j") { // Use command line argument as input parsed.inputs.append(JsonLiteralInput{args.takeFirst()}); } else if (arg == "--find") { // Search for files recursively in the given directory with a given // suffix. if (args.size() < 2) return {}; String directory = args.takeFirst(); String suffix = args.takeFirst(); parsed.inputs.append(FindInput{directory, suffix}); } else if (arg == "-i") { // Update files in place rather than print to stdout parsed.options.inPlace = true; } else if (arg == "--at") { // Insert new object keys at the beginning or end of the document if (!parsed.options.insertLocation.empty() || args.size() < 1) return {}; String pos = args.takeFirst(); if (pos == "beginning" || pos == "start") parsed.options.insertLocation = AtBeginning{}; else if (pos == "end") parsed.options.insertLocation = AtEnd{}; else return {}; } else if (arg == "--before") { // Insert new object keys before the given key if (!parsed.options.insertLocation.empty() || args.size() < 1) return {}; parsed.options.insertLocation = BeforeKey{args.takeFirst()}; } else if (arg == "--after") { // Insert new object keys after the given key if (!parsed.options.insertLocation.empty() || args.size() < 1) return {}; parsed.options.insertLocation = AfterKey{args.takeFirst()}; } else { if (!File::exists(arg)) { cerrf("File {} doesn't exist\n", arg); return {}; } parsed.inputs.append(FileInput{arg}); } } if (!parsed.options.output) parsed.options.output = make_shared(); bool anyFileInputs = false, anyNonFileInputs = false; for (Input const& input : parsed.inputs) { if (input.is() || input.is()) { anyFileInputs = true; } else { anyNonFileInputs = true; } } if (parsed.command.is() && !anyFileInputs) { cerrf("Files to edit must be supplied when using --edit.\n"); return {}; } if (parsed.options.inPlace && !anyFileInputs) { cerrf("In-place writing (-i) can only be used with files specified on the command line.\n"); return {}; } if (parsed.options.inPlace && parsed.command.is()) { cerrf("Interactive edit (--edit) is always in-place. Explicitly specifying -i is not needed.\n"); return {}; } if (parsed.command.is() && anyNonFileInputs) { cerrf("Interactie edit (--edit) can only be used with file input sources.\n"); return {}; } if (parsed.options.editFormat && !parsed.command.is()) { cerrf("--input can only be used with --edit.\n"); return {}; } else if (!parsed.options.editFormat && parsed.command.is()) { parsed.options.editFormat = make_shared(); } return parsed; } String readStdin() { String result; char buffer[1024]; while (!feof(stdin)) { size_t readBytes = fread(buffer, 1, 1024, stdin); result.append(buffer, readBytes); } return result; } int main(int argc, char** argv) { try { Maybe parsedArgs = parseArgs(argc, argv); if (!parsedArgs) { cerrf("Usage: {} [--get ] (-j | *)\n", argv[0]); cerrf( "Usage: {} --set [-i] [(--at (beginning|end) | --before | --after )] (-j " " | *)\n", argv[0]); cerrf( "Usage: {} --add [-i] [(--at (beginning|end) | --before | --after )] (-j " " | *)\n", argv[0]); cerrf( "Usage: {} --edit [(--at (beginning|end) | --before | --after )] [--input " "(csv|json|string)] +\n", argv[0]); cerrf("\n"); cerrf("Example: {} --get /dialog/0/message guard.npctype\n", argv[0]); cerrf("Example: {} --get 'foo[0]' -j '{\"foo\":[0,1,2,3]}'\n", argv[0]); cerrf("Example: {} --edit /tags --input csv --find ../assets/ .object\n", argv[0]); return 1; } OutputPtr output = parsedArgs->options.output; bool success = true; if (parsedArgs->command.is()) { return edit(argc, argv, parsedArgs->command.get().path, parsedArgs->options, parsedArgs->inputs); } else if (parsedArgs->inputs.size() == 0) { // No files were provided. Reading from stdin success &= process(output->toFunction(), parsedArgs->command, parsedArgs->options, readStdin()); } else { for (Input const& input : parsedArgs->inputs) { success &= process(output->toFunction(), parsedArgs->command, parsedArgs->options, input); } } output->flush(); if (!success) return 1; return 0; } catch (JsonParsingException const& e) { cerrf("{}\n", e.what()); return 1; } catch (JsonException const& e) { cerrf("{}\n", e.what()); return 1; } catch (std::exception const& e) { cerrf("Exception caught: {}\n", outputException(e, true)); return 1; } }