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