parse.js 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330
  1. 'use strict';
  2. /**
  3. * @import {
  4. * ControlOperator,
  5. * Env,
  6. * GlobPattern,
  7. * ParseEntry,
  8. * } from './parse' */
  9. // '<(' is process substitution operator and
  10. // can be parsed the same as control operator
  11. var CONTROL = /** @type {const} */ ('(?:') + /** @type {const} */ ([
  12. '\\|\\|',
  13. '\\&\\&',
  14. ';;',
  15. '\\|\\&',
  16. '\\<\\(',
  17. '\\<\\<\\<',
  18. '>>',
  19. '>\\&',
  20. '<\\&',
  21. '[&;()|<>]'
  22. ]).join(/** @type {const} */ ('|')) + /** @type {const} */ (')');
  23. var controlRE = new RegExp('^' + CONTROL + '$');
  24. var META = /** @type {const} */ ('|&;()<> \\t');
  25. var SINGLE_QUOTE = /** @type {const} */ ('\'([^\']*?)\'');
  26. var DOUBLE_QUOTE = /** @type {const} */ ('"((\\\\"|[^"])*?)"');
  27. var hash = /^#$/;
  28. var SQ = /** @type {const} */ ("'");
  29. var DQ = /** @type {const} */ ('"');
  30. var DS = /** @type {const} */ ('$');
  31. var TOKEN = '';
  32. var mult = /** @type {const} */ (0x100000000); // Math.pow(16, 8);
  33. for (var i = 0; i < 4; i++) {
  34. TOKEN += (mult * Math.random()).toString(16);
  35. }
  36. var startsWithToken = new RegExp('^' + TOKEN);
  37. /**
  38. * @param {string} s
  39. * @param {RegExp} r
  40. */
  41. function matchAll(s, r) {
  42. var origIndex = r.lastIndex;
  43. var matches = [];
  44. var matchObj;
  45. while ((matchObj = r.exec(s))) {
  46. matches[matches.length] = matchObj;
  47. if (r.lastIndex === matchObj.index) {
  48. r.lastIndex += 1;
  49. }
  50. }
  51. r.lastIndex = origIndex;
  52. return matches;
  53. }
  54. /**
  55. * @param {Env} env
  56. * @param {string} pre
  57. * @param {string} key
  58. */
  59. function getVar(env, pre, key) {
  60. var r = typeof env === 'function' ? env(key) : env[key];
  61. if (typeof r === 'undefined' && key != '') {
  62. r = '';
  63. } else if (typeof r === 'undefined') {
  64. r = '$';
  65. }
  66. if (typeof r === 'object') {
  67. return pre + TOKEN + JSON.stringify(r) + TOKEN;
  68. }
  69. return pre + r;
  70. }
  71. /**
  72. * @param {string} string
  73. * @param {Env} [env]
  74. * @param {{ escape?: string, splitUnquoted?: boolean | string }} [opts]
  75. * @returns {ParseEntry[]}
  76. */
  77. function parseInternal(string, env, opts) {
  78. if (!opts) {
  79. opts = {};
  80. }
  81. var BS = opts.escape || '\\';
  82. var ifs = opts.splitUnquoted === true ? ' \t\n' : (typeof opts.splitUnquoted === 'string' ? opts.splitUnquoted : '');
  83. var BAREWORD = '(\\' + BS + '[\'"' + META + ']|[^\\s\'"' + META + '])+';
  84. var chunker = new RegExp([
  85. '(' + CONTROL + ')', // control chars
  86. '(' + BAREWORD + '|' + DOUBLE_QUOTE + '|' + SINGLE_QUOTE + ')+'
  87. ].join('|'), 'g');
  88. var matches = matchAll(string, chunker);
  89. if (matches.length === 0) {
  90. return [];
  91. }
  92. if (!env) {
  93. env = {};
  94. }
  95. var commented = false;
  96. return matches.map(function (match) {
  97. var s = match[0];
  98. if (!s || commented) {
  99. return void undefined;
  100. }
  101. if (controlRE.test(s)) {
  102. return /** @type {ControlOperator} */ ({ op: s });
  103. }
  104. // Hand-written scanner/parser for Bash quoting rules:
  105. //
  106. // 1. inside single quotes, all characters are printed literally.
  107. // 2. inside double quotes, all characters are printed literally
  108. // except variables prefixed by '$' and backslashes followed by
  109. // either a double quote or another backslash.
  110. // 3. outside of any quotes, backslashes are treated as escape
  111. // characters and not printed (unless they are themselves escaped)
  112. // 4. quote context can switch mid-token if there is no whitespace
  113. // between the two quote contexts (e.g. all'one'"token" parses as
  114. // "allonetoken")
  115. /** @type {string | boolean} */
  116. var quote = false;
  117. var esc = false;
  118. var out = '';
  119. /** @type {string[]} */
  120. var words = [];
  121. var sawQuote = false;
  122. /** @type {number | null} */
  123. var pendingNw = null;
  124. var isGlob = false;
  125. /** @type {number} */
  126. var i;
  127. function parseEnvVar() {
  128. i += 1;
  129. /** @type {number | RegExpMatchArray | null} */
  130. var varend;
  131. /** @type {string} */
  132. var varname;
  133. var char = s.charAt(i);
  134. if (char === '{') {
  135. i += 1;
  136. if (s.charAt(i) === '}') {
  137. throw new Error('Bad substitution: ' + s.slice(i - 2, i + 1));
  138. }
  139. // match braces by depth so a nested `${` keeps its inner `}` from ending the outer substitution
  140. var depth = 1;
  141. varend = i;
  142. while (depth > 0 && varend < s.length) {
  143. if (s.charAt(varend) === '{' && s.charAt(varend - 1) === '$') {
  144. depth += 1;
  145. } else if (s.charAt(varend) === '}') {
  146. depth -= 1;
  147. }
  148. varend += 1;
  149. }
  150. if (depth !== 0) {
  151. throw new Error('Bad substitution: ' + s.slice(i));
  152. }
  153. varend -= 1;
  154. varname = s.slice(i, varend);
  155. i = varend;
  156. } else if ((/[*@#?$!_-]/).test(char)) {
  157. varname = char;
  158. i += 1;
  159. } else {
  160. var slicedFromI = s.slice(i);
  161. varend = slicedFromI.match(/[^\w\d_]/);
  162. if (!varend) {
  163. varname = slicedFromI;
  164. i = s.length;
  165. } else {
  166. varname = slicedFromI.slice(0, varend.index);
  167. i += /** @type {number} */ (varend.index) - 1;
  168. }
  169. }
  170. return getVar(/** @type {NonNullable<typeof env>} */ (env), '', varname);
  171. }
  172. function flushRun() {
  173. if (pendingNw === null) {
  174. return;
  175. }
  176. if (pendingNw === 0) {
  177. if (out !== '') {
  178. words[words.length] = out;
  179. out = '';
  180. }
  181. } else {
  182. words[words.length] = out;
  183. out = '';
  184. for (var fe = 1; fe < pendingNw; fe += 1) {
  185. words[words.length] = '';
  186. }
  187. }
  188. pendingNw = null;
  189. }
  190. for (i = 0; i < s.length; i++) {
  191. var c = s.charAt(i);
  192. if (ifs && c !== DS) {
  193. flushRun();
  194. }
  195. isGlob = isGlob || (!quote && (c === '*' || c === '?'));
  196. if (esc) {
  197. out += c;
  198. esc = false;
  199. } else if (quote) {
  200. if (c === quote) {
  201. quote = false;
  202. } else if (quote == SQ) {
  203. out += c;
  204. } else { // Double quote
  205. if (c === BS) {
  206. i += 1;
  207. c = s.charAt(i);
  208. if (c === DQ || c === BS || c === DS) {
  209. out += c;
  210. } else {
  211. out += BS + c;
  212. }
  213. } else if (c === DS) {
  214. out += parseEnvVar();
  215. } else {
  216. out += c;
  217. }
  218. }
  219. } else if (c === DQ || c === SQ) {
  220. quote = c;
  221. sawQuote = true;
  222. } else if (controlRE.test(c)) {
  223. return /** @type {ControlOperator} */ ({ op: s });
  224. } else if (hash.test(c)) {
  225. commented = true;
  226. var commentObj = { comment: string.slice(match.index + i + 1) };
  227. if (out.length) {
  228. return /** @type {const} */ ([out, commentObj]);
  229. }
  230. return /** @type {const} */ ([commentObj]);
  231. } else if (c === BS) {
  232. esc = true;
  233. } else if (c === DS) {
  234. var value = parseEnvVar();
  235. if (!ifs) {
  236. out += value;
  237. } else {
  238. for (var vi = 0; vi < value.length; vi += 1) {
  239. var vc = value.charAt(vi);
  240. if (ifs.indexOf(vc) < 0) {
  241. flushRun();
  242. out += vc;
  243. } else if (pendingNw === null) {
  244. pendingNw = vc === ' ' || vc === '\t' || vc === '\n' ? 0 : 1;
  245. } else if (vc !== ' ' && vc !== '\t' && vc !== '\n') {
  246. pendingNw += 1;
  247. }
  248. }
  249. }
  250. } else {
  251. out += c;
  252. }
  253. }
  254. if (isGlob) {
  255. return /** @type {GlobPattern} */ ({ op: 'glob', pattern: out });
  256. }
  257. if (ifs) {
  258. if (pendingNw !== null && pendingNw > 0) {
  259. words[words.length] = out;
  260. out = '';
  261. for (var te = 1; te < pendingNw; te += 1) {
  262. words[words.length] = '';
  263. }
  264. }
  265. if (out !== '' || (sawQuote && words.length === 0)) {
  266. words[words.length] = out;
  267. }
  268. return words;
  269. }
  270. return out;
  271. }).reduce(function (prev, arg) { // finalize parsed arguments
  272. if (typeof arg === 'undefined') {
  273. return prev;
  274. }
  275. /** @type {ParseEntry[]} */ ([]).concat(arg).forEach(function (entry) {
  276. prev[prev.length] = entry;
  277. });
  278. return prev;
  279. }, /** @type {ParseEntry[]} */ ([]));
  280. }
  281. /** @type {typeof import('./parse')} */
  282. module.exports = function parse(s, env, opts) {
  283. var mapped = parseInternal(s, env, opts);
  284. if (typeof env !== 'function') {
  285. return mapped;
  286. }
  287. return mapped.reduce(function (acc, s) {
  288. if (typeof s === 'object') {
  289. acc[acc.length] = s;
  290. return acc;
  291. }
  292. var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g'));
  293. if (xs.length === 1) {
  294. acc[acc.length] = xs[0];
  295. return acc;
  296. }
  297. xs.filter(Boolean).forEach(function (x) {
  298. acc[acc.length] = startsWithToken.test(x)
  299. ? JSON.parse(x.split(TOKEN)[1])
  300. : x;
  301. });
  302. return acc;
  303. }, /** @type {ParseEntry[]} */ ([]));
  304. };