From df2787d3e9aac3e3921524f818ca4a9ba9c8f983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Astori?= Date: Sat, 9 Dec 2017 15:06:41 -0500 Subject: [PATCH] Add a `--config` / `-c` option to the `start` CLI command to arbitrarily override any configuration key The biggest caveat is that JS code (such as functions) will not be interpreted as such, on purpose, for security precautions. If such thing is needed, then a configuration file must be used. --- src/command-line/index.js | 9 +++ src/command-line/utils.js | 50 ++++++++++++++ test/src/command-line/utilsTest.js | 104 +++++++++++++++++++++++++++++ 3 files changed, 163 insertions(+) diff --git a/src/command-line/index.js b/src/command-line/index.js index 2fe21d1a..0ac9e2eb 100644 --- a/src/command-line/index.js +++ b/src/command-line/index.js @@ -2,6 +2,7 @@ global.log = require("../log.js"); +const _ = require("lodash"); const fs = require("fs"); const path = require("path"); const program = require("commander"); @@ -16,6 +17,11 @@ if (require("semver").lt(process.version, "6.0.0")) { program.version(Helper.getVersion(), "-v, --version") .option("--home ", `${colors.bold("[DEPRECATED]")} Use the ${colors.green("THELOUNGE_HOME")} environment variable instead.`) + .option( + "-c, --config ", + "override entries of the configuration file, must be specified for each entry that needs to be overriden", + Utils.parseConfigOptions + ) .on("--help", Utils.extraHelp) .parseOptions(process.argv); @@ -49,6 +55,9 @@ if (!home) { Helper.setHome(home); +// Merge config key-values passed as CLI options into the main config +_.merge(Helper.config, program.config); + require("./start"); require("./config"); if (!Helper.config.public && !Helper.config.ldap.enable) { diff --git a/src/command-line/utils.js b/src/command-line/utils.js index 0a54cad7..0e5e29ac 100644 --- a/src/command-line/utils.js +++ b/src/command-line/utils.js @@ -1,5 +1,6 @@ "use strict"; +const _ = require("lodash"); const colors = require("colors/safe"); const fs = require("fs"); const Helper = require("../helper"); @@ -51,6 +52,55 @@ class Utils { return home; } + + // Parses CLI options such as `-c public=true`, `-c debug.raw=true`, etc. + static parseConfigOptions(val, memo) { + // Invalid option that is not of format `key=value`, do nothing + if (!val.includes("=")) { + return memo; + } + + const parseValue = (value) => { + if (value === "true") { + return true; + } else if (value === "false") { + return false; + } else if (value === "undefined") { + return undefined; + } else if (value === "null") { + return null; + } else if (/^\[.*\]$/.test(value)) { // Arrays + // Supporting arrays `[a,b]` and `[a, b]` + const array = value.slice(1, -1).split(/,\s*/); + // If [] is given, it will be parsed as `[ "" ]`, so treat this as empty + if (array.length === 1 && array[0] === "") { + return []; + } + return array.map(parseValue); // Re-parses all values of the array + } + return value; + }; + + // First time the option is parsed, memo is not set + if (memo === undefined) { + memo = {}; + } + + // Note: If passed `-c foo="bar=42"` (with single or double quotes), `val` + // will always be passed as `foo=bar=42`, never with quotes. + const position = val.indexOf("="); // Only split on the first = found + const key = val.slice(0, position); + const value = val.slice(position + 1); + const parsedValue = parseValue(value); + + if (_.has(memo, key)) { + log.warn(`Configuration key ${colors.bold(key)} was already specified, ignoring...`); + } else { + memo = _.set(memo, key, parsedValue); + } + + return memo; + } } module.exports = Utils; diff --git a/test/src/command-line/utilsTest.js b/test/src/command-line/utilsTest.js index dad5c9a8..ff36856f 100644 --- a/test/src/command-line/utilsTest.js +++ b/test/src/command-line/utilsTest.js @@ -43,4 +43,108 @@ describe("Utils", function() { expect(stdout).to.include("THELOUNGE_HOME"); }); }); + + describe(".parseConfigOptions", function() { + describe("when it's the first option given", function() { + it("should return nothing when passed an invalid config", function() { + expect(Utils.parseConfigOptions("foo")).to.be.undefined; + }); + + it("should correctly parse boolean values", function() { + expect(Utils.parseConfigOptions("foo=true")).to.deep.equal({foo: true}); + expect(Utils.parseConfigOptions("foo=false")).to.deep.equal({foo: false}); + }); + + it("should correctly parse empty strings", function() { + expect(Utils.parseConfigOptions("foo=")).to.deep.equal({foo: ""}); + }); + + it("should correctly parse null values", function() { + expect(Utils.parseConfigOptions("foo=null")).to.deep.equal({foo: null}); + }); + + it("should correctly parse undefined values", function() { + expect(Utils.parseConfigOptions("foo=undefined")) + .to.deep.equal({foo: undefined}); + }); + + it("should correctly parse array values", function() { + expect(Utils.parseConfigOptions("foo=[bar,true]")) + .to.deep.equal({foo: ["bar", true]}); + + expect(Utils.parseConfigOptions("foo=[bar, true]")) + .to.deep.equal({foo: ["bar", true]}); + }); + + it("should correctly parse empty array values", function() { + expect(Utils.parseConfigOptions("foo=[]")) + .to.deep.equal({foo: []}); + }); + + it("should correctly parse values that contain `=` sign", function() { + expect(Utils.parseConfigOptions("foo=bar=42")) + .to.deep.equal({foo: "bar=42"}); + }); + + it("should correctly parse keys using dot-notation", function() { + expect(Utils.parseConfigOptions("foo.bar=value")) + .to.deep.equal({foo: {bar: "value"}}); + }); + + it("should correctly parse keys using array-notation", function() { + expect(Utils.parseConfigOptions("foo[0]=value")) + .to.deep.equal({foo: ["value"]}); + }); + }); + + describe("when some options have already been parsed", function() { + it("should not modify existing options when passed an invalid config", function() { + const memo = {foo: "bar"}; + expect(Utils.parseConfigOptions("foo", memo)).to.equal(memo); + }); + + it("should combine a new option with previously parsed ones", function() { + expect(Utils.parseConfigOptions("bar=false", {foo: true})) + .to.deep.equal({foo: true, bar: false}); + }); + + it("should maintain existing properties of a nested object", function() { + expect(Utils.parseConfigOptions("foo.bar=true", {foo: {baz: false}})) + .to.deep.equal({foo: {bar: true, baz: false}}); + }); + + it("should maintain existing entries of an array", function() { + expect(Utils.parseConfigOptions("foo[1]=baz", {foo: ["bar"]})) + .to.deep.equal({foo: ["bar", "baz"]}); + }); + + describe("when given the same key multiple times", function() { + let originalWarn; + + beforeEach(function() { + originalWarn = log.warn; + }); + + afterEach(function() { + log.warn = originalWarn; + }); + + it("should not override options", function() { + log.warn = () => {}; + + expect(Utils.parseConfigOptions("foo=baz", {foo: "bar"})) + .to.deep.equal({foo: "bar"}); + }); + + it("should display a warning", function() { + let warning = ""; + log.warn = TestUtil.mockLogger((str) => warning += str); + + Utils.parseConfigOptions("foo=bar", {foo: "baz"}); + + expect(warning).to.include("foo was already specified"); + }); + }); + }); + }); });