diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc index a5a4284607bdc9c39b3f94f085d1c49168a9879a..611d9eff4fb066b2c2a387671b6996521e543fac 100644 --- a/CHANGELOG.adoc +++ b/CHANGELOG.adoc @@ -9,12 +9,15 @@ For a detailed view of what's changed, refer to the {url-repo}/commits[commit hi === Changed * exclude `env` property when exporting playbook to json or yaml (#32) +* replace forward slashes in command with backslashes when running on Windows (#33) +* only prepend `./` (*nix) or `.\` (Windows) to bare command (command without any directory segments) === Fixed * account for null value on env key or env entry * account for `on_failure` key name (snake_case) on run entry when using Antora 3.2.0 * preserve command with absolute path when run.local option is true (#34) +* resolve correct path ext (using where) when running command on Windows without shell (#33) == 1.0.0-beta.1 (2024-08-08) diff --git a/docs/modules/ROOT/pages/configuration-keys.adoc b/docs/modules/ROOT/pages/configuration-keys.adoc index ae3c5e6b36ff9bddf1523574c75defaffe431dfa..4801cc2a7b56d9eb08cc22c61f7a54d9f06f3915 100644 --- a/docs/modules/ROOT/pages/configuration-keys.adoc +++ b/docs/modules/ROOT/pages/configuration-keys.adoc @@ -212,6 +212,10 @@ If the `shell` key is set to `true` (default: `false`), then the command will be In this case, all arguments that contain reserved shell characters need to be quoted (e.g, `"console.log(1)"`). The command may also contain references to environment variables (including those defined on the command) (e.g., `$NODE`). +When the `shell` key is not `true` and the command has no file extension (e.g., `gradlew`), on Windows, Collector will search for a file that ends in _.exe_, _.bat_, or _.cmd_ (in that order). +If it finds a match, it will invoke that file instead. +This is the default behavior in Windows when running in a shell. + The value of the `command` key may include context variable expressions. The `env` context variable includes all environment variables added by the `env` key. diff --git a/packages/collector-extension/lib/util/run-command.js b/packages/collector-extension/lib/util/run-command.js index 74baf42603aeb8188f9239d943475611d0f74f45..ae604af024bb70ba6d68aa6f5e93413b54ddc809 100644 --- a/packages/collector-extension/lib/util/run-command.js +++ b/packages/collector-extension/lib/util/run-command.js @@ -4,7 +4,7 @@ const fs = require('node:fs') const { promises: fsp } = fs const ospath = require('node:path') const parseCommand = require('./parse-command') -const { spawn } = require('node:child_process') +const { spawn, execFile } = require('node:child_process') const invariably = { true: () => true, false: () => false } @@ -12,25 +12,49 @@ const IS_WIN = process.platform === 'win32' const ENV_NAME_RX = /\$(\w+)/g async function runCommand (cmd = '', opts = {}) { - let cmdv = parseCommand(String(cmd), { preserveQuotes: opts.shell }) + const shell = opts.shell + let cmdv = parseCommand(String(cmd), { preserveQuotes: shell }) if (!cmdv.length) throw new TypeError('Command not specified') - const cmd0 = cmdv[0] + let cmd0 = cmdv[0] const { quiet, local, ...spawnOpts } = opts if (IS_WIN) { - if (local) { - if (!(cmd0.endsWith('.bat') || cmd0.endsWith('.cmd'))) { - const absCmd0 = ospath.isAbsolute(cmd0) ? ospath.normalize(cmd0) : ospath.join(opts.cwd || '', cmd0) - if (await fsp.access(absCmd0 + '.bat').then(invariably.true, invariably.false)) { - cmdv[0] = absCmd0 + '.bat' - } else if (await fsp.access(absCmd0 + '.cmd').then(invariably.true, invariably.false)) { - cmdv[0] = absCmd0 + '.cmd' + if (~cmd0.indexOf('/')) cmdv[0] = cmd0 = cmd0.replace(/[/]/g, ospath.sep) + if (shell) { + cmdv = cmdv.map((it) => (~it.indexOf('$') ? it.replace(ENV_NAME_RX, '%$1%') : it)) + } else { + const bare = !~cmd0.indexOf(ospath.sep) + if (ospath.extname(cmd)) { + if (bare && local) cmdv[0] = '.' + ospath.sep + cmd0 + } else { + if (bare && !local) { + cmdv[0] = await new Promise((resolve) => { + execFile('where', [cmd0 + '.???'], { cwd: opts.cwd, windowsHide: true }, (_, results) => { + if (!(results = results.trimEnd())) return resolve(cmd0) + const exts = results.split('\r\n', 2).reduce((accum, it) => { + const ext = ospath.extname(it) + if (ext) accum[ext.slice(1)] = ext + return accum + }, {}) + resolve(cmd0 + (exts.exe || exts.bat || exts.cmd || '')) + }) + }) + } else { + let absCmd0 = cmd0 + if (!ospath.isAbsolute(absCmd0)) { + absCmd0 = ospath.join(opts.cwd || '', cmd0) + if (bare) cmdv[0] = cmd0 = '.' + ospath.sep + cmd0 + } + for (const resolvedExt of ['.exe', '.bat', '.cmd']) { + if (!(await fsp.access(absCmd0 + resolvedExt).then(invariably.true, invariably.false))) continue + cmdv[0] = cmd0 + resolvedExt + break + } } } } - if (opts.shell) cmdv = cmdv.map((it) => (~it.indexOf('$') ? it.replace(ENV_NAME_RX, '%$1%') : it)) Object.assign(spawnOpts, { windowsHide: true, windowsVerbatimArguments: false }) } else if (local && !~cmd0.indexOf('/')) { - cmdv[0] = `./${cmd0}` + cmdv[0] = './' + cmd0 } return new Promise((resolve, reject) => { const stderr = [] diff --git a/packages/collector-extension/test/collector-extension-test.js b/packages/collector-extension/test/collector-extension-test.js index 8656b39b3c48872d8e9c8d27cbf40fcd17220ac8..c6a8cf95556cd2a98ca7d0c1eb3e2e9ea9bb7837 100644 --- a/packages/collector-extension/test/collector-extension-test.js +++ b/packages/collector-extension/test/collector-extension-test.js @@ -655,7 +655,7 @@ describe('collector extension', () => { }) }) - it('should run specified local executable command if command begins with ./', async () => { + it('should run specified local command if command begins with ./', async () => { const collectorConfig = { run: { command: './.gen-start-page' }, scan: { dir: 'build' }, @@ -670,7 +670,7 @@ describe('collector extension', () => { }) }) - it('should run specified local executable command if local option is true', async () => { + it('should run specified local command if local option is true', async () => { const collectorConfig = { run: { command: '.gen-start-page', local: true }, scan: { dir: 'build' }, @@ -685,9 +685,9 @@ describe('collector extension', () => { }) }) - it('should run specified local executable command in parent directory', async () => { + it('should run specified local command in parent directory', async () => { const collectorConfig = { - run: { command: '../.gen-docs-start-page', local: true, dir: '.' }, + run: { command: '../.gen-docs-start-page', dir: '.' }, scan: { dir: 'build/docs' }, } await runScenario({ @@ -701,7 +701,7 @@ describe('collector extension', () => { }) }) - it('should run specified local executable command with absolute path if local option is true', async () => { + it('should run specified command with absolute path even if local option is true', async () => { const collectorConfig = { run: { command: '$' + '{{origin.collectorWorktree}}/.gen-start-page', local: true }, scan: { dir: 'build' }, @@ -1385,6 +1385,21 @@ describe('collector extension', () => { }) }) + it('should allow local command with spaces to be run in default shell', async () => { + const collectorConfig = { + run: { command: '".gen start page"', local: true, shell: true }, + scan: { dir: 'build' }, + } + await runScenario({ + repoName: 'test-at-root', + collectorConfig, + after: (contentAggregate) => { + expect(contentAggregate[0].files).to.have.lengthOf(1) + expect(contentAggregate[0].files[0].path).to.equal('modules/ROOT/pages/index.adoc') + }, + }) + }) + it('should set NODE environment variable even if value of env key has no entries', async () => { const node = process.env.NODE try { @@ -1856,6 +1871,17 @@ describe('collector extension', () => { ) }) + it('should throw error if local command is not found', async () => { + const collectorConfig = { run: { command: 'no-such-command', local: true } } + const expectedMessage = + `(@antora/collector-extension): Command not found: .${ospath.sep}no-such-command in ` + + `http://localhost:${gitServerPort}/test-at-root/.git (branch: main)` + expect(await trapAsyncError(() => runScenario({ repoName: 'test-at-root', collectorConfig }))).to.throw( + Error, + expectedMessage + ) + }) + it('should throw error if command is not found for origin with start path', async () => { const collectorConfig = { run: { command: 'no-such-command' } } const expectedMessage = diff --git a/packages/collector-extension/test/fixtures/test-at-root/.gen start page b/packages/collector-extension/test/fixtures/test-at-root/.gen start page new file mode 100755 index 0000000000000000000000000000000000000000..20b14c7d9ad5b8cf18f88d823268bbafe3e19dac --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-at-root/.gen start page @@ -0,0 +1,3 @@ +#!/bin/bash + +"$NODE" .gen-start-page.js diff --git a/packages/collector-extension/test/fixtures/test-at-root/.gen start page.cmd b/packages/collector-extension/test/fixtures/test-at-root/.gen start page.cmd new file mode 100644 index 0000000000000000000000000000000000000000..d6a99add3c176d9b0e2ede3358ff192eaa39e210 --- /dev/null +++ b/packages/collector-extension/test/fixtures/test-at-root/.gen start page.cmd @@ -0,0 +1,3 @@ +@echo off + +"%NODE%" .gen-start-page.js diff --git a/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml b/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml index e98bbb72c336dba89403bf7f13ebea968f7a59f4..32d2defc0779676bb818835b410b433f1cc3f103 100644 --- a/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml +++ b/packages/collector-extension/test/fixtures/test-batch-script/docs/antora.yml @@ -3,6 +3,5 @@ version: '${project.version}' ext: collector: run: - command: mvnw process-resources - local: true + command: ./mvnw process-resources scan: target/docs