index.js 5.0 KB

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