index.js
  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