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"); + }); + }); + }); + }); });