From 825671e6f7552420a553e21c140acb9fc4888da5 Mon Sep 17 00:00:00 2001 From: Alfi Maulana Date: Tue, 26 Nov 2024 14:10:04 +0700 Subject: [PATCH] feat: add `parse` function Signed-off-by: Alfi Maulana --- dist/action.mjs | 293 ++++-------------------------------------------- src/context.ts | 2 +- src/utils.ts | 18 +++ 3 files changed, 38 insertions(+), 275 deletions(-) create mode 100644 src/utils.ts diff --git a/dist/action.mjs b/dist/action.mjs index f639502..e98a1ee 100644 --- a/dist/action.mjs +++ b/dist/action.mjs @@ -123,279 +123,24 @@ async function buildProject(context) { await exec("cmake", ["--build", context.buildDir, ...context.build.args]); } -var shellQuote = {}; - -var quote; -var hasRequiredQuote; - -function requireQuote () { - if (hasRequiredQuote) return quote; - hasRequiredQuote = 1; - - quote = function quote(xs) { - return xs.map(function (s) { - if (s && typeof s === 'object') { - return s.op.replace(/(.)/g, '\\$1'); - } - if ((/["\s]/).test(s) && !(/'/).test(s)) { - return "'" + s.replace(/(['\\])/g, '\\$1') + "'"; - } - if ((/["'\s]/).test(s)) { - return '"' + s.replace(/(["\\$`!])/g, '\\$1') + '"'; - } - return String(s).replace(/([A-Za-z]:)?([#!"$&'()*,:;<=>?@[\\\]^`{|}])/g, '$1\\$2'); - }).join(' '); - }; - return quote; +const regex = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g; +/** + * Converts a space-separated string into a list of arguments. + * + * This function parses the provided string, which contains arguments separated by spaces and possibly enclosed in quotes, into a list of arguments. + * + * @param str - The space-separated string to parse. + * @returns A list of arguments. + */ +function parse(str) { + const args = []; + let match; + while ((match = regex.exec(str)) !== null) { + args.push(match[1] ?? match[2] ?? match[3] ?? match[4]); + } + return args; } -var parse; -var hasRequiredParse; - -function requireParse () { - if (hasRequiredParse) return parse; - hasRequiredParse = 1; - - // '<(' is process substitution operator and - // can be parsed the same as control operator - var CONTROL = '(?:' + [ - '\\|\\|', - '\\&\\&', - ';;', - '\\|\\&', - '\\<\\(', - '\\<\\<\\<', - '>>', - '>\\&', - '<\\&', - '[&;()|<>]' - ].join('|') + ')'; - var controlRE = new RegExp('^' + CONTROL + '$'); - var META = '|&;()<> \\t'; - var SINGLE_QUOTE = '"((\\\\"|[^"])*?)"'; - var DOUBLE_QUOTE = '\'((\\\\\'|[^\'])*?)\''; - var hash = /^#$/; - - var SQ = "'"; - var DQ = '"'; - var DS = '$'; - - var TOKEN = ''; - var mult = 0x100000000; // Math.pow(16, 8); - for (var i = 0; i < 4; i++) { - TOKEN += (mult * Math.random()).toString(16); - } - var startsWithToken = new RegExp('^' + TOKEN); - - function matchAll(s, r) { - var origIndex = r.lastIndex; - - var matches = []; - var matchObj; - - while ((matchObj = r.exec(s))) { - matches.push(matchObj); - if (r.lastIndex === matchObj.index) { - r.lastIndex += 1; - } - } - - r.lastIndex = origIndex; - - return matches; - } - - function getVar(env, pre, key) { - var r = typeof env === 'function' ? env(key) : env[key]; - if (typeof r === 'undefined' && key != '') { - r = ''; - } else if (typeof r === 'undefined') { - r = '$'; - } - - if (typeof r === 'object') { - return pre + TOKEN + JSON.stringify(r) + TOKEN; - } - return pre + r; - } - - function parseInternal(string, env, opts) { - if (!opts) { - opts = {}; - } - var BS = opts.escape || '\\'; - var BAREWORD = '(\\' + BS + '[\'"' + META + ']|[^\\s\'"' + META + '])+'; - - var chunker = new RegExp([ - '(' + CONTROL + ')', // control chars - '(' + BAREWORD + '|' + SINGLE_QUOTE + '|' + DOUBLE_QUOTE + ')+' - ].join('|'), 'g'); - - var matches = matchAll(string, chunker); - - if (matches.length === 0) { - return []; - } - if (!env) { - env = {}; - } - - var commented = false; - - return matches.map(function (match) { - var s = match[0]; - if (!s || commented) { - return void undefined; - } - if (controlRE.test(s)) { - return { op: s }; - } - - // Hand-written scanner/parser for Bash quoting rules: - // - // 1. inside single quotes, all characters are printed literally. - // 2. inside double quotes, all characters are printed literally - // except variables prefixed by '$' and backslashes followed by - // either a double quote or another backslash. - // 3. outside of any quotes, backslashes are treated as escape - // characters and not printed (unless they are themselves escaped) - // 4. quote context can switch mid-token if there is no whitespace - // between the two quote contexts (e.g. all'one'"token" parses as - // "allonetoken") - var quote = false; - var esc = false; - var out = ''; - var isGlob = false; - var i; - - function parseEnvVar() { - i += 1; - var varend; - var varname; - var char = s.charAt(i); - - if (char === '{') { - i += 1; - if (s.charAt(i) === '}') { - throw new Error('Bad substitution: ' + s.slice(i - 2, i + 1)); - } - varend = s.indexOf('}', i); - if (varend < 0) { - throw new Error('Bad substitution: ' + s.slice(i)); - } - varname = s.slice(i, varend); - i = varend; - } else if ((/[*@#?$!_-]/).test(char)) { - varname = char; - i += 1; - } else { - var slicedFromI = s.slice(i); - varend = slicedFromI.match(/[^\w\d_]/); - if (!varend) { - varname = slicedFromI; - i = s.length; - } else { - varname = slicedFromI.slice(0, varend.index); - i += varend.index - 1; - } - } - return getVar(env, '', varname); - } - - for (i = 0; i < s.length; i++) { - var c = s.charAt(i); - isGlob = isGlob || (!quote && (c === '*' || c === '?')); - if (esc) { - out += c; - esc = false; - } else if (quote) { - if (c === quote) { - quote = false; - } else if (quote == SQ) { - out += c; - } else { // Double quote - if (c === BS) { - i += 1; - c = s.charAt(i); - if (c === DQ || c === BS || c === DS) { - out += c; - } else { - out += BS + c; - } - } else if (c === DS) { - out += parseEnvVar(); - } else { - out += c; - } - } - } else if (c === DQ || c === SQ) { - quote = c; - } else if (controlRE.test(c)) { - return { op: s }; - } else if (hash.test(c)) { - commented = true; - var commentObj = { comment: string.slice(match.index + i + 1) }; - if (out.length) { - return [out, commentObj]; - } - return [commentObj]; - } else if (c === BS) { - esc = true; - } else if (c === DS) { - out += parseEnvVar(); - } else { - out += c; - } - } - - if (isGlob) { - return { op: 'glob', pattern: out }; - } - - return out; - }).reduce(function (prev, arg) { // finalize parsed arguments - // TODO: replace this whole reduce with a concat - return typeof arg === 'undefined' ? prev : prev.concat(arg); - }, []); - } - - parse = function parse(s, env, opts) { - var mapped = parseInternal(s, env, opts); - if (typeof env !== 'function') { - return mapped; - } - return mapped.reduce(function (acc, s) { - if (typeof s === 'object') { - return acc.concat(s); - } - var xs = s.split(RegExp('(' + TOKEN + '.*?' + TOKEN + ')', 'g')); - if (xs.length === 1) { - return acc.concat(xs[0]); - } - return acc.concat(xs.filter(Boolean).map(function (x) { - if (startsWithToken.test(x)) { - return JSON.parse(x.split(TOKEN)[1]); - } - return x; - })); - }, []); - }; - return parse; -} - -var hasRequiredShellQuote; - -function requireShellQuote () { - if (hasRequiredShellQuote) return shellQuote; - hasRequiredShellQuote = 1; - - shellQuote.quote = requireQuote(); - shellQuote.parse = requireParse(); - return shellQuote; -} - -var shellQuoteExports = requireShellQuote(); - function getContext() { const sourceDir = getInput("source-dir"); const options = []; @@ -417,7 +162,7 @@ function getContext() { } input = getInput("options"); if (input) { - options.push(...shellQuoteExports.parse(input).map((opt) => opt.toString())); + options.push(...parse(input).map((opt) => opt.toString())); } return { sourceDir, @@ -425,11 +170,11 @@ function getContext() { configure: { generator: getInput("generator"), options, - args: shellQuoteExports.parse(getInput("args")).map((arg) => arg.toString()), + args: parse(getInput("args")).map((arg) => arg.toString()), }, build: { enabled: getInput("run-build") == "true", - args: shellQuoteExports.parse(getInput("build-args")).map((arg) => arg.toString()), + args: parse(getInput("build-args")).map((arg) => arg.toString()), }, }; } diff --git a/src/context.ts b/src/context.ts index 1415418..8d563ec 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,6 +1,6 @@ import { getInput } from "gha-utils"; import path from "node:path"; -import { parse } from "shell-quote"; +import { parse } from "./utils.js"; export interface Context { sourceDir: string; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..efa4463 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,18 @@ +const regex = /"([^"]*)"|'([^']*)'|`([^`]*)`|(\S+)/g; + +/** + * Converts a space-separated string into a list of arguments. + * + * This function parses the provided string, which contains arguments separated by spaces and possibly enclosed in quotes, into a list of arguments. + * + * @param str - The space-separated string to parse. + * @returns A list of arguments. + */ +export function parse(str: string): string[] { + const args: string[] = []; + let match: RegExpExecArray | null; + while ((match = regex.exec(str)) !== null) { + args.push(match[1] ?? match[2] ?? match[3] ?? match[4]); + } + return args; +}