parse.js 5.1 KB

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