mirror of https://github.com/actions/toolkit
Add findInPath method to locate all matching executables in the system path (#609)
Signed-off-by: Sora Morimoto <sora@morimoto.io>pull/610/head
parent
de122731f3
commit
bd9017e99f
|
@ -1,5 +1,9 @@
|
||||||
# @actions/io Releases
|
# @actions/io Releases
|
||||||
|
|
||||||
|
### 1.1.0
|
||||||
|
|
||||||
|
- Add `findInPath` method to locate all matching executables in the system path
|
||||||
|
|
||||||
### 1.0.2
|
### 1.0.2
|
||||||
|
|
||||||
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
- [Add \"types\" to package.json](https://github.com/actions/toolkit/pull/221)
|
||||||
|
|
|
@ -6,6 +6,10 @@ import * as io from '../src/io'
|
||||||
import * as ioUtil from '../src/io-util'
|
import * as ioUtil from '../src/io-util'
|
||||||
|
|
||||||
describe('cp', () => {
|
describe('cp', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
it('copies file with no flags', async () => {
|
it('copies file with no flags', async () => {
|
||||||
const root = path.join(getTestTemp(), 'cp_with_no_flags')
|
const root = path.join(getTestTemp(), 'cp_with_no_flags')
|
||||||
const sourceFile = path.join(root, 'cp_source')
|
const sourceFile = path.join(root, 'cp_source')
|
||||||
|
@ -166,6 +170,10 @@ describe('cp', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('mv', () => {
|
describe('mv', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
it('moves file with no flags', async () => {
|
it('moves file with no flags', async () => {
|
||||||
const root = path.join(getTestTemp(), ' mv_with_no_flags')
|
const root = path.join(getTestTemp(), ' mv_with_no_flags')
|
||||||
const sourceFile = path.join(root, ' mv_source')
|
const sourceFile = path.join(root, ' mv_source')
|
||||||
|
@ -264,6 +272,10 @@ describe('mv', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('rmRF', () => {
|
describe('rmRF', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
it('removes single folder with rmRF', async () => {
|
it('removes single folder with rmRF', async () => {
|
||||||
const testPath = path.join(getTestTemp(), 'testFolder')
|
const testPath = path.join(getTestTemp(), 'testFolder')
|
||||||
|
|
||||||
|
@ -841,6 +853,10 @@ describe('mkdirP', () => {
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('which', () => {
|
describe('which', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
it('which() finds file name', async () => {
|
it('which() finds file name', async () => {
|
||||||
// create a executable file
|
// create a executable file
|
||||||
const testPath = path.join(getTestTemp(), 'which-finds-file-name')
|
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(
|
async function findsExecutableWithScopedPermissions(
|
||||||
chmodOptions: string
|
chmodOptions: string
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"name": "@actions/io",
|
"name": "@actions/io",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"lockfileVersion": 1
|
"lockfileVersion": 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@actions/io",
|
"name": "@actions/io",
|
||||||
"version": "1.0.2",
|
"version": "1.1.0",
|
||||||
"description": "Actions io lib",
|
"description": "Actions io lib",
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"github",
|
"github",
|
||||||
|
|
|
@ -192,69 +192,85 @@ export async function which(tool: string, check?: boolean): Promise<string> {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const matches: string[] = await findInPath(tool)
|
||||||
// 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 (matches && matches.length > 0) {
|
||||||
if (ioUtil.isRooted(tool)) {
|
return matches[0]
|
||||||
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}`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of all occurrences of the given tool on the system path.
|
||||||
|
*
|
||||||
|
* @returns Promise<string[]> the paths of the tool
|
||||||
|
*/
|
||||||
|
export async function findInPath(tool: string): Promise<string[]> {
|
||||||
|
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<CopyOptions> {
|
function readCopyOptions(options: CopyOptions): Required<CopyOptions> {
|
||||||
|
|
Loading…
Reference in New Issue