diff --git a/dist/action.mjs b/dist/action.mjs index eb9d4d5..57061eb 100644 --- a/dist/action.mjs +++ b/dist/action.mjs @@ -80,6 +80,279 @@ function buildProject(context) { }); } +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; +} + +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 = []; @@ -101,10 +374,7 @@ function getContext() { } input = getInput("options"); if (input) { - const opts = input.split(/\s+/).filter((arg) => arg != ""); - for (const opt of opts) { - options.push(opt); - } + options.push(...shellQuoteExports.parse(input).map((opt) => opt.toString())); } return { sourceDir, @@ -112,15 +382,11 @@ function getContext() { configure: { generator: getInput("generator"), options, - args: getInput("args") - .split(/\s+/) - .filter((arg) => arg != ""), + args: shellQuoteExports.parse(getInput("args")).map((arg) => arg.toString()), }, build: { enabled: getInput("run-build") == "true", - args: getInput("build-args") - .split(/\s+/) - .filter((arg) => arg != ""), + args: shellQuoteExports.parse(getInput("build-args")).map((arg) => arg.toString()), }, }; } diff --git a/package.json b/package.json index eafc327..4dd46b6 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,18 @@ "test": "jest" }, "dependencies": { - "gha-utils": "^0.4.1" + "gha-utils": "^0.4.1", + "shell-quote": "^1.8.1" }, "devDependencies": { "@eslint/js": "^9.15.0", "@jest/globals": "^29.7.0", + "@rollup/plugin-commonjs": "^28.0.1", "@rollup/plugin-node-resolve": "^15.3.0", "@rollup/plugin-typescript": "^12.1.1", "@types/jest": "^29.5.14", "@types/node": "^22.9.0", + "@types/shell-quote": "^1", "eslint": "^9.14.0", "jest": "^29.7.0", "prettier": "^3.3.3", diff --git a/rollup.config.js b/rollup.config.js index 5119846..8ba4836 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,3 +1,4 @@ +import commonjs from "@rollup/plugin-commonjs"; import { nodeResolve } from "@rollup/plugin-node-resolve"; import typescript from "@rollup/plugin-typescript"; @@ -7,5 +8,5 @@ export default { dir: "dist", entryFileNames: "[name].mjs", }, - plugins: [nodeResolve(), typescript()], + plugins: [commonjs(), nodeResolve(), typescript()], }; diff --git a/src/context.test.ts b/src/context.test.ts index 918fc7d..163a8c4 100644 --- a/src/context.test.ts +++ b/src/context.test.ts @@ -99,24 +99,29 @@ describe("get action context", () => { { name: "with additional options specified", inputs: { - options: "BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON", + options: `BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON FOO="BAR BAZ"`, }, expectedContext: { configure: { generator: "", - options: ["BUILD_TESTING=ON", "BUILD_EXAMPLES=ON", "BUILD_DOCS=ON"], + options: [ + "BUILD_TESTING=ON", + "BUILD_EXAMPLES=ON", + "BUILD_DOCS=ON", + "FOO=BAR BAZ", + ], args: [], }, }, }, { name: "with additional arguments specified", - inputs: { args: "-Wdev -Wdeprecated\n--fresh" }, + inputs: { args: `-Wdev -Wdeprecated\n--fresh --foo "bar baz"` }, expectedContext: { configure: { generator: "", options: [], - args: ["-Wdev", "-Wdeprecated", "--fresh"], + args: ["-Wdev", "-Wdeprecated", "--fresh", "--foo", "bar baz"], }, }, }, @@ -127,11 +132,11 @@ describe("get action context", () => { }, { name: "with additional build arguments specified", - inputs: { "build-args": "--target foo\n--parallel 8" }, + inputs: { "build-args": `--target foo\n--parallel 8 --foo "bar baz"` }, expectedContext: { build: { enabled: false, - args: ["--target", "foo", "--parallel", "8"], + args: ["--target", "foo", "--parallel", "8", "--foo", "bar baz"], }, }, }, @@ -145,10 +150,10 @@ describe("get action context", () => { "cxx-compiler": "clang++", "c-flags": "-Werror -Wall\n-Wextra", "cxx-flags": "-Werror -Wall\n-Wextra -Wpedantic", - options: "BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON", - args: "-Wdev -Wdeprecated\n--fresh", + options: `BUILD_TESTING=ON BUILD_EXAMPLES=ON\nBUILD_DOCS=ON FOO="BAR BAZ"`, + args: `-Wdev -Wdeprecated\n--fresh --foo "bar baz"`, "run-build": "true", - "build-args": "--target foo\n--parallel 8", + "build-args": `--target foo\n--parallel 8 --foo "bar baz"`, }, expectedContext: { sourceDir: "project", @@ -163,12 +168,13 @@ describe("get action context", () => { "BUILD_TESTING=ON", "BUILD_EXAMPLES=ON", "BUILD_DOCS=ON", + "FOO=BAR BAZ", ], - args: ["-Wdev", "-Wdeprecated", "--fresh"], + args: ["-Wdev", "-Wdeprecated", "--fresh", "--foo", "bar baz"], }, build: { enabled: true, - args: ["--target", "foo", "--parallel", "8"], + args: ["--target", "foo", "--parallel", "8", "--foo", "bar baz"], }, }, }, diff --git a/src/context.ts b/src/context.ts index c8a20b4..1415418 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,5 +1,6 @@ import { getInput } from "gha-utils"; import path from "node:path"; +import { parse } from "shell-quote"; export interface Context { sourceDir: string; @@ -39,10 +40,7 @@ export function getContext(): Context { input = getInput("options"); if (input) { - const opts = input.split(/\s+/).filter((arg) => arg != ""); - for (const opt of opts) { - options.push(opt); - } + options.push(...parse(input).map((opt) => opt.toString())); } return { @@ -51,15 +49,11 @@ export function getContext(): Context { configure: { generator: getInput("generator"), options, - args: getInput("args") - .split(/\s+/) - .filter((arg) => arg != ""), + args: parse(getInput("args")).map((arg) => arg.toString()), }, build: { enabled: getInput("run-build") == "true", - args: getInput("build-args") - .split(/\s+/) - .filter((arg) => arg != ""), + args: parse(getInput("build-args")).map((arg) => arg.toString()), }, }; } diff --git a/yarn.lock b/yarn.lock index 428508b..300d063 100644 --- a/yarn.lock +++ b/yarn.lock @@ -839,6 +839,13 @@ __metadata: languageName: node linkType: hard +"@jridgewell/sourcemap-codec@npm:^1.5.0": + version: 1.5.0 + resolution: "@jridgewell/sourcemap-codec@npm:1.5.0" + checksum: 10c0/2eb864f276eb1096c3c11da3e9bb518f6d9fc0023c78344cdc037abadc725172c70314bdb360f2d4b7bffec7f5d657ce006816bc5d4ecb35e61b66132db00c18 + languageName: node + linkType: hard + "@jridgewell/trace-mapping@npm:^0.3.12, @jridgewell/trace-mapping@npm:^0.3.17, @jridgewell/trace-mapping@npm:^0.3.18, @jridgewell/trace-mapping@npm:^0.3.24": version: 0.3.25 resolution: "@jridgewell/trace-mapping@npm:0.3.25" @@ -905,6 +912,26 @@ __metadata: languageName: node linkType: hard +"@rollup/plugin-commonjs@npm:^28.0.1": + version: 28.0.1 + resolution: "@rollup/plugin-commonjs@npm:28.0.1" + dependencies: + "@rollup/pluginutils": "npm:^5.0.1" + commondir: "npm:^1.0.1" + estree-walker: "npm:^2.0.2" + fdir: "npm:^6.2.0" + is-reference: "npm:1.2.1" + magic-string: "npm:^0.30.3" + picomatch: "npm:^4.0.2" + peerDependencies: + rollup: ^2.68.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + checksum: 10c0/15d73306f539763a4b0d5723a0be9099b56d07118ff12b4c7f4c04b26e762076706e9f88a45f131d639ed9b7bd52e51facf93f2ca265b994172677b48ca705fe + languageName: node + linkType: hard + "@rollup/plugin-node-resolve@npm:^15.3.0": version: 15.3.0 resolution: "@rollup/plugin-node-resolve@npm:15.3.0" @@ -1150,7 +1177,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.6, @types/estree@npm:^1.0.6": +"@types/estree@npm:*, @types/estree@npm:1.0.6, @types/estree@npm:^1.0.6": version: 1.0.6 resolution: "@types/estree@npm:1.0.6" checksum: 10c0/cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a @@ -1240,6 +1267,13 @@ __metadata: languageName: node linkType: hard +"@types/shell-quote@npm:^1": + version: 1.7.5 + resolution: "@types/shell-quote@npm:1.7.5" + checksum: 10c0/ddcf225e85e5520e3f44411d7d79eee0e56477fab705d0d93e293b61b9f8de2a57db6e859d492a24bc9e0d071c0490271efeae832756e2ac0d4d255922ac281d + languageName: node + linkType: hard + "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -1843,6 +1877,13 @@ __metadata: languageName: node linkType: hard +"commondir@npm:^1.0.1": + version: 1.0.1 + resolution: "commondir@npm:1.0.1" + checksum: 10c0/33a124960e471c25ee19280c9ce31ccc19574b566dc514fe4f4ca4c34fa8b0b57cf437671f5de380e11353ea9426213fca17687dd2ef03134fea2dbc53809fd6 + languageName: node + linkType: hard + "concat-map@npm:0.0.1": version: 0.0.1 resolution: "concat-map@npm:0.0.1" @@ -2291,6 +2332,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.2.0": + version: 6.4.2 + resolution: "fdir@npm:6.4.2" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/34829886f34a3ca4170eca7c7180ec4de51a3abb4d380344063c0ae2e289b11d2ba8b724afee974598c83027fea363ff598caf2b51bc4e6b1e0d8b80cc530573 + languageName: node + linkType: hard + "file-entry-cache@npm:^8.0.0": version: 8.0.0 resolution: "file-entry-cache@npm:8.0.0" @@ -2736,6 +2789,15 @@ __metadata: languageName: node linkType: hard +"is-reference@npm:1.2.1": + version: 1.2.1 + resolution: "is-reference@npm:1.2.1" + dependencies: + "@types/estree": "npm:*" + checksum: 10c0/7dc819fc8de7790264a0a5d531164f9f5b9ef5aa1cd05f35322d14db39c8a2ec78fd5d4bf57f9789f3ddd2b3abeea7728432b759636157a42db12a9e8c3b549b + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -3468,6 +3530,15 @@ __metadata: languageName: node linkType: hard +"magic-string@npm:^0.30.3": + version: 0.30.13 + resolution: "magic-string@npm:0.30.13" + dependencies: + "@jridgewell/sourcemap-codec": "npm:^1.5.0" + checksum: 10c0/a275faeca1564c545019b4742c38a42ca80226c8c9e0805c32d1a1cc58b0e6ff7bbd914ed885fd10043858a7da0f732cb8f49c8975c3ecebde9cad4b57db5115 + languageName: node + linkType: hard + "make-dir@npm:^4.0.0": version: 4.0.0 resolution: "make-dir@npm:4.0.0" @@ -3911,6 +3982,13 @@ __metadata: languageName: node linkType: hard +"picomatch@npm:^4.0.2": + version: 4.0.2 + resolution: "picomatch@npm:4.0.2" + checksum: 10c0/7c51f3ad2bb42c776f49ebf964c644958158be30d0a510efd5a395e8d49cb5acfed5b82c0c5b365523ce18e6ab85013c9ebe574f60305892ec3fa8eee8304ccc + languageName: node + linkType: hard + "pirates@npm:^4.0.4": version: 4.0.6 resolution: "pirates@npm:4.0.6" @@ -4161,15 +4239,18 @@ __metadata: dependencies: "@eslint/js": "npm:^9.15.0" "@jest/globals": "npm:^29.7.0" + "@rollup/plugin-commonjs": "npm:^28.0.1" "@rollup/plugin-node-resolve": "npm:^15.3.0" "@rollup/plugin-typescript": "npm:^12.1.1" "@types/jest": "npm:^29.5.14" "@types/node": "npm:^22.9.0" + "@types/shell-quote": "npm:^1" eslint: "npm:^9.14.0" gha-utils: "npm:^0.4.1" jest: "npm:^29.7.0" prettier: "npm:^3.3.3" rollup: "npm:^4.26.0" + shell-quote: "npm:^1.8.1" ts-jest: "npm:^29.2.5" tslib: "npm:^2.8.1" typescript: "npm:^5.6.3" @@ -4238,6 +4319,13 @@ __metadata: languageName: node linkType: hard +"shell-quote@npm:^1.8.1": + version: 1.8.1 + resolution: "shell-quote@npm:1.8.1" + checksum: 10c0/8cec6fd827bad74d0a49347057d40dfea1e01f12a6123bf82c4649f3ef152fc2bc6d6176e6376bffcd205d9d0ccb4f1f9acae889384d20baff92186f01ea455a + languageName: node + linkType: hard + "signal-exit@npm:^3.0.3, signal-exit@npm:^3.0.7": version: 3.0.7 resolution: "signal-exit@npm:3.0.7"