v1.4.4
This commit is contained in:
commit
9c94d113d3
10260 changed files with 1237388 additions and 0 deletions
19
source/json_tool/CMakeLists.txt
Normal file
19
source/json_tool/CMakeLists.txt
Normal file
|
@ -0,0 +1,19 @@
|
|||
INCLUDE_DIRECTORIES (
|
||||
${STAR_EXTERN_INCLUDES}
|
||||
${STAR_CORE_INCLUDES}
|
||||
)
|
||||
|
||||
FIND_PACKAGE (Qt5Core)
|
||||
FIND_PACKAGE (Qt5Widgets)
|
||||
|
||||
SET (CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
SET (CMAKE_AUTOMOC ON)
|
||||
|
||||
ADD_EXECUTABLE (json_tool WIN32
|
||||
$<TARGET_OBJECTS:star_extern> $<TARGET_OBJECTS:star_core>
|
||||
json_tool.cpp editor_gui.cpp)
|
||||
QT5_USE_MODULES (json_tool Widgets Gui Core)
|
||||
TARGET_LINK_LIBRARIES (json_tool ${STAR_EXT_LIBS})
|
||||
|
||||
SET (CMAKE_AUTOMOC OFF)
|
||||
SET (CMAKE_INCLUDE_CURRENT_DIR OFF)
|
206
source/json_tool/editor_gui.cpp
Normal file
206
source/json_tool/editor_gui.cpp
Normal file
|
@ -0,0 +1,206 @@
|
|||
#include <QApplication>
|
||||
#include <QGridLayout>
|
||||
#include <QPushButton>
|
||||
#include "StarFile.hpp"
|
||||
#include "json_tool.hpp"
|
||||
#include "editor_gui.hpp"
|
||||
|
||||
using namespace Star;
|
||||
|
||||
int const ImagePreviewWidth = 300;
|
||||
int const ImagePreviewHeight = 600;
|
||||
|
||||
JsonEditor::JsonEditor(JsonPath::PathPtr const& path, Options const& options, List<String> const& files)
|
||||
: QMainWindow(), m_path(path), m_editFormat(options.editFormat), m_options(options), m_files(files), m_fileIndex(0) {
|
||||
auto* w = new QWidget(this);
|
||||
w->setObjectName("background");
|
||||
setCentralWidget(w);
|
||||
setWindowTitle("Json Editor");
|
||||
resize(1280, 720);
|
||||
|
||||
auto* layout = new QGridLayout(centralWidget());
|
||||
|
||||
QFont font("Monospace");
|
||||
font.setStyleHint(QFont::StyleHint::Monospace);
|
||||
|
||||
m_jsonDocument = new QTextDocument("Hello world");
|
||||
m_jsonDocument->setDefaultFont(font);
|
||||
|
||||
m_statusLabel = new QLabel(centralWidget());
|
||||
layout->addWidget(m_statusLabel, 0, 0, 1, 5);
|
||||
|
||||
m_jsonPreview = new QTextEdit(this);
|
||||
m_jsonPreview->setReadOnly(true);
|
||||
m_jsonPreview->setDocument(m_jsonDocument);
|
||||
layout->addWidget(m_jsonPreview, 1, 0, 1, 5);
|
||||
|
||||
m_backButton = new QPushButton(centralWidget());
|
||||
m_backButton->setText("« Back");
|
||||
layout->addWidget(m_backButton, 2, 0);
|
||||
connect(m_backButton, SIGNAL(pressed()), this, SLOT(back()));
|
||||
|
||||
m_pathLabel = new QLabel(centralWidget());
|
||||
layout->addWidget(m_pathLabel, 2, 1);
|
||||
|
||||
m_imageLabel = new QLabel(centralWidget());
|
||||
m_imageLabel->setMaximumSize(ImagePreviewWidth, ImagePreviewHeight);
|
||||
m_imageLabel->setMinimumSize(ImagePreviewWidth, ImagePreviewHeight);
|
||||
m_imageLabel->setAlignment(Qt::AlignCenter);
|
||||
if (!m_options.editorImages.empty())
|
||||
layout->addWidget(m_imageLabel, 1, 5);
|
||||
|
||||
m_valueEditor = new QLineEdit(centralWidget());
|
||||
m_valueEditor->setFont(font);
|
||||
layout->addWidget(m_valueEditor, 2, 2);
|
||||
connect(m_valueEditor, SIGNAL(returnPressed()), this, SLOT(next()));
|
||||
connect(m_valueEditor, SIGNAL(textChanged(QString const&)), this, SLOT(updatePreview(QString const&)));
|
||||
|
||||
m_nextButton = new QPushButton(centralWidget());
|
||||
m_nextButton->setText("Next »");
|
||||
m_nextButton->setDefault(true);
|
||||
layout->addWidget(m_nextButton, 2, 3);
|
||||
connect(m_nextButton, SIGNAL(pressed()), this, SLOT(next()));
|
||||
|
||||
m_errorDialog = new QErrorMessage(this);
|
||||
m_errorDialog->setModal(true);
|
||||
|
||||
displayCurrentFile();
|
||||
}
|
||||
|
||||
void JsonEditor::next() {
|
||||
if (!m_valueEditor->isEnabled() || saveChanges()) {
|
||||
++m_fileIndex;
|
||||
if (m_fileIndex >= m_files.size()) {
|
||||
close();
|
||||
return;
|
||||
}
|
||||
|
||||
displayCurrentFile();
|
||||
}
|
||||
}
|
||||
|
||||
void JsonEditor::back() {
|
||||
if (m_fileIndex == 0)
|
||||
return;
|
||||
|
||||
--m_fileIndex;
|
||||
displayCurrentFile();
|
||||
}
|
||||
|
||||
void JsonEditor::updatePreview(QString const& valueStr) {
|
||||
try {
|
||||
FormattedJson newValue = m_editFormat->toJson(valueStr.toStdString());
|
||||
FormattedJson preview = addOrSet(false, m_path, m_currentJson, m_options.insertLocation, newValue);
|
||||
m_jsonDocument->setPlainText(preview.repr().utf8Ptr());
|
||||
|
||||
} catch (JsonException const&) {
|
||||
} catch (JsonParsingException const&) {
|
||||
// Don't update the preview if it's not valid Json.
|
||||
}
|
||||
}
|
||||
|
||||
bool JsonEditor::saveChanges() {
|
||||
try {
|
||||
FormattedJson newValue = m_editFormat->toJson(m_valueEditor->text().toStdString());
|
||||
m_currentJson = addOrSet(false, m_path, m_currentJson, m_options.insertLocation, newValue);
|
||||
String repr = reprWithLineEnding(m_currentJson);
|
||||
File::writeFile(repr, m_files.get(m_fileIndex));
|
||||
return true;
|
||||
|
||||
} catch (StarException const& e) {
|
||||
m_errorDialog->showMessage(e.what());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void JsonEditor::displayCurrentFile() {
|
||||
String file = m_files.get(m_fileIndex);
|
||||
|
||||
size_t progress = (m_fileIndex + 1) * 100 / m_files.size();
|
||||
String status = strf("Editing file %s/%s (%s%%): %s", m_fileIndex + 1, m_files.size(), progress, file);
|
||||
m_statusLabel->setText(status.utf8Ptr());
|
||||
|
||||
m_backButton->setEnabled(m_fileIndex != 0);
|
||||
m_nextButton->setText(m_fileIndex == m_files.size() - 1 ? "Done" : "Next »");
|
||||
|
||||
m_pathLabel->setText(m_path->path().utf8Ptr());
|
||||
|
||||
m_imageLabel->setText("No preview");
|
||||
m_jsonDocument->setPlainText("");
|
||||
m_valueEditor->setText("");
|
||||
m_valueEditor->setEnabled(false);
|
||||
|
||||
try {
|
||||
m_currentJson = FormattedJson::parse(File::readFileString(file));
|
||||
|
||||
m_jsonDocument->setPlainText(m_currentJson.repr().utf8Ptr());
|
||||
|
||||
updateValueEditor();
|
||||
|
||||
updateImagePreview();
|
||||
|
||||
} catch (StarException const& e) {
|
||||
// Something else went wrong (maybe while parsing the document) and allowing
|
||||
// the user to edit this file might cause us to lose data.
|
||||
m_errorDialog->showMessage(e.what());
|
||||
}
|
||||
|
||||
m_jsonPreview->moveCursor(QTextCursor::Start);
|
||||
|
||||
m_valueEditor->selectAll();
|
||||
m_valueEditor->setFocus(Qt::FocusReason::OtherFocusReason);
|
||||
}
|
||||
|
||||
void JsonEditor::updateValueEditor() {
|
||||
FormattedJson value;
|
||||
try {
|
||||
value = m_path->get(m_currentJson);
|
||||
} catch (JsonPath::TraversalException const&) {
|
||||
// Path does not already exist in the Json document. We're adding it.
|
||||
value = m_editFormat->getDefault();
|
||||
}
|
||||
|
||||
String valueText;
|
||||
try {
|
||||
valueText = m_editFormat->fromJson(value);
|
||||
} catch (JsonException const& e) {
|
||||
// The value already present in the was no thte type we expected, e.g.
|
||||
// it was an int, when we wanted a string array for CSV.
|
||||
// Clear the value already present.
|
||||
m_errorDialog->showMessage(e.what());
|
||||
valueText = m_editFormat->fromJson(m_editFormat->getDefault());
|
||||
}
|
||||
m_valueEditor->setText(valueText.utf8Ptr());
|
||||
m_valueEditor->setEnabled(true);
|
||||
}
|
||||
|
||||
void JsonEditor::updateImagePreview() {
|
||||
String file = m_files.get(m_fileIndex);
|
||||
for (JsonPath::PathPtr const& imagePath : m_options.editorImages) {
|
||||
try {
|
||||
String image = imagePath->get(m_currentJson).toJson().toString();
|
||||
image = File::relativeTo(File::dirName(file), image.extract(":"));
|
||||
|
||||
QPixmap pixmap = QPixmap(image.utf8Ptr()).scaledToWidth(ImagePreviewWidth);
|
||||
if (pixmap.height() > ImagePreviewHeight)
|
||||
pixmap = pixmap.scaledToHeight(ImagePreviewHeight);
|
||||
m_imageLabel->setPixmap(pixmap);
|
||||
break;
|
||||
} catch (JsonPath::TraversalException const&) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int Star::edit(int argc, char* argv[], JsonPath::PathPtr const& path, Options const& options, List<Input> const& inputs) {
|
||||
QApplication app(argc, argv);
|
||||
StringList files;
|
||||
for (Input const& input : inputs) {
|
||||
if (input.is<FindInput>())
|
||||
files.appendAll(findFiles(input.get<FindInput>()));
|
||||
else
|
||||
files.append(input.get<FileInput>().filename);
|
||||
}
|
||||
JsonEditor e(path, options, files);
|
||||
e.show();
|
||||
return app.exec();
|
||||
}
|
56
source/json_tool/editor_gui.hpp
Normal file
56
source/json_tool/editor_gui.hpp
Normal file
|
@ -0,0 +1,56 @@
|
|||
#ifndef EDITOR_GUI_HPP
|
||||
#define EDITOR_GUI_HPP
|
||||
|
||||
#include <QErrorMessage>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QMainWindow>
|
||||
#include <QScrollBar>
|
||||
#include <QTextEdit>
|
||||
|
||||
#include "json_tool.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
class JsonEditor : public QMainWindow {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit JsonEditor(JsonPath::PathPtr const& path, Options const& options, List<String> const& files);
|
||||
|
||||
private slots:
|
||||
void next();
|
||||
void back();
|
||||
void updatePreview(QString const& valueStr);
|
||||
|
||||
private:
|
||||
// Returns false if the change can't be made or the edit is invalid Json
|
||||
bool saveChanges();
|
||||
|
||||
void displayCurrentFile();
|
||||
void updateValueEditor();
|
||||
void updateImagePreview();
|
||||
|
||||
QLabel* m_statusLabel;
|
||||
QLabel* m_pathLabel;
|
||||
QLabel* m_imageLabel;
|
||||
QTextEdit* m_jsonPreview;
|
||||
QTextDocument* m_jsonDocument;
|
||||
QLineEdit* m_valueEditor;
|
||||
QErrorMessage* m_errorDialog;
|
||||
QPushButton* m_backButton;
|
||||
QPushButton* m_nextButton;
|
||||
|
||||
JsonPath::PathPtr m_path;
|
||||
JsonInputFormatPtr m_editFormat;
|
||||
Options m_options;
|
||||
List<String> m_files;
|
||||
size_t m_fileIndex;
|
||||
FormattedJson m_currentJson;
|
||||
};
|
||||
|
||||
int edit(int argc, char* argv[], JsonPath::PathPtr const& path, Options const& options, List<Input> const& inputs);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
471
source/json_tool/json_tool.cpp
Normal file
471
source/json_tool/json_tool.cpp
Normal file
|
@ -0,0 +1,471 @@
|
|||
#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<Json>());
|
||||
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("%s\r\n", repr);
|
||||
return strf("%s\n", repr);
|
||||
}
|
||||
|
||||
void OutputOnSeparateLines::out(FormattedJson const& json) {
|
||||
coutf("%s", 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("%s", reprWithLineEnding(array));
|
||||
}
|
||||
|
||||
FormattedJson Star::addOrSet(bool add,
|
||||
JsonPath::PathPtr path,
|
||||
FormattedJson const& input,
|
||||
InsertLocation insertLocation,
|
||||
FormattedJson const& value) {
|
||||
JsonPath::EmptyPathOp<FormattedJson> 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<FormattedJson> objectOp = [&value, &insertLocation](
|
||||
FormattedJson const& object, String const& key) {
|
||||
if (insertLocation.is<AtBeginning>())
|
||||
return object.prepend(key, value);
|
||||
if (insertLocation.is<AtEnd>())
|
||||
return object.append(key, value);
|
||||
if (insertLocation.is<BeforeKey>())
|
||||
return object.insertBefore(key, value, insertLocation.get<BeforeKey>().key);
|
||||
if (insertLocation.is<AfterKey>())
|
||||
return object.insertAfter(key, value, insertLocation.get<AfterKey>().key);
|
||||
return object.set(key, value);
|
||||
};
|
||||
JsonPath::ArrayOp<FormattedJson> arrayOp = [&value, add](FormattedJson const& array, Maybe<size_t> 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<void(String)> func) {
|
||||
for (pair<String, bool> 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<void(FormattedJson const&)> 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 %s, must be either Array or Object", parent.typeName());
|
||||
}
|
||||
}
|
||||
|
||||
bool process(function<void(FormattedJson const&)> output,
|
||||
Command const& command,
|
||||
Options const& options,
|
||||
FormattedJson const& input) {
|
||||
if (command.is<GetCommand>()) {
|
||||
GetCommand const& getCmd = command.get<GetCommand>();
|
||||
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>()) {
|
||||
SetCommand const& setCmd = command.get<SetCommand>();
|
||||
output(addOrSet(false, setCmd.path, input, options.insertLocation, setCmd.value));
|
||||
|
||||
} else if (command.is<AddCommand>()) {
|
||||
AddCommand const& addCmd = command.get<AddCommand>();
|
||||
output(addOrSet(true, addCmd.path, input, options.insertLocation, addCmd.value));
|
||||
|
||||
} else if (command.is<RemoveCommand>()) {
|
||||
output(command.get<RemoveCommand>().path->remove(input));
|
||||
|
||||
} else {
|
||||
starAssert(command.empty());
|
||||
output(input);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool process(
|
||||
function<void(FormattedJson const&)> output, Command const& command, Options const& options, String const& input) {
|
||||
FormattedJson inJson = FormattedJson::parse(input);
|
||||
return process(output, command, options, inJson);
|
||||
}
|
||||
|
||||
bool process(
|
||||
function<void(FormattedJson const&)> output, Command const& command, Options const& options, Input const& input) {
|
||||
if (input.is<JsonLiteralInput>()) {
|
||||
return process(output, command, options, input.get<JsonLiteralInput>().json);
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
StringList files;
|
||||
|
||||
if (input.is<FileInput>()) {
|
||||
files = StringList{input.get<FileInput>().filename};
|
||||
} else {
|
||||
files = findFiles(input.get<FindInput>());
|
||||
}
|
||||
|
||||
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<JsonPath::Pointer>(path);
|
||||
return make_shared<JsonPath::QueryPath>(path);
|
||||
}
|
||||
|
||||
pair<JsonPath::PathPtr, bool> 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<ParsedArgs> parseArgs(int argc, char** argv) {
|
||||
// Skip the program name
|
||||
Deque<String> args = Deque<String>(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<GenericInputFormat>();
|
||||
else if (format == "css" || format == "csv")
|
||||
parsed.options.editFormat = make_shared<CommaSeparatedStrings>();
|
||||
else if (format == "string")
|
||||
parsed.options.editFormat = make_shared<StringInputFormat>();
|
||||
else
|
||||
return {};
|
||||
|
||||
} else if (arg == "--array") {
|
||||
// Output multiple results as a single array
|
||||
parsed.options.output = make_shared<ArrayOutput>(false);
|
||||
|
||||
} else if (arg == "--array-unique") {
|
||||
// Output multiple results as a single array, with duplicate results
|
||||
// removed
|
||||
parsed.options.output = make_shared<ArrayOutput>(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 %s doesn't exist\n", arg);
|
||||
return {};
|
||||
}
|
||||
parsed.inputs.append(FileInput{arg});
|
||||
}
|
||||
}
|
||||
|
||||
if (!parsed.options.output)
|
||||
parsed.options.output = make_shared<OutputOnSeparateLines>();
|
||||
|
||||
bool anyFileInputs = false, anyNonFileInputs = false;
|
||||
for (Input const& input : parsed.inputs) {
|
||||
if (input.is<FindInput>() || input.is<FileInput>()) {
|
||||
anyFileInputs = true;
|
||||
} else {
|
||||
anyNonFileInputs = true;
|
||||
}
|
||||
}
|
||||
if (parsed.command.is<EditCommand>() && !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<EditCommand>()) {
|
||||
cerrf("Interactive edit (--edit) is always in-place. Explicitly specifying -i is not needed.\n");
|
||||
return {};
|
||||
}
|
||||
if (parsed.command.is<EditCommand>() && anyNonFileInputs) {
|
||||
cerrf("Interactie edit (--edit) can only be used with file input sources.\n");
|
||||
return {};
|
||||
}
|
||||
|
||||
if (parsed.options.editFormat && !parsed.command.is<EditCommand>()) {
|
||||
cerrf("--input can only be used with --edit.\n");
|
||||
return {};
|
||||
} else if (!parsed.options.editFormat && parsed.command.is<EditCommand>()) {
|
||||
parsed.options.editFormat = make_shared<GenericInputFormat>();
|
||||
}
|
||||
|
||||
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> parsedArgs = parseArgs(argc, argv);
|
||||
|
||||
if (!parsedArgs) {
|
||||
cerrf("Usage: %s [--get <json-path>] (-j <json> | <json-file>*)\n", argv[0]);
|
||||
cerrf(
|
||||
"Usage: %s --set <json-path> <json> [-i] [(--at (beginning|end) | --before <key> | --after <key>)] (-j "
|
||||
"<json> | <json-file>*)\n",
|
||||
argv[0]);
|
||||
cerrf(
|
||||
"Usage: %s --add <json-path> <json> [-i] [(--at (beginning|end) | --before <key> | --after <key>)] (-j "
|
||||
"<json> | <json-file>*)\n",
|
||||
argv[0]);
|
||||
cerrf(
|
||||
"Usage: %s --edit <json-path> [(--at (beginning|end) | --before <key> | --after <key>)] [--input "
|
||||
"(csv|json|string)] <json-file>+\n",
|
||||
argv[0]);
|
||||
cerrf("\n");
|
||||
cerrf("Example: %s --get /dialog/0/message guard.npctype\n", argv[0]);
|
||||
cerrf("Example: %s --get 'foo[0]' -j '{\"foo\":[0,1,2,3]}'\n", argv[0]);
|
||||
cerrf("Example: %s --edit /tags --input csv --find ../assets/ .object\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
OutputPtr output = parsedArgs->options.output;
|
||||
bool success = true;
|
||||
|
||||
if (parsedArgs->command.is<EditCommand>()) {
|
||||
return edit(argc, argv, parsedArgs->command.get<EditCommand>().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("%s\n", e.what());
|
||||
return 1;
|
||||
|
||||
} catch (JsonException const& e) {
|
||||
cerrf("%s\n", e.what());
|
||||
return 1;
|
||||
|
||||
} catch (std::exception const& e) {
|
||||
cerrf("Exception caught: %s\n", outputException(e, true));
|
||||
return 1;
|
||||
}
|
||||
}
|
150
source/json_tool/json_tool.hpp
Normal file
150
source/json_tool/json_tool.hpp
Normal file
|
@ -0,0 +1,150 @@
|
|||
#ifndef JSON_TOOL_HPP
|
||||
#define JSON_TOOL_HPP
|
||||
|
||||
#include "StarFormattedJson.hpp"
|
||||
#include "StarJsonPath.hpp"
|
||||
|
||||
namespace Star {
|
||||
|
||||
struct GetCommand {
|
||||
JsonPath::PathPtr path;
|
||||
bool opt;
|
||||
bool children;
|
||||
};
|
||||
|
||||
struct SetCommand {
|
||||
JsonPath::PathPtr path;
|
||||
FormattedJson value;
|
||||
};
|
||||
|
||||
struct AddCommand {
|
||||
JsonPath::PathPtr path;
|
||||
FormattedJson value;
|
||||
};
|
||||
|
||||
struct RemoveCommand {
|
||||
JsonPath::PathPtr path;
|
||||
};
|
||||
|
||||
struct EditCommand {
|
||||
JsonPath::PathPtr path;
|
||||
};
|
||||
|
||||
typedef MVariant<GetCommand, SetCommand, AddCommand, RemoveCommand, EditCommand> Command;
|
||||
|
||||
struct AtBeginning {};
|
||||
|
||||
struct AtEnd {};
|
||||
|
||||
struct BeforeKey {
|
||||
String key;
|
||||
};
|
||||
|
||||
struct AfterKey {
|
||||
String key;
|
||||
};
|
||||
|
||||
typedef MVariant<AtBeginning, AtEnd, BeforeKey, AfterKey> InsertLocation;
|
||||
|
||||
STAR_CLASS(JsonInputFormat);
|
||||
|
||||
class JsonInputFormat {
|
||||
public:
|
||||
virtual ~JsonInputFormat() {}
|
||||
virtual FormattedJson toJson(String const& input) const = 0;
|
||||
virtual String fromJson(FormattedJson const& json) const = 0;
|
||||
virtual FormattedJson getDefault() const = 0;
|
||||
};
|
||||
|
||||
class GenericInputFormat : public JsonInputFormat {
|
||||
public:
|
||||
virtual FormattedJson toJson(String const& input) const override;
|
||||
virtual String fromJson(FormattedJson const& json) const override;
|
||||
virtual FormattedJson getDefault() const override;
|
||||
};
|
||||
|
||||
class CommaSeparatedStrings : public JsonInputFormat {
|
||||
public:
|
||||
virtual FormattedJson toJson(String const& input) const override;
|
||||
virtual String fromJson(FormattedJson const& json) const override;
|
||||
virtual FormattedJson getDefault() const override;
|
||||
};
|
||||
|
||||
class StringInputFormat : public JsonInputFormat {
|
||||
public:
|
||||
virtual FormattedJson toJson(String const& input) const override;
|
||||
virtual String fromJson(FormattedJson const& json) const override;
|
||||
virtual FormattedJson getDefault() const override;
|
||||
};
|
||||
|
||||
STAR_CLASS(Output);
|
||||
|
||||
class Output {
|
||||
public:
|
||||
virtual ~Output() {}
|
||||
virtual void out(FormattedJson const& json) = 0;
|
||||
virtual void flush() = 0;
|
||||
|
||||
function<void(FormattedJson const& json)> toFunction() {
|
||||
return [this](FormattedJson const& json) { this->out(json); };
|
||||
}
|
||||
};
|
||||
|
||||
class OutputOnSeparateLines : public Output {
|
||||
public:
|
||||
virtual void out(FormattedJson const& json) override;
|
||||
virtual void flush() override;
|
||||
};
|
||||
|
||||
class ArrayOutput : public Output {
|
||||
public:
|
||||
ArrayOutput(bool unique) : m_unique(unique), m_results() {}
|
||||
|
||||
virtual void out(FormattedJson const& json) override;
|
||||
virtual void flush() override;
|
||||
|
||||
private:
|
||||
bool m_unique;
|
||||
List<FormattedJson> m_results;
|
||||
};
|
||||
|
||||
struct Options {
|
||||
Options() : inPlace(false), insertLocation(), editFormat(nullptr), editorImages(), output() {}
|
||||
|
||||
bool inPlace;
|
||||
InsertLocation insertLocation;
|
||||
JsonInputFormatPtr editFormat;
|
||||
List<JsonPath::PathPtr> editorImages;
|
||||
OutputPtr output;
|
||||
};
|
||||
|
||||
struct JsonLiteralInput {
|
||||
String json;
|
||||
};
|
||||
|
||||
struct FileInput {
|
||||
String filename;
|
||||
};
|
||||
|
||||
struct FindInput {
|
||||
String directory;
|
||||
String filenameSuffix;
|
||||
};
|
||||
|
||||
typedef MVariant<JsonLiteralInput, FileInput, FindInput> Input;
|
||||
|
||||
struct ParsedArgs {
|
||||
ParsedArgs() : inputs(), command(), options() {}
|
||||
|
||||
List<Input> inputs;
|
||||
Command command;
|
||||
Options options;
|
||||
};
|
||||
|
||||
FormattedJson addOrSet(bool add, JsonPath::PathPtr path, FormattedJson const& input, InsertLocation insertLocation, FormattedJson const& value);
|
||||
String reprWithLineEnding(FormattedJson const& json);
|
||||
StringList findFiles(FindInput const& findArgs);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
Loading…
Add table
Add a link
Reference in a new issue