diff --git a/packages/ctl/ctl.js b/packages/ctl/ctl.js index c58e3bda..0c6dd755 100755 --- a/packages/ctl/ctl.js +++ b/packages/ctl/ctl.js @@ -23,6 +23,7 @@ const libCommand = require("@clusterio/lib/command"); const libSharedCommands = require("@clusterio/lib/shared_commands"); const { ConsoleTransport, levels, logger } = require("@clusterio/lib/logging"); const libLoggingUtils = require("@clusterio/lib/logging_utils"); +const libHelpers = require("@clusterio/lib/helpers"); function print(...content) { @@ -134,13 +135,21 @@ instanceConfigCommands.add(new libCommand.Command({ })); instanceConfigCommands.add(new libCommand.Command({ - definition: ["set ", "Set field in instance config", (yargs) => { + definition: ["set [value]", "Set field in instance config", (yargs) => { yargs.positional("instance", { describe: "Instance to set config on", type: "string" }); yargs.positional("field", { describe: "Field to set", type: "string" }); yargs.positional("value", { describe: "Value to set", type: "string" }); + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + } else if (args.value === undefined) { + args.value = ""; + } await libLink.messages.setInstanceConfigField.send(control, { instance_id: instanceId, field: args.field, @@ -155,9 +164,15 @@ instanceConfigCommands.add(new libCommand.Command({ yargs.positional("field", { describe: "Field to set", type: "string" }); yargs.positional("prop", { describe: "Property to set", type: "string" }); yargs.positional("value", { describe: "JSON parsed value to set", type: "string" }); + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); }], handler: async function(args, control) { let instanceId = await libCommand.resolveInstance(control, args.instance); + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + } let request = { instance_id: instanceId, field: args.field, @@ -168,8 +183,8 @@ instanceConfigCommands.add(new libCommand.Command({ request.value = JSON.parse(args.value); } } catch (err) { - // If this is looks like an array, object or string literal - // throw the parse error, otherwise assume this is a string. + // If this is from stdin or looks like an array, object or string + // literal throw the parse error, otherwise assume this is a string. // The resoning behind this is that correctly quoting the string // with the all the layers of quote removal at play is difficult. // See the following table for how to pass "That's a \" quote" in @@ -181,7 +196,7 @@ instanceConfigCommands.add(new libCommand.Command({ // bash : '""That'\''s a \" quote""' // bash + npx : '""That'\''s a \" quote""' // bash + npx -s sh : "'\"\"That'\\''s a \\\" quote\"\"'" - if (/^(\[.*]|{.*}|".*")$/.test(value)) { + if (args.stdin || /^(\[.*]|{.*}|".*")$/.test(args.value)) { throw new libErrors.CommandError(`In parsing value '${args.value}': ${err.message}`); } request.value = args.value; diff --git a/packages/lib/helpers.js b/packages/lib/helpers.js index cf1a657c..cbbed42e 100644 --- a/packages/lib/helpers.js +++ b/packages/lib/helpers.js @@ -52,8 +52,32 @@ async function timeout(promise, time, timeoutResult) { } } + +/** + * Read stream to the end and return its content + * + * Reads the stream given asynchronously until the end is reached and + * returns all the data which was read from the stream. + * + * @param {Readable} stream - byte stream to read to the end. + * @returns {Buffer} content of the stream. + */ +async function readStream(stream) { + let chunks = []; + for await (let chunk of stream) { + // Support using ^Z to end input on Windows + if (process.platform === "win32" && stream.isTTY && chunk.toString() === "\x1a\r\n") { + break; + } + chunks.push(chunk); + } + return Buffer.concat(chunks); +} + + module.exports = { basicType, wait, timeout, + readStream, }; diff --git a/packages/lib/shared_commands.js b/packages/lib/shared_commands.js index 5b4f39a3..5decd578 100644 --- a/packages/lib/shared_commands.js +++ b/packages/lib/shared_commands.js @@ -8,6 +8,7 @@ const path = require("path"); const libConfig = require("@clusterio/lib/config"); const { logger } = require("@clusterio/lib/logging"); +const libHelpers = require("@clusterio/lib/helpers"); function print(...content) { @@ -100,7 +101,11 @@ async function handlePluginCommand(args, pluginList, pluginListPath) { */ function configCommand(yargs) { yargs - .command("set [value]", "Set config field") + .command("set [value]", "Set config field", yargs => { + yargs.options({ + "stdin": { describe: "read value from stdin", nargs: 0, type: "boolean" }, + }); + }) .command("show ", "Show value of the given config field") .command("list", "List all configuration fields and their values") .demandCommand(1, "You need to specify a command to run") @@ -141,7 +146,10 @@ async function handleConfigCommand(args, instance, configPath) { } } else if (command === "set") { - if (args.value === undefined) { + if (args.stdin) { + args.value = (await libHelpers.readStream(process.stdin)).toString().replace(/\r?\n$/, ""); + + } else if (args.value === undefined) { args.value = null; }