index.js 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200
  1. var json = typeof JSON !== undefined ? JSON : require('jsonify');
  2. var map = require('array-map');
  3. var filter = require('array-filter');
  4. var reduce = require('array-reduce');
  5. exports.quote = function (xs) {
  6. return map(xs, function (s) {
  7. if (s && typeof s === 'object') {
  8. return s.op.replace(/(.)/g, '\\$1');
  9. }
  10. else if (/["\s]/.test(s) && !/'/.test(s)) {
  11. return "'" + s.replace(/(['\\])/g, '\\$1') + "'";
  12. }
  13. else if (/["'\s]/.test(s)) {
  14. return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"';
  15. }
  16. else {
  17. return String(s).replace(/([#!"$&'()*,:;<=>?@\[\\\]^`{|}])/g, '\\$1');
  18. }
  19. }).join(' ');
  20. };
  21. var CONTROL = '(?:' + [
  22. '\\|\\|', '\\&\\&', ';;', '\\|\\&', '[&;()|<>]'
  23. ].join('|') + ')';
  24. var META = '|&;()<> \\t';
  25. var BAREWORD = '(\\\\[\'"' + META + ']|[^\\s\'"' + META + '])+';
  26. var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"';
  27. var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\'';
  28. var TOKEN = '';
  29. for (var i = 0; i < 4; i++) {
  30. TOKEN += (Math.pow(16,8)*Math.random()).toString(16);
  31. }
  32. exports.parse = function (s, env, opts) {
  33. var mapped = parse(s, env, opts);
  34. if (typeof env !== 'function') return mapped;
  35. return reduce(mapped, function (acc, s) {
  36. if (typeof s === 'object') return acc.concat(s);
  37. var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g'));
  38. if (xs.length === 1) return acc.concat(xs[0]);
  39. return acc.concat(map(filter(xs, Boolean), function (x) {
  40. if (RegExp('^' + TOKEN).test(x)) {
  41. return json.parse(x.split(TOKEN)[1]);
  42. }
  43. else return x;
  44. }));
  45. }, []);
  46. };
  47. function parse (s, env, opts) {
  48. var chunker = new RegExp([
  49. '(' + CONTROL + ')', // control chars
  50. '(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')*'
  51. ].join('|'), 'g');
  52. var match = filter(s.match(chunker), Boolean);
  53. var commented = false;
  54. if (!match) return [];
  55. if (!env) env = {};
  56. if (!opts) opts = {};
  57. return map(match, function (s, j) {
  58. if (commented) {
  59. return;
  60. }
  61. if (RegExp('^' + CONTROL + '$').test(s)) {
  62. return { op: s };
  63. }
  64. // Hand-written scanner/parser for Bash quoting rules:
  65. //
  66. // 1. inside single quotes, all characters are printed literally.
  67. // 2. inside double quotes, all characters are printed literally
  68. // except variables prefixed by '$' and backslashes followed by
  69. // either a double quote or another backslash.
  70. // 3. outside of any quotes, backslashes are treated as escape
  71. // characters and not printed (unless they are themselves escaped)
  72. // 4. quote context can switch mid-token if there is no whitespace
  73. // between the two quote contexts (e.g. all'one'"token" parses as
  74. // "allonetoken")
  75. var SQ = "'";
  76. var DQ = '"';
  77. var DS = '$';
  78. var BS = opts.escape || '\\';
  79. var quote = false;
  80. var esc = false;
  81. var out = '';
  82. var isGlob = false;
  83. for (var i = 0, len = s.length; i < len; i++) {
  84. var c = s.charAt(i);
  85. isGlob = isGlob || (!quote && (c === '*' || c === '?'));
  86. if (esc) {
  87. out += c;
  88. esc = false;
  89. }
  90. else if (quote) {
  91. if (c === quote) {
  92. quote = false;
  93. }
  94. else if (quote == SQ) {
  95. out += c;
  96. }
  97. else { // Double quote
  98. if (c === BS) {
  99. i += 1;
  100. c = s.charAt(i);
  101. if (c === DQ || c === BS || c === DS) {
  102. out += c;
  103. } else {
  104. out += BS + c;
  105. }
  106. }
  107. else if (c === DS) {
  108. out += parseEnvVar();
  109. }
  110. else {
  111. out += c;
  112. }
  113. }
  114. }
  115. else if (c === DQ || c === SQ) {
  116. quote = c;
  117. }
  118. else if (RegExp('^' + CONTROL + '$').test(c)) {
  119. return { op: s };
  120. }
  121. else if (RegExp('^#$').test(c)) {
  122. commented = true;
  123. if (out.length){
  124. return [out, { comment: s.slice(i+1) + match.slice(j+1).join(' ') }];
  125. }
  126. return [{ comment: s.slice(i+1) + match.slice(j+1).join(' ') }];
  127. }
  128. else if (c === BS) {
  129. esc = true;
  130. }
  131. else if (c === DS) {
  132. out += parseEnvVar();
  133. }
  134. else out += c;
  135. }
  136. if (isGlob) return {op: 'glob', pattern: out};
  137. return out;
  138. function parseEnvVar() {
  139. i += 1;
  140. var varend, varname;
  141. //debugger
  142. if (s.charAt(i) === '{') {
  143. i += 1;
  144. if (s.charAt(i) === '}') {
  145. throw new Error("Bad substitution: " + s.substr(i - 2, 3));
  146. }
  147. varend = s.indexOf('}', i);
  148. if (varend < 0) {
  149. throw new Error("Bad substitution: " + s.substr(i));
  150. }
  151. varname = s.substr(i, varend - i);
  152. i = varend;
  153. }
  154. else if (/[*@#?$!_\-]/.test(s.charAt(i))) {
  155. varname = s.charAt(i);
  156. i += 1;
  157. }
  158. else {
  159. varend = s.substr(i).match(/[^\w\d_]/);
  160. if (!varend) {
  161. varname = s.substr(i);
  162. i = s.length;
  163. } else {
  164. varname = s.substr(i, varend.index);
  165. i += varend.index - 1;
  166. }
  167. }
  168. return getVar(null, '', varname);
  169. }
  170. })
  171. // finalize parsed aruments
  172. .reduce(function(prev, arg){
  173. if (arg === undefined){
  174. return prev;
  175. }
  176. return prev.concat(arg);
  177. },[]);
  178. function getVar (_, pre, key) {
  179. var r = typeof env === 'function' ? env(key) : env[key];
  180. if (r === undefined) r = '';
  181. if (typeof r === 'object') {
  182. return pre + TOKEN + json.stringify(r) + TOKEN;
  183. }
  184. else return pre + r;
  185. }
  186. }