const balanced = require('../balancedMatch'); const escSlash = '\0SLASH' + Math.random() + '\0'; const escOpen = '\0OPEN' + Math.random() + '\0'; const escClose = '\0CLOSE' + Math.random() + '\0'; const escComma = '\0COMMA' + Math.random() + '\0'; const escPeriod = '\0PERIOD' + Math.random() + '\0'; /** * @return {number} */ function numeric(str) { return parseInt(str, 10) == str ? parseInt(str, 10) : str.charCodeAt(0); } /** * @param {string} str */ function escapeBraces(str) { return str.split('\\\\').join(escSlash) .split('\\{').join(escOpen) .split('\\}').join(escClose) .split('\\,').join(escComma) .split('\\.').join(escPeriod); } /** * @param {string} str */ function unescapeBraces(str) { return str.split(escSlash).join('\\') .split(escOpen).join('{') .split(escClose).join('}') .split(escComma).join(',') .split(escPeriod).join('.'); } /** * Basically just str.split(","), but handling cases * where we have nested braced sections, which should be * treated as individual members, like {a,{b,c},d} * @param {string} str */ function parseCommaParts(str) { if (!str) return ['']; const parts = []; const m = balanced('{', '}', str); if (!m) return str.split(','); const { pre, body, post } = m; const p = pre.split(','); p[p.length - 1] += '{' + body + '}'; const postParts = parseCommaParts(post); if (post.length) { p[p.length - 1] += postParts.shift(); p.push.apply(p, postParts); } parts.push.apply(parts, p); return parts; } /** * @param {string} str */ function expandTop(str) { if (!str) return []; // I don't know why Bash 4.3 does this, but it does. // Anything starting with {} will have the first two bytes preserved // but *only* at the top level, so {},a}b will not expand to anything, // but a{},b}c will be expanded to [a}c,abc]. // One could argue that this is a bug in Bash, but since the goal of // this module is to match Bash's rules, we escape a leading {} if (str.slice(0, 2) === '{}') { str = '\\{\\}' + str.slice(2); } return expand(escapeBraces(str), true).map(unescapeBraces); } /** * @param {string} str */ function embrace(str) { return '{' + str + '}'; } /** * @param {string} el */ function isPadded(el) { return /^-?0\d/.test(el); } /** * @param {number} i * @param {number} y */ function lte(i, y) { return i <= y; } /** * @param {number} i * @param {number} y */ function gte(i, y) { return i >= y; } /** * @param {string} str * @param {boolean} [isTop] */ function expand(str, isTop) { /** @type {string[]} */ const expansions = []; const m = balanced('{', '}', str); if (!m) return [str]; // no need to expand pre, since it is guaranteed to be free of brace-sets const pre = m.pre; const post = m.post.length ? expand(m.post, false) : ['']; if (/\$$/.test(m.pre)) { for (let k = 0; k < post.length; k++) { const expansion = pre + '{' + m.body + '}' + post[k]; expansions.push(expansion); } } else { const isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); const isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); const isSequence = isNumericSequence || isAlphaSequence; const isOptions = m.body.indexOf(',') >= 0; if (!isSequence && !isOptions) { // {a},b} if (m.post.match(/,.*\}/)) { str = m.pre + '{' + m.body + escClose + m.post; return expand(str); } return [str]; } let n; if (isSequence) { n = m.body.split(/\.\./); } else { n = parseCommaParts(m.body); if (n.length === 1) { // x{{a,b}}y ==> x{a}y x{b}y n = expand(n[0], false).map(embrace); if (n.length === 1) { return post.map(function (p) { return m.pre + n[0] + p; }); } } } // at this point, n is the parts, and we know it's not a comma set // with a single entry. let N; if (isSequence) { const x = numeric(n[0]); const y = numeric(n[1]); const width = Math.max(n[0].length, n[1].length) let incr = n.length == 3 ? Math.abs(numeric(n[2])) : 1; let test = lte; const reverse = y < x; if (reverse) { incr *= -1; test = gte; } const pad = n.some(isPadded); N = []; for (let i = x; test(i, y); i += incr) { let c; if (isAlphaSequence) { c = String.fromCharCode(i); if (c === '\\') c = ''; } else { c = String(i); if (pad) { const need = width - c.length; if (need > 0) { const z = new Array(need + 1).join('0'); if (i < 0) c = '-' + z + c.slice(1); else c = z + c; } } } N.push(c); } } else { N = []; for (let j = 0; j < n.length; j++) { N.push.apply(N, expand(n[j], false)); } } for (let j = 0; j < N.length; j++) { for (let k = 0; k < post.length; k++) { const expansion = pre + N[j] + post[k]; if (!isTop || isSequence || expansion) expansions.push(expansion); } } } return expansions; } module.exports = expandTop;