1var balanced = require('balanced-match'); 2 3module.exports = expandTop; 4 5var escSlash = '\0SLASH'+Math.random()+'\0'; 6var escOpen = '\0OPEN'+Math.random()+'\0'; 7var escClose = '\0CLOSE'+Math.random()+'\0'; 8var escComma = '\0COMMA'+Math.random()+'\0'; 9var escPeriod = '\0PERIOD'+Math.random()+'\0'; 10 11function numeric(str) { 12 return parseInt(str, 10) == str 13 ? parseInt(str, 10) 14 : str.charCodeAt(0); 15} 16 17function escapeBraces(str) { 18 return str.split('\\\\').join(escSlash) 19 .split('\\{').join(escOpen) 20 .split('\\}').join(escClose) 21 .split('\\,').join(escComma) 22 .split('\\.').join(escPeriod); 23} 24 25function unescapeBraces(str) { 26 return str.split(escSlash).join('\\') 27 .split(escOpen).join('{') 28 .split(escClose).join('}') 29 .split(escComma).join(',') 30 .split(escPeriod).join('.'); 31} 32 33 34// Basically just str.split(","), but handling cases 35// where we have nested braced sections, which should be 36// treated as individual members, like {a,{b,c},d} 37function parseCommaParts(str) { 38 if (!str) 39 return ['']; 40 41 var parts = []; 42 var m = balanced('{', '}', str); 43 44 if (!m) 45 return str.split(','); 46 47 var pre = m.pre; 48 var body = m.body; 49 var post = m.post; 50 var p = pre.split(','); 51 52 p[p.length-1] += '{' + body + '}'; 53 var postParts = parseCommaParts(post); 54 if (post.length) { 55 p[p.length-1] += postParts.shift(); 56 p.push.apply(p, postParts); 57 } 58 59 parts.push.apply(parts, p); 60 61 return parts; 62} 63 64function expandTop(str) { 65 if (!str) 66 return []; 67 68 // I don't know why Bash 4.3 does this, but it does. 69 // Anything starting with {} will have the first two bytes preserved 70 // but *only* at the top level, so {},a}b will not expand to anything, 71 // but a{},b}c will be expanded to [a}c,abc]. 72 // One could argue that this is a bug in Bash, but since the goal of 73 // this module is to match Bash's rules, we escape a leading {} 74 if (str.substr(0, 2) === '{}') { 75 str = '\\{\\}' + str.substr(2); 76 } 77 78 return expand(escapeBraces(str), true).map(unescapeBraces); 79} 80 81function embrace(str) { 82 return '{' + str + '}'; 83} 84function isPadded(el) { 85 return /^-?0\d/.test(el); 86} 87 88function lte(i, y) { 89 return i <= y; 90} 91function gte(i, y) { 92 return i >= y; 93} 94 95function expand(str, isTop) { 96 var expansions = []; 97 98 var m = balanced('{', '}', str); 99 if (!m) return [str]; 100 101 // no need to expand pre, since it is guaranteed to be free of brace-sets 102 var pre = m.pre; 103 var post = m.post.length 104 ? expand(m.post, false) 105 : ['']; 106 107 if (/\$$/.test(m.pre)) { 108 for (var k = 0; k < post.length; k++) { 109 var expansion = pre+ '{' + m.body + '}' + post[k]; 110 expansions.push(expansion); 111 } 112 } else { 113 var isNumericSequence = /^-?\d+\.\.-?\d+(?:\.\.-?\d+)?$/.test(m.body); 114 var isAlphaSequence = /^[a-zA-Z]\.\.[a-zA-Z](?:\.\.-?\d+)?$/.test(m.body); 115 var isSequence = isNumericSequence || isAlphaSequence; 116 var isOptions = m.body.indexOf(',') >= 0; 117 if (!isSequence && !isOptions) { 118 // {a},b} 119 if (m.post.match(/,.*\}/)) { 120 str = m.pre + '{' + m.body + escClose + m.post; 121 return expand(str); 122 } 123 return [str]; 124 } 125 126 var n; 127 if (isSequence) { 128 n = m.body.split(/\.\./); 129 } else { 130 n = parseCommaParts(m.body); 131 if (n.length === 1) { 132 // x{{a,b}}y ==> x{a}y x{b}y 133 n = expand(n[0], false).map(embrace); 134 if (n.length === 1) { 135 return post.map(function(p) { 136 return m.pre + n[0] + p; 137 }); 138 } 139 } 140 } 141 142 // at this point, n is the parts, and we know it's not a comma set 143 // with a single entry. 144 var N; 145 146 if (isSequence) { 147 var x = numeric(n[0]); 148 var y = numeric(n[1]); 149 var width = Math.max(n[0].length, n[1].length) 150 var incr = n.length == 3 151 ? Math.abs(numeric(n[2])) 152 : 1; 153 var test = lte; 154 var reverse = y < x; 155 if (reverse) { 156 incr *= -1; 157 test = gte; 158 } 159 var pad = n.some(isPadded); 160 161 N = []; 162 163 for (var i = x; test(i, y); i += incr) { 164 var c; 165 if (isAlphaSequence) { 166 c = String.fromCharCode(i); 167 if (c === '\\') 168 c = ''; 169 } else { 170 c = String(i); 171 if (pad) { 172 var need = width - c.length; 173 if (need > 0) { 174 var z = new Array(need + 1).join('0'); 175 if (i < 0) 176 c = '-' + z + c.slice(1); 177 else 178 c = z + c; 179 } 180 } 181 } 182 N.push(c); 183 } 184 } else { 185 N = []; 186 187 for (var j = 0; j < n.length; j++) { 188 N.push.apply(N, expand(n[j], false)); 189 } 190 } 191 192 for (var j = 0; j < N.length; j++) { 193 for (var k = 0; k < post.length; k++) { 194 var expansion = pre + N[j] + post[k]; 195 if (!isTop || isSequence || expansion) 196 expansions.push(expansion); 197 } 198 } 199 } 200 201 return expansions; 202} 203 204