diff --git a/.eslintrc b/.eslintrc index 4c29af2..9569d51 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,29 +5,18 @@ "rules": { "array-bracket-newline": 0, - "array-callback-return": 1, - "brace-style": 1, "complexity": 0, - "consistent-return": 1, - "curly": 1, "eqeqeq": 1, - "func-style": 1, + "func-style": [2, "declaration"], "max-depth": 0, - "max-lines-per-function": 1, + "max-lines-per-function": 0, "max-statements": 0, "multiline-comment-style": 0, - "no-else-return": 1, - "no-lonely-if": 1, "no-negated-condition": 1, "no-param-reassign": 1, + "no-lonely-if": 1, "no-shadow": 1, "no-template-curly-in-string": 0, - "no-use-before-define": 1, - "no-useless-escape": 1, - "nonblock-statement-body-position": 1, - "prefer-regex-literals": 1, - "quotes": 1, - "wrap-regex": 1, }, "overrides": [ diff --git a/index.js b/index.js index ce534c0..0a9ae2d 100644 --- a/index.js +++ b/index.js @@ -4,16 +4,12 @@ exports.quote = function (xs) { return xs.map(function (s) { if (s && typeof s === 'object') { return s.op.replace(/(.)/g, '\\$1'); - } - else if (/["\s]/.test(s) && !/'/.test(s)) { + } else if ((/["\s]/).test(s) && !(/'/).test(s)) { return "'" + s.replace(/(['\\])/g, '\\$1') + "'"; - } - else if (/["'\s]/.test(s)) { + } else if ((/["'\s]/).test(s)) { return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"'; } - else { - return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@\[\\\]^`{|}])/g, '$1\\$2'); - } + return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2'); }).join(' '); }; @@ -32,36 +28,42 @@ for (var i = 0; i < 4; i++) { TOKEN += (Math.pow(16, 8) * Math.random()).toString(16); } -exports.parse = function (s, env, opts) { - var mapped = parse(s, env, opts); - if (typeof env !== 'function') return mapped; - return mapped.reduce(function (acc, s) { - if (typeof s === 'object') return acc.concat(s); - var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); - if (xs.length === 1) return acc.concat(xs[0]); - return acc.concat(xs.filter(Boolean).map(function (x) { - if (RegExp('^' + TOKEN).test(x)) { - return JSON.parse(x.split(TOKEN)[1]); - } - else return x; - })); - }, []); -}; - function parse(s, env, opts) { var chunker = new RegExp([ '(' + CONTROL + ')', // control chars '(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*' ].join('|'), 'g'); var match = s.match(chunker).filter(Boolean); + + if (!match) { + return []; + } + if (!env) { + env = {}; + } + if (!opts) { + opts = {}; + } + var commented = false; - if (!match) return []; - if (!env) env = {}; - if (!opts) opts = {}; + function getVar(_, pre, key) { + var r = typeof env === 'function' ? env(key) : env[key]; + if (r === undefined && key != '') { + r = ''; + } else if (r === undefined) { + r = '$'; + } + + if (typeof r === 'object') { + return pre + TOKEN + JSON.stringify(r) + TOKEN; + } + return pre + r; + } + return match.map(function (s, j) { if (commented) { - return; + return void undefined; } if (RegExp('^' + CONTROL + '$').test(s)) { return { op: s }; @@ -69,13 +71,13 @@ function parse(s, env, opts) { // Hand-written scanner/parser for Bash quoting rules: // - // 1. inside single quotes, all characters are printed literally. - // 2. inside double quotes, all characters are printed literally - // except variables prefixed by '$' and backslashes followed by - // either a double quote or another backslash. - // 3. outside of any quotes, backslashes are treated as escape - // characters and not printed (unless they are themselves escaped) - // 4. quote context can switch mid-token if there is no whitespace + // 1. inside single quotes, all characters are printed literally. + // 2. inside double quotes, all characters are printed literally + // except variables prefixed by '$' and backslashes followed by + // either a double quote or another backslash. + // 3. outside of any quotes, backslashes are treated as escape + // characters and not printed (unless they are themselves escaped) + // 4. quote context can switch mid-token if there is no whitespace // between the two quote contexts (e.g. all'one'"token" parses as // "allonetoken") var SQ = "'"; @@ -86,22 +88,52 @@ function parse(s, env, opts) { var esc = false; var out = ''; var isGlob = false; + var i; - for (var i = 0, len = s.length; i < len; i++) { + function parseEnvVar() { + i += 1; + var varend; + var varname; + // debugger + if (s.charAt(i) === '{') { + i += 1; + if (s.charAt(i) === '}') { + throw new Error('Bad substitution: ' + s.substr(i - 2, 3)); + } + varend = s.indexOf('}', i); + if (varend < 0) { + throw new Error('Bad substitution: ' + s.substr(i)); + } + varname = s.substr(i, varend - i); + i = varend; + } else if ((/[*@#?$!_-]/).test(s.charAt(i))) { + varname = s.charAt(i); + i += 1; + } else { + varend = s.substr(i).match(/[^\w\d_]/); + if (!varend) { + varname = s.substr(i); + i = s.length; + } else { + varname = s.substr(i, varend.index); + i += varend.index - 1; + } + } + return getVar(null, '', varname); + } + + for (i = 0; i < s.length; i++) { var c = s.charAt(i); isGlob = isGlob || (!quote && (c === '*' || c === '?')); if (esc) { out += c; esc = false; - } - else if (quote) { + } else if (quote) { if (c === quote) { quote = false; - } - else if (quote == SQ) { + } else if (quote == SQ) { out += c; - } - else { // Double quote + } else { // Double quote if (c === BS) { i += 1; c = s.charAt(i); @@ -110,92 +142,62 @@ function parse(s, env, opts) { } else { out += BS + c; } - } - else if (c === DS) { + } else if (c === DS) { out += parseEnvVar(); - } - else { + } else { out += c; } } - } - else if (c === DQ || c === SQ) { + } else if (c === DQ || c === SQ) { quote = c; - } - else if (RegExp('^' + CONTROL + '$').test(c)) { + } else if (RegExp('^' + CONTROL + '$').test(c)) { return { op: s }; - } - else if (RegExp('^#$').test(c)) { + } else if ((/^#$/).test(c)) { commented = true; if (out.length) { return [out, { comment: s.slice(i + 1) + match.slice(j + 1).join(' ') }]; } return [{ comment: s.slice(i + 1) + match.slice(j + 1).join(' ') }]; - } - else if (c === BS) { + } else if (c === BS) { esc = true; - } - else if (c === DS) { + } else if (c === DS) { out += parseEnvVar(); + } else { + out += c; } - else out += c; } - if (isGlob) return { op: 'glob', pattern: out }; + if (isGlob) { + return { op: 'glob', pattern: out }; + } return out; - - function parseEnvVar() { - i += 1; - var varend, varname; - // debugger - if (s.charAt(i) === '{') { - i += 1; - if (s.charAt(i) === '}') { - throw new Error("Bad substitution: " + s.substr(i - 2, 3)); - } - varend = s.indexOf('}', i); - if (varend < 0) { - throw new Error("Bad substitution: " + s.substr(i)); - } - varname = s.substr(i, varend - i); - i = varend; - } - else if (/[*@#?$!_\-]/.test(s.charAt(i))) { - varname = s.charAt(i); - i += 1; - } - else { - varend = s.substr(i).match(/[^\w\d_]/); - if (!varend) { - varname = s.substr(i); - i = s.length; - } else { - varname = s.substr(i, varend.index); - i += varend.index - 1; - } - } - return getVar(null, '', varname); + }).reduce(function (prev, arg) { // finalize parsed aruments + if (arg === undefined) { + return prev; } - }) - // finalize parsed aruments - .reduce(function (prev, arg) { - if (arg === undefined) { - return prev; - } - return prev.concat(arg); - }, []); - - function getVar(_, pre, key) { - var r = typeof env === 'function' ? env(key) : env[key]; - if (r === undefined && key != '') - r = ''; - else if (r === undefined) - r = '$'; + return prev.concat(arg); + }, []); +} - if (typeof r === 'object') { - return pre + TOKEN + JSON.stringify(r) + TOKEN; - } - else return pre + r; +exports.parse = function (s, env, opts) { + var mapped = parse(s, env, opts); + if (typeof env !== 'function') { + return mapped; } -} + return mapped.reduce(function (acc, s) { + if (typeof s === 'object') { + return acc.concat(s); + } + var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); + if (xs.length === 1) { + return acc.concat(xs[0]); + } + return acc.concat(xs.filter(Boolean).map(function (x) { + if (RegExp('^' + TOKEN).test(x)) { + return JSON.parse(x.split(TOKEN)[1]); + } + return x; + })); + }, []); +}; diff --git a/test/env.js b/test/env.js index 7b48b88..deb854b 100644 --- a/test/env.js +++ b/test/env.js @@ -11,13 +11,13 @@ test('expand environment variables', function (t) { t.same(parse("'-$X-$Y-'", { X: 'a', Y: 'b' }), ['-$X-$Y-']); t.same(parse('qrs"$zzz"wxy', { zzz: 'tuv' }), ['qrstuvwxy']); t.same(parse("qrs'$zzz'wxy", { zzz: 'tuv' }), ['qrs$zzzwxy']); - t.same(parse("qrs${zzz}wxy"), ['qrswxy']); - t.same(parse("qrs$wxy $"), ['qrs', '$']); + t.same(parse('qrs${zzz}wxy'), ['qrswxy']); + t.same(parse('qrs$wxy $'), ['qrs', '$']); t.same(parse('grep "xy$"'), ['grep', 'xy$']); - t.same(parse("ab$x", { x: 'c' }), ['abc']); - t.same(parse("ab\\$x", { x: 'c' }), ['ab$x']); - t.same(parse("ab${x}def", { x: 'c' }), ['abcdef']); - t.same(parse("ab\\${x}def", { x: 'c' }), ['ab${x}def']); + t.same(parse('ab$x', { x: 'c' }), ['abc']); + t.same(parse('ab\\$x', { x: 'c' }), ['ab$x']); + t.same(parse('ab${x}def', { x: 'c' }), ['abcdef']); + t.same(parse('ab\\${x}def', { x: 'c' }), ['ab${x}def']); t.same(parse('"ab\\${x}def"', { x: 'c' }), ['ab${x}def']); t.end(); diff --git a/test/env_fn.js b/test/env_fn.js index dd46980..968e912 100644 --- a/test/env_fn.js +++ b/test/env_fn.js @@ -3,6 +3,14 @@ var test = require('tape'); var parse = require('../').parse; +function getEnv() { + return 'xxx'; +} + +function getEnvObj() { + return { op: '@@' }; +} + test('functional env expansion', function (t) { t.plan(4); @@ -10,12 +18,4 @@ test('functional env expansion', function (t) { t.same(parse('a $XYZ c', getEnvObj), ['a', { op: '@@' }, 'c']); t.same(parse('a${XYZ}c', getEnvObj), ['a', { op: '@@' }, 'c']); t.same(parse('"a $XYZ c"', getEnvObj), ['a ', { op: '@@' }, ' c']); - - function getEnv() { - return 'xxx'; - } - - function getEnvObj() { - return { op: '@@' }; - } }); diff --git a/test/parse.js b/test/parse.js index 7373181..9c78cbf 100644 --- a/test/parse.js +++ b/test/parse.js @@ -14,7 +14,7 @@ test('parse shell commands', function (t) { t.same(parse('echo "foo = \\"foo\\""'), ['echo', 'foo = "foo"']); t.same(parse(''), []); t.same(parse(' '), []); - t.same(parse("\t"), []); + t.same(parse('\t'), []); t.same(parse('a"b c d"e'), ['ab c de']); t.same(parse('a\\ b"c d"\\ e f'), ['a bc d e', 'f']); t.same(parse('a\\ b"c d"\\ e\'f g\' h'), ['a bc d ef g', 'h']); diff --git a/test/quote.js b/test/quote.js index d972077..8ecd7b5 100644 --- a/test/quote.js +++ b/test/quote.js @@ -14,11 +14,11 @@ test('quote', function (t) { '\\$ \\` "\'"' ); t.equal(quote([]), ''); - t.equal(quote(["a\nb"]), "'a\nb'"); + t.equal(quote(['a\nb']), "'a\nb'"); t.equal(quote([' #(){}*|][!']), "' #(){}*|][!'"); t.equal(quote(["'#(){}*|][!"]), '"\'#(){}*|][\\!"'); - t.equal(quote(["X#(){}*|][!"]), "X\\#\\(\\)\\{\\}\\*\\|\\]\\[\\!"); - t.equal(quote(["a\n#\nb"]), "'a\n#\nb'"); + t.equal(quote(['X#(){}*|][!']), 'X\\#\\(\\)\\{\\}\\*\\|\\]\\[\\!'); + t.equal(quote(['a\n#\nb']), "'a\n#\nb'"); t.equal(quote(['><;{}']), '\\>\\<\\;\\{\\}'); t.equal(quote(['a', 1, true, false]), 'a 1 true false'); t.equal(quote(['a', 1, null, undefined]), 'a 1 null undefined');