mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-16 13:32:44 +00:00
CLI: let --export-settings - write the merged config to stdout
--export-settings already writes the merged config as JSON at the right point in the CLI flow. Passing - writes the same document to stdout. - ConfigBase::save_to_json gains a stream overload. The file overload serializes through it before opening the file, so the format is unchanged and a config that cannot be serialized leaves an existing file untouched instead of truncating it. - On stdout, invalid UTF-8 in string values is written as U+FFFD instead of ending the process with an uncaught type_error; files keep the strict behaviour. - - is rejected up front when combined with an action or transform that can write to stdout or does real work, so stdout carries only the JSON. - The unconditional "skip locked instance" stdout write during arrange now goes to the log. - Tests in tests/libslic3r/test_config.cpp.
This commit is contained in:
+25
-2
@@ -1387,6 +1387,25 @@ int CLI::run(int argc, char **argv)
|
|||||||
if (downward_check_option)
|
if (downward_check_option)
|
||||||
downward_check = downward_check_option->value;
|
downward_check = downward_check_option->value;
|
||||||
|
|
||||||
|
// --export-settings - writes its JSON to stdout, so reject every action or transform that may write there
|
||||||
|
// too (--info, --help, --orient, slicing and exporting). The allowed ones do nothing when nothing is
|
||||||
|
// sliced or exported.
|
||||||
|
if (std::find(m_actions.begin(), m_actions.end(), "export_settings") != m_actions.end() && m_config.opt_string("export_settings") == "-") {
|
||||||
|
static const std::set<std::string> stdout_compatible = { "export_settings", "uptodate", "load_defaultfila", "min_save",
|
||||||
|
"mtcpp", "mstpp", "no_check", "normative_check", "pipe" };
|
||||||
|
for (const std::vector<std::string> *opt_keys : { &m_actions, &m_transforms }) {
|
||||||
|
for (const std::string &opt_key : *opt_keys) {
|
||||||
|
if (stdout_compatible.count(opt_key) == 0) {
|
||||||
|
std::string flag = opt_key;
|
||||||
|
std::replace(flag.begin(), flag.end(), '_', '-');
|
||||||
|
boost::nowide::cerr << "--export-settings - cannot be combined with --" << flag << std::endl;
|
||||||
|
record_exit_reson(outfile_dir, CLI_INVALID_PARAMS, 0, cli_errors[CLI_INVALID_PARAMS], sliced_info);
|
||||||
|
flush_and_exit(CLI_INVALID_PARAMS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bool start_gui = m_actions.empty() && !downward_check;
|
bool start_gui = m_actions.empty() && !downward_check;
|
||||||
if (start_gui) {
|
if (start_gui) {
|
||||||
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
|
BOOST_LOG_TRIVIAL(info) << "no action, start gui directly" << std::endl;
|
||||||
@@ -5348,7 +5367,7 @@ int CLI::run(int argc, char **argv)
|
|||||||
//skip this object due to be locked in plate
|
//skip this object due to be locked in plate
|
||||||
ap.itemid = locked_aps.size();
|
ap.itemid = locked_aps.size();
|
||||||
locked_aps.emplace_back(ap);
|
locked_aps.emplace_back(ap);
|
||||||
boost::nowide::cout <<__FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": skip locked instance, obj_id %1%, instance_id %2%") % oidx % inst_idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5937,7 +5956,11 @@ int CLI::run(int argc, char **argv)
|
|||||||
//FIXME check for mixing the FFF / SLA parameters.
|
//FIXME check for mixing the FFF / SLA parameters.
|
||||||
// or better save fff_print_config vs. sla_print_config
|
// or better save fff_print_config vs. sla_print_config
|
||||||
//m_print_config.save(m_config.opt_string("save"));
|
//m_print_config.save(m_config.opt_string("save"));
|
||||||
m_print_config.save_to_json(m_config.opt_string(opt_key), std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
const std::string &settings_file = m_config.opt_string(opt_key);
|
||||||
|
if (settings_file == "-")
|
||||||
|
m_print_config.save_to_json(boost::nowide::cout, "project_settings", "project", SoftFever_VERSION, /*replace_invalid_utf8=*/true);
|
||||||
|
else
|
||||||
|
m_print_config.save_to_json(settings_file, std::string("project_settings"), std::string("project"), std::string(SoftFever_VERSION));
|
||||||
} else if (opt_key == "info") {
|
} else if (opt_key == "info") {
|
||||||
// --info works on unrepaired model
|
// --info works on unrepaired model
|
||||||
for (Model &model : m_models) {
|
for (Model &model : m_models) {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
#include <sstream>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
@@ -1515,6 +1516,19 @@ std::optional<PluginCapabilityRef> parse_capability_ref(const std::string& value
|
|||||||
|
|
||||||
//BBS: add json support
|
//BBS: add json support
|
||||||
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
|
void ConfigBase::save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const
|
||||||
|
{
|
||||||
|
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
|
||||||
|
std::ostringstream ss;
|
||||||
|
this->save_to_json(ss, name, from, version);
|
||||||
|
boost::nowide::ofstream c;
|
||||||
|
c.open(file, std::ios::out | std::ios::trunc);
|
||||||
|
c << ss.str();
|
||||||
|
c.close();
|
||||||
|
|
||||||
|
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
|
||||||
{
|
{
|
||||||
json j;
|
json j;
|
||||||
//record the headers
|
//record the headers
|
||||||
@@ -1561,12 +1575,7 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
|
|||||||
j["plugins"] = unique_refs;
|
j["plugins"] = unique_refs;
|
||||||
}
|
}
|
||||||
|
|
||||||
boost::nowide::ofstream c;
|
os << j.dump(1, '\t', false, replace_invalid_utf8 ? json::error_handler_t::replace : json::error_handler_t::strict) << std::endl;
|
||||||
c.open(file, std::ios::out | std::ios::trunc);
|
|
||||||
c << j.dump(1, '\t') << std::endl;
|
|
||||||
c.close();
|
|
||||||
|
|
||||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ConfigBase::save(const std::string &file) const
|
void ConfigBase::save(const std::string &file) const
|
||||||
|
|||||||
@@ -2825,6 +2825,9 @@ public:
|
|||||||
|
|
||||||
//BBS: add json support
|
//BBS: add json support
|
||||||
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
|
void save_to_json(const std::string &file, const std::string &name, const std::string &from, const std::string &version) const;
|
||||||
|
// Same document, written to a stream. Invalid UTF-8 in a string value throws nlohmann's type_error unless
|
||||||
|
// replace_invalid_utf8 is set, which writes U+FFFD instead (for callers such as stdout with no handler).
|
||||||
|
void save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8 = false) const;
|
||||||
|
|
||||||
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
|
// Rebuild the in-memory "plugins" manifest (the "name;uuid;capability" references the plugin
|
||||||
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
|
// dispatchers consume) from the plugin-backed options via the registered resolver. save_to_json()
|
||||||
|
|||||||
@@ -11916,7 +11916,7 @@ CLIActionsConfigDef::CLIActionsConfigDef()
|
|||||||
|
|
||||||
def = this->add("export_settings", coString);
|
def = this->add("export_settings", coString);
|
||||||
def->label = L("Export Settings");
|
def->label = L("Export Settings");
|
||||||
def->tooltip = L("This exports settings to a file.");
|
def->tooltip = L("This exports settings to a file. Use - to write them to stdout.");
|
||||||
def->cli_params = "settings.json";
|
def->cli_params = "settings.json";
|
||||||
def->set_default_value(new ConfigOptionString("output.json"));
|
def->set_default_value(new ConfigOptionString("output.json"));
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
#include <boost/nowide/fstream.hpp>
|
#include <boost/nowide/fstream.hpp>
|
||||||
#include <nlohmann/json.hpp>
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
|
|
||||||
SCENARIO("Generic config validation performs as expected.", "[Config]") {
|
SCENARIO("Generic config validation performs as expected.", "[Config]") {
|
||||||
@@ -488,6 +490,59 @@ TEST_CASE("save_to_json round-trips plugin capability references as strings", "[
|
|||||||
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
|
CHECK(reloaded.option<ConfigOptionStrings>("slicing_pipeline_plugin")->values == refs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE("save_to_json writes the same document to a stream as to a file", "[Config]") {
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
config.set_key_value("layer_height", new ConfigOptionFloat(0.2));
|
||||||
|
config.set_key_value("wall_loops", new ConfigOptionInt(3));
|
||||||
|
config.set_key_value("filament_type", new ConfigOptionStrings({ "PLA", "PETG" }));
|
||||||
|
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28\nG1 Z5"));
|
||||||
|
|
||||||
|
ScopedTemporaryFile tmp(".json");
|
||||||
|
config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0");
|
||||||
|
std::string file_contents;
|
||||||
|
{
|
||||||
|
boost::nowide::ifstream ifs(tmp.string());
|
||||||
|
file_contents.assign(std::istreambuf_iterator<char>(ifs), std::istreambuf_iterator<char>());
|
||||||
|
}
|
||||||
|
// The file format: one tab per nesting level and a trailing newline.
|
||||||
|
REQUIRE_FALSE(file_contents.empty());
|
||||||
|
CHECK(file_contents.rfind("{\n\t\"", 0) == 0);
|
||||||
|
CHECK(file_contents.back() == '\n');
|
||||||
|
|
||||||
|
std::ostringstream strict, replaced;
|
||||||
|
config.save_to_json(strict, "test_preset", "User", "1.0.0.0");
|
||||||
|
config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true);
|
||||||
|
CHECK(strict.str() == file_contents);
|
||||||
|
CHECK(replaced.str() == file_contents);
|
||||||
|
CHECK(nlohmann::json::parse(strict.str())["machine_start_gcode"] == "G28\nG1 Z5");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("save_to_json replaces invalid UTF-8 in a stream only when asked", "[Config]") {
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||||
|
|
||||||
|
std::ostringstream strict, replaced;
|
||||||
|
CHECK_THROWS_AS(config.save_to_json(strict, "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||||
|
REQUIRE_NOTHROW(config.save_to_json(replaced, "test_preset", "User", "1.0.0.0", true));
|
||||||
|
CHECK(nlohmann::json::parse(replaced.str())["machine_start_gcode"] == "G28 ; \xEF\xBF\xBD");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("save_to_json leaves an existing file untouched when the config cannot be serialized", "[Config]") {
|
||||||
|
DynamicPrintConfig config;
|
||||||
|
config.set_key_value("machine_start_gcode", new ConfigOptionString("G28 ; \xff"));
|
||||||
|
|
||||||
|
ScopedTemporaryFile tmp(".json");
|
||||||
|
{
|
||||||
|
boost::nowide::ofstream ofs(tmp.string());
|
||||||
|
ofs << "previous";
|
||||||
|
}
|
||||||
|
CHECK_THROWS_AS(config.save_to_json(tmp.string(), "test_preset", "User", "1.0.0.0"), nlohmann::json::type_error);
|
||||||
|
|
||||||
|
boost::nowide::ifstream ifs(tmp.string());
|
||||||
|
const std::string contents((std::istreambuf_iterator<char>(ifs)), std::istreambuf_iterator<char>());
|
||||||
|
CHECK(contents == "previous");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
|
TEST_CASE("plugin capability references survive string-map serialization", "[Config][plugins]") {
|
||||||
const std::vector<std::string> refs = {
|
const std::vector<std::string> refs = {
|
||||||
"master_plugin;;header-stamp",
|
"master_plugin;;header-stamp",
|
||||||
|
|||||||
Reference in New Issue
Block a user