parse.js 4.6 KB

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