index.js 4.99 KB
'use strict';

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)) {
			return "'" + s.replace(/(['\\])/g, '\\$1') + "'";
		} else if ((/["'\s]/).test(s)) {
			return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"';
		}
		return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2');
	}).join(' ');
};

// '<(' is process substitution operator and
// can be parsed the same as control operator
var CONTROL = '(?:' + [
	'\\|\\|', '\\&\\&', ';;', '\\|\\&', '\\<\\(', '>>', '>\\&', '[&;()|<>]'
].join('|') + ')';
var META = '|&;()<> \\t';
var BAREWORD = '(\\\\[\'"' + META + ']|[^\\s\'"' + META + '])+';
var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';

var TOKEN = '';
for (var i = 0; i < 4; i++) {
	TOKEN += (Math.pow(16, 8) * Math.random()).toString(16);
}

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;

	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 void undefined;
		}
		if (RegExp('^' + CONTROL + '$').test(s)) {
			return { op: s };
		}

		// 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
		//     between the two quote contexts (e.g. all'one'"token" parses as
		//     "allonetoken")
		var SQ = "'";
		var DQ = '"';
		var DS = '$';
		var BS = opts.escape || '\\';
		var quote = false;
		var esc = false;
		var out = '';
		var isGlob = false;
		var 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) {
				if (c === quote) {
					quote = false;
				} else if (quote == SQ) {
					out += c;
				} else { // Double quote
					if (c === BS) {
						i += 1;
						c = s.charAt(i);
						if (c === DQ || c === BS || c === DS) {
							out += c;
						} else {
							out += BS + c;
						}
					} else if (c === DS) {
						out += parseEnvVar();
					} else {
						out += c;
					}
				}
			} else if (c === DQ || c === SQ) {
				quote = c;
			} else if (RegExp('^' + CONTROL + '$').test(c)) {
				return { op: s };
			} 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) {
				esc = true;
			} else if (c === DS) {
				out += parseEnvVar();
			} else {
				out += c;
			}
		}

		if (isGlob) {
			return { op: 'glob', pattern: out };
		}

		return out;
	}).reduce(function (prev, arg) { // finalize parsed aruments
		if (arg === undefined) {
			return prev;
		}
		return prev.concat(arg);
	}, []);
}

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;
		}));
	}, []);
};