From bd9017e99fb184a9116967add54765c14366649f Mon Sep 17 00:00:00 2001 From: Sora Morimoto Date: Sat, 3 Apr 2021 01:22:30 +0900 Subject: [PATCH] Add findInPath method to locate all matching executables in the system path (#609) Signed-off-by: Sora Morimoto --- packages/io/RELEASES.md | 6 +- packages/io/__tests__/io.test.ts | 63 +++++++++++++++ packages/io/package-lock.json | 2 +- packages/io/package.json | 2 +- packages/io/src/io.ts | 134 +++++++++++++++++-------------- 5 files changed, 145 insertions(+), 62 deletions(-) diff --git a/packages/io/RELEASES.md b/packages/io/RELEASES.md index be3f1ad1..fe469f33 100644 --- a/packages/io/RELEASES.md +++ b/packages/io/RELEASES.md @@ -1,9 +1,13 @@ # @actions/io Releases +### 1.1.0 + +- Add `findInPath` method to locate all matching executables in the system path + ### 1.0.2 - [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221) ### 1.0.0 -- Initial release \ No newline at end of file +- Initial release diff --git a/packages/io/__tests__/io.test.ts b/packages/io/__tests__/io.test.ts index 76d07f5b..9d42465b 100644 --- a/packages/io/__tests__/io.test.ts +++ b/packages/io/__tests__/io.test.ts @@ -6,6 +6,10 @@ import * as io from '../src/io' import * as ioUtil from '../src/io-util' describe('cp', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) + it('copies file with no flags', async () => { const root = path.join(getTestTemp(), 'cp_with_no_flags') const sourceFile = path.join(root, 'cp_source') @@ -166,6 +170,10 @@ describe('cp', () => { }) describe('mv', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) + it('moves file with no flags', async () => { const root = path.join(getTestTemp(), ' mv_with_no_flags') const sourceFile = path.join(root, ' mv_source') @@ -264,6 +272,10 @@ describe('mv', () => { }) describe('rmRF', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) + it('removes single folder with rmRF', async () => { const testPath = path.join(getTestTemp(), 'testFolder') @@ -841,6 +853,10 @@ describe('mkdirP', () => { }) describe('which', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) + it('which() finds file name', async () => { // create a executable file const testPath = path.join(getTestTemp(), 'which-finds-file-name') @@ -1373,6 +1389,53 @@ describe('which', () => { } }) +describe('findInPath', () => { + beforeAll(async () => { + await io.rmRF(getTestTemp()) + }) + + it('findInPath() not found', async () => { + expect(await io.findInPath('findInPath-test-no-such-file')).toEqual([]) + }) + + it('findInPath() finds file names', async () => { + // create executable files + let fileName = 'FindInPath-Test-File' + if (process.platform === 'win32') { + fileName += '.exe' + } + + const testPaths = ['1', '2', '3'].map(count => + path.join(getTestTemp(), `findInPath-finds-file-names-${count}`) + ) + for (const testPath of testPaths) { + await io.mkdirP(testPath) + } + + const filePaths = testPaths.map(testPath => path.join(testPath, fileName)) + for (const filePath of filePaths) { + await fs.writeFile(filePath, '') + if (process.platform !== 'win32') { + chmod(filePath, '+x') + } + } + + const originalPath = process.env['PATH'] + try { + // update the PATH + for (const testPath of testPaths) { + process.env[ + 'PATH' + ] = `${process.env['PATH']}${path.delimiter}${testPath}` + } + // exact file names + expect(await io.findInPath(fileName)).toEqual(filePaths) + } finally { + process.env['PATH'] = originalPath + } + }) +}) + async function findsExecutableWithScopedPermissions( chmodOptions: string ): Promise { diff --git a/packages/io/package-lock.json b/packages/io/package-lock.json index c115e2de..db0ed97b 100644 --- a/packages/io/package-lock.json +++ b/packages/io/package-lock.json @@ -1,5 +1,5 @@ { "name": "@actions/io", - "version": "1.0.2", + "version": "1.1.0", "lockfileVersion": 1 } diff --git a/packages/io/package.json b/packages/io/package.json index 9ef283f8..db165edc 100644 --- a/packages/io/package.json +++ b/packages/io/package.json @@ -1,6 +1,6 @@ { "name": "@actions/io", - "version": "1.0.2", + "version": "1.1.0", "description": "Actions io lib", "keywords": [ "github", diff --git a/packages/io/src/io.ts b/packages/io/src/io.ts index d6649945..f97eb067 100644 --- a/packages/io/src/io.ts +++ b/packages/io/src/io.ts @@ -192,69 +192,85 @@ export async function which(tool: string, check?: boolean): Promise { ) } } + + return result } - try { - // build the list of extensions to try - const extensions: string[] = [] - if (ioUtil.IS_WINDOWS && process.env.PATHEXT) { - for (const extension of process.env.PATHEXT.split(path.delimiter)) { - if (extension) { - extensions.push(extension) - } - } - } + const matches: string[] = await findInPath(tool) - // if it's rooted, return it if exists. otherwise return empty. - if (ioUtil.isRooted(tool)) { - const filePath: string = await ioUtil.tryGetExecutablePath( - tool, - extensions - ) - - if (filePath) { - return filePath - } - - return '' - } - - // if any path separators, return empty - if (tool.includes('/') || (ioUtil.IS_WINDOWS && tool.includes('\\'))) { - return '' - } - - // build the list of directories - // - // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, - // it feels like we should not do this. Checking the current directory seems like more of a use - // case of a shell, and the which() function exposed by the toolkit should strive for consistency - // across platforms. - const directories: string[] = [] - - if (process.env.PATH) { - for (const p of process.env.PATH.split(path.delimiter)) { - if (p) { - directories.push(p) - } - } - } - - // return the first match - for (const directory of directories) { - const filePath = await ioUtil.tryGetExecutablePath( - directory + path.sep + tool, - extensions - ) - if (filePath) { - return filePath - } - } - - return '' - } catch (err) { - throw new Error(`which failed with message ${err.message}`) + if (matches && matches.length > 0) { + return matches[0] } + + return '' +} + +/** + * Returns a list of all occurrences of the given tool on the system path. + * + * @returns Promise the paths of the tool + */ +export async function findInPath(tool: string): Promise { + if (!tool) { + throw new Error("parameter 'tool' is required") + } + + // build the list of extensions to try + const extensions: string[] = [] + if (ioUtil.IS_WINDOWS && process.env['PATHEXT']) { + for (const extension of process.env['PATHEXT'].split(path.delimiter)) { + if (extension) { + extensions.push(extension) + } + } + } + + // if it's rooted, return it if exists. otherwise return empty. + if (ioUtil.isRooted(tool)) { + const filePath: string = await ioUtil.tryGetExecutablePath(tool, extensions) + + if (filePath) { + return [filePath] + } + + return [] + } + + // if any path separators, return empty + if (tool.includes(path.sep)) { + return [] + } + + // build the list of directories + // + // Note, technically "where" checks the current directory on Windows. From a toolkit perspective, + // it feels like we should not do this. Checking the current directory seems like more of a use + // case of a shell, and the which() function exposed by the toolkit should strive for consistency + // across platforms. + const directories: string[] = [] + + if (process.env.PATH) { + for (const p of process.env.PATH.split(path.delimiter)) { + if (p) { + directories.push(p) + } + } + } + + // find all matches + const matches: string[] = [] + + for (const directory of directories) { + const filePath = await ioUtil.tryGetExecutablePath( + path.join(directory, tool), + extensions + ) + if (filePath) { + matches.push(filePath) + } + } + + return matches } function readCopyOptions(options: CopyOptions): Required {