From 0753c9505ebc657d2cdc45cb38c69a1e031f5e32 Mon Sep 17 00:00:00 2001 From: Kris Maglione Date: Fri, 24 Sep 2010 15:24:21 -0400 Subject: [PATCH] Better list option parsing and serialization. --- common/content/commandline.js | 3 +- common/content/commands.js | 62 +++++++++++++++------- common/content/dactyl.js | 2 +- common/content/finder.js | 2 +- common/content/options.js | 93 ++++++++++++++++++++------------- common/locale/en-US/options.xml | 15 +++--- common/modules/base.jsm | 5 +- pentadactyl/NEWS | 5 ++ 8 files changed, 121 insertions(+), 66 deletions(-) diff --git a/common/content/commandline.js b/common/content/commandline.js index 599011c1..cfb78233 100644 --- a/common/content/commandline.js +++ b/common/content/commandline.js @@ -1071,7 +1071,8 @@ const CommandLine = Module("commandline", { */ select: function (backward, matchCurrent) { // always reset the tab completion if we use up/down keys - commandline._completions.reset(); + if (commandline._completions) + commandline._completions.reset(); let diff = backward ? -1 : 1; diff --git a/common/content/commands.js b/common/content/commands.js index 6262fd7f..6372bbe5 100644 --- a/common/content/commands.js +++ b/common/content/commands.js @@ -189,7 +189,15 @@ const Command = Class("Command", { * @returns {Args} * @see Commands#parseArgs */ - parseArgs: function (args, complete, extra) commands.parseArgs(args, this.options, this.argCount, false, this.literal, complete, extra), + parseArgs: function (args, complete, extra) commands.parseArgs(args, { + allowUnknownOptions: !!this.allowUnknownOptions, + argCount: this.argCount, + complete: complete, + extra: extra, + keepQuotes: !!this.keepQuotes, + literal: this.literal, + options: this.options + }), /** * @property {string[]} All of this command's name specs. e.g., "com[mand]" @@ -405,16 +413,15 @@ const Commands = Module("commands", { */ commandToString: function (args) { let res = [args.command + (args.bang ? "!" : "")]; - function quote(str) Commands.quoteArg[/[\s"'\\]|^$/.test(str) ? "'" : ""](str); for (let [opt, val] in Iterator(args.options || {})) { let chr = /^-.$/.test(opt) ? " " : "="; if (val != null) - opt += chr + quote(val); + opt += chr + Commands.quote(val); res.push(opt); } for (let [, arg] in Iterator(args.arguments || [])) - res.push(quote(arg)); + res.push(Commands.quote(arg)); let str = args.literalArg; if (str) @@ -556,7 +563,7 @@ const Commands = Module("commands", { */ parseArgs: function (str, options, argCount, allowUnknownOptions, literal, complete, extra) { function getNextArg(str) { - let [count, arg, quote] = Commands.parseArg(str); + let [count, arg, quote] = Commands.parseArg(str, null, keepQuotes); if (quote == "\\" && !complete) return [,,,"Trailing \\"]; if (quote && !complete) @@ -564,6 +571,11 @@ const Commands = Module("commands", { return [count, arg, quote]; } + let keepQuotes; + + if (isObject(options)) + ({ allowUnknownOptions, argCount, complete, extra, literal, options, keepQuotes }) = options; + if (!options) options = []; @@ -581,7 +593,7 @@ const Commands = Module("commands", { var invalid = false; // FIXME: best way to specify these requirements? - var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0 || false; // after a -- has been found + var onlyArgumentsRemaining = allowUnknownOptions || options.length == 0; // after a -- has been found var arg = null; var count = 0; // the length of the argument var i = 0; @@ -866,19 +878,26 @@ const Commands = Module("commands", { } }, { // returns [count, parsed_argument] - parseArg: function (str) { + parseArg: function (str, sep, keepQuotes) { let arg = ""; let quote = null; let len = str.length; - while (str.length && !/^\s/.test(str)) { + // Fix me. + if (isString(sep)) + sep = RegExp(sep); + sep = sep != null ? sep : /\s/; + let re1 = RegExp("^" + (sep.source === "" ? "(?!)" : sep.source)); + let re2 = RegExp(/^()((?:[^\\S"']|\\.)+)((?:\\$)?)/.source.replace("S", sep.source)); + + while (str.length && !re1.test(str)) { let res; - if ((res = str.match = str.match(/^()((?:[^\\\s"']|\\.)+)((?:\\$)?)/))) - arg += res[2].replace(/\\(.)/g, "$1"); - else if ((res = str.match(/^(")((?:[^\\"]|\\.)*)("?)/))) - arg += eval(res[0] + (res[3] ? "" : '"')); - else if ((res = str.match(/^(')((?:[^']|'')*)('?)/))) - arg += res[2].replace("''", "'", "g"); + if ((res = re2.exec(str))) + arg += keepQuotes ? res[0] : res[2].replace(/\\(.)/g, "$1"); + else if ((res = /^(")((?:[^\\"]|\\.)*)("?)/.exec(str))) + arg += keepQuotes ? res[0] : eval(res[0] + (res[3] ? "" : '"')); + else if ((res = /^(')((?:[^']|'')*)('?)/.exec(str))) + arg += keepQuotes ? res[0] : res[2].replace("''", "'", "g"); else break; @@ -890,7 +909,9 @@ const Commands = Module("commands", { } return [len - str.length, arg, quote]; - } + }, + + quote: function quote(str) Commands.quoteArg[/[\s"'\\]|^$/.test(str) ? "'" : ""](str) }, { completion: function () { completion.command = function command(context) { @@ -1155,8 +1176,9 @@ const Commands = Module("commands", { (function () { Commands.quoteMap = { - "\n": "n", - "\t": "t" + "\n": "\\n", + "\t": "\\t", + "'": "''" }; function quote(q, list) { let re = RegExp("[" + list + "]", "g"); @@ -1166,14 +1188,14 @@ const Commands = Module("commands", { }; Commands.complQuote = { '"': ['"', quote("", '\n\t"\\\\'), '"'], - "'": ["'", quote("", "\\\\'"), "'"], + "'": ["'", quote("", "'"), "'"], "": ["", quote("", "\\\\ '\""), ""] }; Commands.quoteArg = { '"': quote('"', '\n\t"\\\\'), - "'": quote("'", "\\\\'"), - "": quote("", "\\\\ '\"") + "'": quote("'", "'"), + "": quote("", "\\\\\\s'\"") }; Commands.parseBool = function (arg) { diff --git a/common/content/dactyl.js b/common/content/dactyl.js index 34892fd3..7ba4fceb 100644 --- a/common/content/dactyl.js +++ b/common/content/dactyl.js @@ -1291,7 +1291,7 @@ const Dactyl = Module("dactyl", { options.add(["loadplugins", "lpl"], "A regex list that defines which plugins are loaded at startup and via :loadplugins", - "regexlist", "\\.(js|vimp)$"); + "regexlist", "'\\.(js|vimp)$'"); options.add(["titlestring"], "Change the title of the window", diff --git a/common/content/finder.js b/common/content/finder.js index 359b08e9..fdff006f 100644 --- a/common/content/finder.js +++ b/common/content/finder.js @@ -211,7 +211,7 @@ const RangeFinder = Module("rangefinder", { options.add(["hlsearch", "hls"], "Highlight previous search pattern matches", - "boolean", "false", { + "boolean", false, { setter: function (value) { try { if (value) diff --git a/common/content/options.js b/common/content/options.js index 5546e4da..73b1ae42 100644 --- a/common/content/options.js +++ b/common/content/options.js @@ -50,8 +50,11 @@ const Option = Class("Option", { this._op = Option.ops[this.type]; - if (arguments.length > 3) - this.defaultValue = defaultValue; + if (arguments.length > 3) { + if (this.type == "string") + defaultValue = Commands.quote(defaultValue); + this.defaultValue = this.joinValues(this.parseValues(defaultValue)); + } if (extraInfo) update(this, extraInfo); @@ -75,7 +78,7 @@ const Option = Class("Option", { * @param {value} value The option value. * @returns {value|string[]} */ - parseValues: function (value) value, + parseValues: function (value) Option.dequote(value), /** * Returns values packed in the appropriate format for the option @@ -84,7 +87,7 @@ const Option = Class("Option", { * @param {value|string[]} values The option value. * @returns {value} */ - joinValues: function (vals) vals, + joinValues: function (vals) Commands.quote(vals), /** @property {value|string[]} The option value or array of values. */ get values() this.getValues(this.scope), @@ -376,8 +379,8 @@ const Option = Class("Option", { re.toString = function () Option.unparseRegex(this); return re; }, - unparseRegex: function (re) re.bang + re.source.replace(/\\(.)/g, function (m, n1) n1 == "/" ? n1 : m) + - (typeof re.result == "string" ? ":" + re.result : ""), + unparseRegex: function (re) re.bang + Option.quote(re.source.replace(/\\(.)/g, function (m, n1) n1 == "/" ? n1 : m)) + + (typeof re.result == "string" ? ":" + Option.quote(re.result) : ""), getKey: { stringlist: function (k) this.values.indexOf(k) >= 0, @@ -393,24 +396,46 @@ const Option = Class("Option", { }, joinValues: { - charlist: function (vals) vals.join(""), - stringlist: function (vals) vals.join(","), - stringmap: function (vals) [k + ":" + v for ([k, v] in Iterator(vals))].join(","), - regexlist: function (vals) vals.map(Option.unparseRegex).join(","), + charlist: function (vals) Commands.quote(vals.join("")), + stringlist: function (vals) vals.map(Option.quote).join(","), + stringmap: function (vals) [Option.quote(k) + ":" + Option.quote(v) for ([k, v] in Iterator(vals))].join(","), + regexlist: function (vals) vals.join(","), get regexmap() this.regexlist }, parseValues: { - number: function (value) Number(value), - boolean: function (value) value == "true" || value == true ? true : false, - charlist: function (value) Array.slice(value), - stringlist: function (value) (value === "") ? [] : value.split(","), - stringmap: function (value) array(util.split(v, /:/g, 2) for (v in values(value.split(",")))).toObject(), - regexlist: function (value) (value === "") ? [] : value.split(",").map(Option.parseRegex), - regexmap: function (value) value.split(",").map(function (v) util.split(v, /:/g, 2)) - .map(function ([k, v]) v != null ? Option.parseRegex(k, v) : Option.parseRegex(".?", k)) + number: function (value) Number(Option.dequote(value)), + boolean: function (value) Option.dequote(value) == "true" || value == true ? true : false, + charlist: function (value) Array.slice(Option.dequote(value)), + stringlist: function (value) (value === "") ? [] : Option.splitList(value), + stringmap: function (value) array(util.split(v, /:/g, 2) for (v in values(Option.splitList(value)))).toObject(), + regexlist: function (value) (value === "") ? [] : Option.splitList(value).map(Option.parseRegex), + regexmap: function (value) Option.splitList(value) + .map(function (v) util.split(v, /:/g, 2)) + .map(function ([k, v]) v != null ? Option.parseRegex(k, v) : Option.parseRegex(".?", k)) }, + dequote: function (value) { + let arg; + [, arg, Option._quote] = Commands.parseArg(String(value), ""); + Option._splitAt = 0; + return arg; + }, + splitList: function (value) { + let res = []; + Option._splitAt = 0; + do { + if (count !== undefined) + Option._splitAt += count + 1; + var [count, arg, quote] = Commands.parseArg(value, /,/); + Option._quote = quote; // FIXME + res.push(arg); + value = value.slice(count + 1); + } while (value.length); + return res; + }, + quote: function quote(str) Commands.quoteArg[/[\s"'\\,]|^$/.test(str) ? "'" : ""](str), + ops: { boolean: function (operator, values, scope, invert) { if (operator != "=") @@ -640,7 +665,7 @@ const Options = Module("options", { if (!scope) scope = Option.SCOPE_BOTH; - if (name in this._optionMap && (this._optionMap[name].scope & scope)) + if (this._optionMap[name] && (this._optionMap[name].scope & scope)) return this._optionMap[name]; return null; }, @@ -661,7 +686,7 @@ const Options = Module("options", { function opts(opt) { for (let opt in Iterator(options)) { let option = { - isDefault: opt.value == opt.defaultValue, + isDefault: opt.value === opt.defaultValue, name: opt.name, default: opt.defaultValue, pre: "\u00a0\u00a0", // Unicode nonbreaking space. @@ -1261,8 +1286,8 @@ const Options = Module("options", { serialize: function () [ { command: this.name, - arguments: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name - : opt.name + "=" + opt.value] + literalArg: [opt.type == "boolean" ? (opt.value ? "" : "no") + opt.name + : opt.name + "=" + opt.value] } for (opt in options) if (!opt.getter && opt.value != opt.defaultValue && (opt.scope & Option.SCOPE_GLOBAL)) @@ -1276,6 +1301,9 @@ const Options = Module("options", { }, update({ bang: true, + completer: function (context, args) { + return setCompleter(context, args); + }, domains: function (args) array.flatten(args.map(function (spec) { try { let opt = options.parseOpt(spec); @@ -1287,9 +1315,7 @@ const Options = Module("options", { } return []; })), - completer: function (context, args) { - return setCompleter(context, args); - }, + keepQuotes: true, privateData: function (args) args.some(function (spec) { let opt = options.parseOpt(spec); return opt.option && opt.option.privateData && @@ -1347,33 +1373,30 @@ const Options = Module("options", { return; } - let len = context.filter.length; switch (opt.type) { case "boolean": if (!completer) completer = function () [["true", ""], ["false", ""]]; break; case "regexlist": - newValues = context.filter.split(","); + newValues = Option.splitList(context.filter); // Fallthrough case "stringlist": - let target = newValues.pop() || ""; - len = target.length; + var target = newValues.pop() || ""; break; case "stringmap": case "regexmap": - let vals = context.filter.split(","); + let vals = Option.splitList(context.filter); target = vals.pop() || ""; - len = target.length - (target.indexOf(":") + 1); - break; - case "charlist": - len = 0; + Option._splitAt += target.indexOf(":") ? target.indexOf(":") + 1 : 0; break; } // TODO: Highlight when invalid - context.advance(context.filter.length - len); + context.advance(Option._splitAt); + context.filter = target != null ? target : Option.dequote(context.filter); context.title = ["Option Value"]; + context.quote = Commands.complQuote[Option._quote] || Commands.complQuote[""] // Not Vim compatible, but is a significant enough improvement // that it's worth breaking compatibility. if (isArray(newValues)) { diff --git a/common/locale/en-US/options.xml b/common/locale/en-US/options.xml index 8628e360..9e7b6b56 100644 --- a/common/locale/en-US/options.xml +++ b/common/locale/en-US/options.xml @@ -30,14 +30,21 @@
number
A numeric value
string
A string value
charlist
A string containing a discrete set of distinct characters
-
stringlist
A comma-separated list of strings
+
stringlist
+
+ A comma-separated list of strings. Any comma appearing within single + or double quotes, or prefixed with a \, will not be treated + as an item separator. +
stringmap
A comma-separated list of key-value pairs, e.g., key:val,foo:bar
regexlist
A comma-separated list of regular expressions. Expressions may be prefixed with a !, in which case the match will be negated. A literal ! at the begining of the expression may be matched with - [!]. Generally, the first matching regular expression is used. + [!]. Generally, the first matching regular expression is + used. Any comma appearing within single or double quotes, or prefixed + with a \, will not be treated as an item separator.
regexmap
@@ -1452,10 +1459,6 @@ :set wildignore=\.o$,^\..*\.s[a-z]2$ Unlike Vim, each pattern is a regular expression rather than a glob. - - The only way to include a literal comma in a pattern is with the - escape sequence \u0044. - diff --git a/common/modules/base.jsm b/common/modules/base.jsm index 12a007ad..413df1a0 100644 --- a/common/modules/base.jsm +++ b/common/modules/base.jsm @@ -474,8 +474,9 @@ function call(fn) { * value of the property. */ function memoize(obj, key, getter) { - obj.__defineGetter__(key, function () - Class.replaceProperty(this, key, getter.call(this, key))); + obj.__defineGetter__(key, function () ( + Class.replaceProperty(this, key, null), + Class.replaceProperty(this, key, getter.call(this, key)))); } /** diff --git a/pentadactyl/NEWS b/pentadactyl/NEWS index 3759507b..9638af63 100644 --- a/pentadactyl/NEWS +++ b/pentadactyl/NEWS @@ -26,6 +26,11 @@ I.e., 'fo\o''bar' ≡ fo\o'bar * IMPORTANT: 'cdpath' and 'runtimepath' no longer treat ‘,,’ specially. Use ‘.’ instead. + * IMPORTANT: Option value quoting has changed. List options will + no longer be split at quoted commas and the option name, + operators, and = sign may no longer be quoted. This will break + certain automatically-generated configuration files. + See :help stringlist * Added 'altwildmode' and commandline key binding. * Added 'autocomplete' option for specifying which completion groups should be auto-completed.