mirror of https://github.com/actions/toolkit
glob (#268)
parent
a94e2440cb
commit
a11539e1db
|
@ -0,0 +1,671 @@
|
||||||
|
import * as child from 'child_process'
|
||||||
|
import * as glob from '../src/glob'
|
||||||
|
import * as io from '../../io/src/io'
|
||||||
|
import * as path from 'path'
|
||||||
|
import {promises as fs} from 'fs'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* These test focus on the ability of glob to find files
|
||||||
|
* and not on the pattern matching aspect
|
||||||
|
*/
|
||||||
|
describe('glob', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects cycle', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/file
|
||||||
|
// <root>/symDir -> <root>
|
||||||
|
const root = path.join(getTestTemp(), 'detects-cycle')
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(root, path.join(root, 'symDir'))
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root)
|
||||||
|
expect(itemPaths).toEqual([root, path.join(root, 'file')])
|
||||||
|
// todo: ? expect(itemPaths[2]).toBe(path.join(root, 'symDir'))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects deep cycle starting from middle', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/file-under-root
|
||||||
|
// <root>/folder-a
|
||||||
|
// <root>/folder-a/file-under-a
|
||||||
|
// <root>/folder-a/folder-b
|
||||||
|
// <root>/folder-a/folder-b/file-under-b
|
||||||
|
// <root>/folder-a/folder-b/folder-c
|
||||||
|
// <root>/folder-a/folder-b/folder-c/file-under-c
|
||||||
|
// <root>/folder-a/folder-b/folder-c/sym-folder -> <root>
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'detects-deep-cycle-starting-from-middle'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'folder-a', 'folder-b', 'folder-c'), {
|
||||||
|
recursive: true
|
||||||
|
})
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'file-under-root'),
|
||||||
|
'test file under root contents'
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'folder-a', 'file-under-a'),
|
||||||
|
'test file under a contents'
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'file-under-b'),
|
||||||
|
'test file under b contents'
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c', 'file-under-c'),
|
||||||
|
'test file under c contents'
|
||||||
|
)
|
||||||
|
await createSymlinkDir(
|
||||||
|
root,
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c', 'sym-folder')
|
||||||
|
)
|
||||||
|
await fs.stat(
|
||||||
|
path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'sym-folder',
|
||||||
|
'file-under-root'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(path.join(root, 'folder-a', 'folder-b'))
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
path.join(root, 'folder-a', 'folder-b'),
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'file-under-b'),
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c'),
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c', 'file-under-c'),
|
||||||
|
path.join(root, 'folder-a', 'folder-b', 'folder-c', 'sym-folder'),
|
||||||
|
path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'sym-folder',
|
||||||
|
'file-under-root'
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'sym-folder',
|
||||||
|
'folder-a'
|
||||||
|
),
|
||||||
|
path.join(
|
||||||
|
root,
|
||||||
|
'folder-a',
|
||||||
|
'folder-b',
|
||||||
|
'folder-c',
|
||||||
|
'sym-folder',
|
||||||
|
'folder-a',
|
||||||
|
'file-under-a'
|
||||||
|
)
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('detects cycle starting from symlink', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/file
|
||||||
|
// <root>/symDir -> <root>
|
||||||
|
const root: string = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'detects-cycle-starting-from-symlink'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(root, path.join(root, 'symDir'))
|
||||||
|
await fs.stat(path.join(root, 'symDir'))
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(path.join(root, 'symDir'))
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
path.join(root, 'symDir'),
|
||||||
|
path.join(root, 'symDir', 'file')
|
||||||
|
])
|
||||||
|
// todo: ? expect(itemPaths[2]).toBe(path.join(root, 'symDir', 'symDir'));
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not follow symlink when followSymbolicLink=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/file
|
||||||
|
// <root>/symDir -> <root>/realDir
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'does-not-follow-symlink-when-follow-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root, {followSymbolicLinks: false})
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'realDir', 'file'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not follow symlink when search path is symlink and followSymbolicLink=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// realDir
|
||||||
|
// realDir/file
|
||||||
|
// symDir -> realDir
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'does-not-follow-symlink-when-search-path-is-symlink-and-follow-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(path.join(root, 'symDir'), {
|
||||||
|
followSymbolicLinks: false
|
||||||
|
})
|
||||||
|
expect(itemPaths).toEqual([path.join(root, 'symDir')])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not return broken symlink', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/file
|
||||||
|
// <root>/symDir -> <root>/realDir
|
||||||
|
const root = path.join(getTestTemp(), 'does-not-return-broken-symlink')
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'noSuch'),
|
||||||
|
path.join(root, 'brokenSym')
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root)
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'realDir', 'file'),
|
||||||
|
path.join(root, 'symDir'),
|
||||||
|
path.join(root, 'symDir', 'file')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not return broken symlink when search path is broken symlink', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'does-not-return-broken-symlink-when-search-path-is-broken-symlink'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
const brokenSymPath = path.join(root, 'brokenSym')
|
||||||
|
await createSymlinkDir(path.join(root, 'noSuch'), brokenSymPath)
|
||||||
|
await fs.lstat(brokenSymPath)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(brokenSymPath)
|
||||||
|
expect(itemPaths).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not search paths that are not partial matches', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/nested
|
||||||
|
// <root>/realDir/nested/file
|
||||||
|
// <root>/realDir2
|
||||||
|
// <root>/realDir2/nested2
|
||||||
|
// <root>/realDir2/nested2/symDir -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'does-not-search-paths-that-are-not-partial-matches'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir', 'nested'), {recursive: true})
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'realDir', 'nested', 'file'),
|
||||||
|
'test file content'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir2', 'nested2'), {recursive: true})
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'noSuch'),
|
||||||
|
path.join(root, 'realDir2', 'nested2', 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const options: glob.IGlobOptions = {
|
||||||
|
followSymbolicLinks: true,
|
||||||
|
omitBrokenSymbolicLinks: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should throw
|
||||||
|
try {
|
||||||
|
await glob.glob(`${root}/*Dir*/*nested*/*`, options)
|
||||||
|
throw new Error('should not reach here')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toMatch(/broken symbolic link/i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not partial match
|
||||||
|
let itemPaths = await glob.glob(`${root}/*Dir/*nested*/*`, options)
|
||||||
|
expect(itemPaths).toEqual([path.join(root, 'realDir', 'nested', 'file')])
|
||||||
|
|
||||||
|
// Not partial match
|
||||||
|
itemPaths = await glob.glob(`${root}/*Dir*/*nested/*`, options)
|
||||||
|
expect(itemPaths).toEqual([path.join(root, 'realDir', 'nested', 'file')])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('does not throw for broken symlinks that are not matches or partial matches', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/file
|
||||||
|
// <root>/symDir -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'does-not-throw-for-broken-symlinks-that-are-not-matches-or-partial-matches'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(path.join(root, 'noSuch'), path.join(root, 'symDir'))
|
||||||
|
|
||||||
|
const options: glob.IGlobOptions = {
|
||||||
|
followSymbolicLinks: true,
|
||||||
|
omitBrokenSymbolicLinks: false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match should throw
|
||||||
|
try {
|
||||||
|
await glob.glob(`${root}/*`, options)
|
||||||
|
throw new Error('should not reach here')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toMatch(/broken symbolic link/i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Partial match should throw
|
||||||
|
try {
|
||||||
|
await glob.glob(`${root}/*/*`, options)
|
||||||
|
throw new Error('should not reach here')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toMatch(/broken symbolic link/i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not match or partial match
|
||||||
|
const itemPaths = await glob.glob(`${root}/*eal*/*`, options)
|
||||||
|
expect(itemPaths).toEqual([path.join(root, 'realDir', 'file')])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('follows symlink', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/file
|
||||||
|
// <root>/symDir -> <root>/realDir
|
||||||
|
const root = path.join(getTestTemp(), 'follows-symlink')
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root)
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'realDir', 'file'),
|
||||||
|
path.join(root, 'symDir'),
|
||||||
|
path.join(root, 'symDir', 'file')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('follows symlink when search path is symlink', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// realDir
|
||||||
|
// realDir/file
|
||||||
|
// symDir -> realDir
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'follows-symlink-when-search-path-is-symlink'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(path.join(root, 'symDir'))
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
path.join(root, 'symDir'),
|
||||||
|
path.join(root, 'symDir', 'file')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns broken symlink when followSymbolicLinks=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
// <root>/realDir
|
||||||
|
// <root>/realDir/file
|
||||||
|
// <root>/symDir -> <root>/realDir
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'returns-broken-symlink-when-follow-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'noSuch'),
|
||||||
|
path.join(root, 'brokenSym')
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'realDir'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'realDir', 'file'), 'test file content')
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root, {followSymbolicLinks: false})
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'brokenSym'),
|
||||||
|
path.join(root, 'realDir'),
|
||||||
|
path.join(root, 'realDir', 'file'),
|
||||||
|
path.join(root, 'symDir')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns broken symlink when search path is broken symlink and followSymbolicLinks=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'returns-broken-symlink-when-search-path-is-broken-symlink-and-follow-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
const brokenSymPath = path.join(root, 'brokenSym')
|
||||||
|
await createSymlinkDir(path.join(root, 'noSuch'), brokenSymPath)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(brokenSymPath, {
|
||||||
|
followSymbolicLinks: false
|
||||||
|
})
|
||||||
|
expect(itemPaths).toEqual([brokenSymPath])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns depth first', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>/a-file
|
||||||
|
// <root>/b-folder
|
||||||
|
// <root>/b-folder/a-file
|
||||||
|
// <root>/b-folder/b-folder
|
||||||
|
// <root>/b-folder/b-folder/file
|
||||||
|
// <root>/b-folder/c-file
|
||||||
|
// <root>/c-file
|
||||||
|
const root = path.join(getTestTemp(), 'returns-depth-first')
|
||||||
|
await fs.mkdir(path.join(root, 'b-folder', 'b-folder'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'a-file'), 'test a-file content')
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'b-folder', 'a-file'),
|
||||||
|
'test b-folder/a-file content'
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'b-folder', 'b-folder', 'file'),
|
||||||
|
'test b-folder/b-folder/file content'
|
||||||
|
)
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, 'b-folder', 'c-file'),
|
||||||
|
'test b-folder/c-file content'
|
||||||
|
)
|
||||||
|
await fs.writeFile(path.join(root, 'c-file'), 'test c-file content')
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root)
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'a-file'),
|
||||||
|
path.join(root, 'b-folder'),
|
||||||
|
path.join(root, 'b-folder', 'a-file'),
|
||||||
|
path.join(root, 'b-folder', 'b-folder'),
|
||||||
|
path.join(root, 'b-folder', 'b-folder', 'file'),
|
||||||
|
path.join(root, 'b-folder', 'c-file'),
|
||||||
|
path.join(root, 'c-file')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns descendants', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>/file-1
|
||||||
|
// <root>/dir-1
|
||||||
|
// <root>/dir-1/file-2
|
||||||
|
// <root>/dir-1/dir-2
|
||||||
|
// <root>/dir-1/dir-2/file-3
|
||||||
|
const root = path.join(getTestTemp(), 'returns-descendants')
|
||||||
|
await fs.mkdir(path.join(root, 'dir-1', 'dir-2'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'file-1'), '')
|
||||||
|
await fs.writeFile(path.join(root, 'dir-1', 'file-2'), '')
|
||||||
|
await fs.writeFile(path.join(root, 'dir-1', 'dir-2', 'file-3'), '')
|
||||||
|
|
||||||
|
// When pattern ends with `/**/`
|
||||||
|
let pattern = `${root}${path.sep}**${path.sep}`
|
||||||
|
expect(
|
||||||
|
await glob.glob(pattern, {
|
||||||
|
implicitDescendants: false
|
||||||
|
})
|
||||||
|
).toHaveLength(3) // sanity check
|
||||||
|
expect(await glob.glob(pattern)).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'dir-1'),
|
||||||
|
path.join(root, 'dir-1', 'dir-2'),
|
||||||
|
path.join(root, 'dir-1', 'dir-2', 'file-3'),
|
||||||
|
path.join(root, 'dir-1', 'file-2'),
|
||||||
|
path.join(root, 'file-1')
|
||||||
|
])
|
||||||
|
|
||||||
|
// When pattern ends with something other than `/**/`
|
||||||
|
pattern = `${root}${path.sep}**${path.sep}dir-?`
|
||||||
|
expect(
|
||||||
|
await glob.glob(pattern, {
|
||||||
|
implicitDescendants: false
|
||||||
|
})
|
||||||
|
).toHaveLength(2) // sanity check
|
||||||
|
expect(await glob.glob(pattern)).toEqual([
|
||||||
|
path.join(root, 'dir-1'),
|
||||||
|
path.join(root, 'dir-1', 'dir-2'),
|
||||||
|
path.join(root, 'dir-1', 'dir-2', 'file-3'),
|
||||||
|
path.join(root, 'dir-1', 'file-2')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns directories only when trailing slash and implicit descendants false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>/file-1
|
||||||
|
// <root>/dir-1
|
||||||
|
// <root>/dir-1/file-2
|
||||||
|
// <root>/dir-1/dir-2
|
||||||
|
// <root>/dir-1/dir-2/file-3
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'returns-directories-only-when-trailing-slash-and-implicit-descendants-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(path.join(root, 'dir-1', 'dir-2'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'file-1'), '')
|
||||||
|
await fs.writeFile(path.join(root, 'dir-1', 'file-2'), '')
|
||||||
|
await fs.writeFile(path.join(root, 'dir-1', 'dir-2', 'file-3'), '')
|
||||||
|
|
||||||
|
const pattern = `${root}${path.sep}**${path.sep}`
|
||||||
|
expect(await glob.glob(pattern)).toHaveLength(6) // sanity check
|
||||||
|
expect(
|
||||||
|
await glob.glob(pattern, {
|
||||||
|
implicitDescendants: false
|
||||||
|
})
|
||||||
|
).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, 'dir-1'),
|
||||||
|
path.join(root, 'dir-1', 'dir-2')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns empty when search path does not exist', async () => {
|
||||||
|
const itemPaths = await glob.glob(path.join(getTestTemp(), 'nosuch'))
|
||||||
|
expect(itemPaths).toEqual([])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns hidden files', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/.emptyFolder
|
||||||
|
// <root>/.file
|
||||||
|
// <root>/.folder
|
||||||
|
// <root>/.folder/file
|
||||||
|
const root = path.join(getTestTemp(), 'returns-hidden-files')
|
||||||
|
await createHiddenDirectory(path.join(root, '.emptyFolder'))
|
||||||
|
await createHiddenDirectory(path.join(root, '.folder'))
|
||||||
|
await createHiddenFile(path.join(root, '.file'), 'test .file content')
|
||||||
|
await fs.writeFile(
|
||||||
|
path.join(root, '.folder', 'file'),
|
||||||
|
'test .folder/file content'
|
||||||
|
)
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(root)
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
root,
|
||||||
|
path.join(root, '.emptyFolder'),
|
||||||
|
path.join(root, '.file'),
|
||||||
|
path.join(root, '.folder'),
|
||||||
|
path.join(root, '.folder', 'file')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns normalized paths', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>/hello/world.txt
|
||||||
|
const root: string = path.join(getTestTemp(), 'returns-normalized-paths')
|
||||||
|
await fs.mkdir(path.join(root, 'hello'), {recursive: true})
|
||||||
|
await fs.writeFile(path.join(root, 'hello', 'world.txt'), '')
|
||||||
|
|
||||||
|
const itemPaths = await glob.glob(
|
||||||
|
`${root}${path.sep}${path.sep}${path.sep}hello`
|
||||||
|
)
|
||||||
|
expect(itemPaths).toEqual([
|
||||||
|
path.join(root, 'hello'),
|
||||||
|
path.join(root, 'hello', 'world.txt')
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when match broken symlink and omitBrokenSymbolicLinks=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'throws-when-match-broken-symlink-and-omit-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
await createSymlinkDir(
|
||||||
|
path.join(root, 'noSuch'),
|
||||||
|
path.join(root, 'brokenSym')
|
||||||
|
)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await glob.glob(root, {omitBrokenSymbolicLinks: false})
|
||||||
|
throw new Error('Expected tl.find to throw')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toMatch(/broken symbolic link/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws when search path is broken symlink and omitBrokenSymbolicLinks=false', async () => {
|
||||||
|
// Create the following layout:
|
||||||
|
// <root>
|
||||||
|
// <root>/brokenSym -> <root>/noSuch
|
||||||
|
const root = path.join(
|
||||||
|
getTestTemp(),
|
||||||
|
'throws-when-search-path-is-broken-symlink-and-omit-false'
|
||||||
|
)
|
||||||
|
await fs.mkdir(root, {recursive: true})
|
||||||
|
const brokenSymPath = path.join(root, 'brokenSym')
|
||||||
|
await createSymlinkDir(path.join(root, 'noSuch'), brokenSymPath)
|
||||||
|
await fs.lstat(brokenSymPath)
|
||||||
|
|
||||||
|
try {
|
||||||
|
await glob.glob(brokenSymPath, {omitBrokenSymbolicLinks: false})
|
||||||
|
throw new Error('Expected tl.find to throw')
|
||||||
|
} catch (err) {
|
||||||
|
expect(err.message).toMatch(/broken symbolic link/)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createHiddenDirectory(dir: string): Promise<void> {
|
||||||
|
if (!path.basename(dir).match(/^\./)) {
|
||||||
|
throw new Error(`Expected dir '${dir}' to start with '.'.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(dir, {recursive: true})
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const result = child.spawnSync('attrib.exe', ['+H', dir])
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const message: string = (result.output || []).join(' ').trim()
|
||||||
|
throw new Error(
|
||||||
|
`Failed to set hidden attribute for directory '${dir}'. ${message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createHiddenFile(file: string, content: string): Promise<void> {
|
||||||
|
if (!path.basename(file).match(/^\./)) {
|
||||||
|
throw new Error(`Expected dir '${file}' to start with '.'.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
await fs.mkdir(path.dirname(file), {recursive: true})
|
||||||
|
await fs.writeFile(file, content)
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const result = child.spawnSync('attrib.exe', ['+H', file])
|
||||||
|
if (result.status !== 0) {
|
||||||
|
const message: string = (result.output || []).join(' ').trim()
|
||||||
|
throw new Error(
|
||||||
|
`Failed to set hidden attribute for file '${file}'. ${message}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTestTemp(): string {
|
||||||
|
return path.join(__dirname, '_temp', 'glob')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a symlink directory on OSX/Linux, and a junction point directory on Windows.
|
||||||
|
* A symlink directory is not created on Windows since it requires an elevated context.
|
||||||
|
*/
|
||||||
|
async function createSymlinkDir(real: string, link: string): Promise<void> {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
await fs.symlink(real, link, 'junction')
|
||||||
|
} else {
|
||||||
|
await fs.symlink(real, link)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,640 @@
|
||||||
|
import * as pathHelper from '../src/internal-path-helper'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
describe('path-helper', () => {
|
||||||
|
it('dirname interprets directory name from paths', () => {
|
||||||
|
assertDirectoryName('', '.')
|
||||||
|
assertDirectoryName('.', '.')
|
||||||
|
assertDirectoryName('..', '.')
|
||||||
|
assertDirectoryName('hello', '.')
|
||||||
|
assertDirectoryName('hello/', '.')
|
||||||
|
assertDirectoryName('hello/world', 'hello')
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Removes redundant slashes
|
||||||
|
assertDirectoryName('C:\\\\hello\\\\\\world\\\\', 'C:\\hello')
|
||||||
|
assertDirectoryName('C://hello///world//', 'C:\\hello')
|
||||||
|
// Relative root:
|
||||||
|
assertDirectoryName('\\hello\\\\world\\\\again\\\\', '\\hello\\world')
|
||||||
|
assertDirectoryName('/hello///world//again//', '\\hello\\world')
|
||||||
|
// UNC:
|
||||||
|
assertDirectoryName('\\\\hello\\world\\again\\', '\\\\hello\\world')
|
||||||
|
assertDirectoryName(
|
||||||
|
'\\\\hello\\\\\\world\\\\again\\\\',
|
||||||
|
'\\\\hello\\world'
|
||||||
|
)
|
||||||
|
assertDirectoryName(
|
||||||
|
'\\\\\\hello\\\\\\world\\\\again\\\\',
|
||||||
|
'\\\\hello\\world'
|
||||||
|
)
|
||||||
|
assertDirectoryName(
|
||||||
|
'\\\\\\\\hello\\\\\\world\\\\again\\\\',
|
||||||
|
'\\\\hello\\world'
|
||||||
|
)
|
||||||
|
assertDirectoryName('//hello///world//again//', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('///hello///world//again//', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('/////hello///world//again//', '\\\\hello\\world')
|
||||||
|
// Relative:
|
||||||
|
assertDirectoryName('hello\\world', 'hello')
|
||||||
|
|
||||||
|
// Directory trimming
|
||||||
|
assertDirectoryName('a:/hello', 'a:\\')
|
||||||
|
assertDirectoryName('z:/hello', 'z:\\')
|
||||||
|
assertDirectoryName('A:/hello', 'A:\\')
|
||||||
|
assertDirectoryName('Z:/hello', 'Z:\\')
|
||||||
|
assertDirectoryName('C:/', 'C:\\')
|
||||||
|
assertDirectoryName('C:/hello', 'C:\\')
|
||||||
|
assertDirectoryName('C:/hello/', 'C:\\')
|
||||||
|
assertDirectoryName('C:/hello/world', 'C:\\hello')
|
||||||
|
assertDirectoryName('C:/hello/world/', 'C:\\hello')
|
||||||
|
assertDirectoryName('C:', 'C:')
|
||||||
|
assertDirectoryName('C:hello', 'C:')
|
||||||
|
assertDirectoryName('C:hello/', 'C:')
|
||||||
|
assertDirectoryName('C:hello/world', 'C:hello')
|
||||||
|
assertDirectoryName('C:hello/world/', 'C:hello')
|
||||||
|
assertDirectoryName('/', '\\')
|
||||||
|
assertDirectoryName('/hello', '\\')
|
||||||
|
assertDirectoryName('/hello/', '\\')
|
||||||
|
assertDirectoryName('/hello/world', '\\hello')
|
||||||
|
assertDirectoryName('/hello/world/', '\\hello')
|
||||||
|
assertDirectoryName('\\', '\\')
|
||||||
|
assertDirectoryName('\\hello', '\\')
|
||||||
|
assertDirectoryName('\\hello\\', '\\')
|
||||||
|
assertDirectoryName('\\hello\\world', '\\hello')
|
||||||
|
assertDirectoryName('\\hello\\world\\', '\\hello')
|
||||||
|
assertDirectoryName('//hello', '\\\\hello')
|
||||||
|
assertDirectoryName('//hello/', '\\\\hello')
|
||||||
|
assertDirectoryName('//hello/world', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('//hello/world/', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('\\\\hello', '\\\\hello')
|
||||||
|
assertDirectoryName('\\\\hello\\', '\\\\hello')
|
||||||
|
assertDirectoryName('\\\\hello\\world', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('\\\\hello\\world\\', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('//hello/world/again', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('//hello/world/again/', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('hello/world/', 'hello')
|
||||||
|
assertDirectoryName('hello/world/again', 'hello\\world')
|
||||||
|
assertDirectoryName('../../hello', '..\\..')
|
||||||
|
} else {
|
||||||
|
// Should not converts slashes
|
||||||
|
assertDirectoryName('/hello\\world', '/')
|
||||||
|
assertDirectoryName('/hello\\world/', '/')
|
||||||
|
assertDirectoryName('\\\\hello\\world\\again', '.')
|
||||||
|
assertDirectoryName('\\\\hello\\world/', '.')
|
||||||
|
assertDirectoryName('\\\\hello\\world/again', '\\\\hello\\world')
|
||||||
|
assertDirectoryName('hello\\world', '.')
|
||||||
|
assertDirectoryName('hello\\world/', '.')
|
||||||
|
|
||||||
|
// Should remove redundant slashes (rooted paths; UNC format not special)
|
||||||
|
assertDirectoryName('//hello', '/')
|
||||||
|
assertDirectoryName('//hello/world', '/hello')
|
||||||
|
assertDirectoryName('//hello/world/', '/hello')
|
||||||
|
assertDirectoryName('//hello//world//', '/hello')
|
||||||
|
assertDirectoryName('///hello////world///', '/hello')
|
||||||
|
|
||||||
|
// Should remove redundant slashes (relative paths)
|
||||||
|
assertDirectoryName('hello//world//again//', 'hello/world')
|
||||||
|
assertDirectoryName('hello///world///again///', 'hello/world')
|
||||||
|
|
||||||
|
// Directory trimming (Windows drive root format not special)
|
||||||
|
assertDirectoryName('C:/', '.')
|
||||||
|
assertDirectoryName('C:/hello', 'C:')
|
||||||
|
assertDirectoryName('C:/hello/', 'C:')
|
||||||
|
assertDirectoryName('C:/hello/world', 'C:/hello')
|
||||||
|
assertDirectoryName('C:/hello/world/', 'C:/hello')
|
||||||
|
assertDirectoryName('C:', '.')
|
||||||
|
assertDirectoryName('C:hello', '.')
|
||||||
|
assertDirectoryName('C:hello/', '.')
|
||||||
|
assertDirectoryName('C:hello/world', 'C:hello')
|
||||||
|
assertDirectoryName('C:hello/world/', 'C:hello')
|
||||||
|
|
||||||
|
// Directory trimming (rooted paths)
|
||||||
|
assertDirectoryName('/', '/')
|
||||||
|
assertDirectoryName('/hello', '/')
|
||||||
|
assertDirectoryName('/hello/', '/')
|
||||||
|
assertDirectoryName('/hello/world', '/hello')
|
||||||
|
assertDirectoryName('/hello/world/', '/hello')
|
||||||
|
|
||||||
|
// Directory trimming (relative paths)
|
||||||
|
assertDirectoryName('hello/world/', 'hello')
|
||||||
|
assertDirectoryName('hello/world/again', 'hello/world')
|
||||||
|
assertDirectoryName('../../hello', '../..')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('ensureAbsoluteRoot roots paths', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const currentDrive = process.cwd().substr(0, 2)
|
||||||
|
expect(currentDrive.match(/^[A-Z]:$/i)).toBeTruthy()
|
||||||
|
const otherDrive = currentDrive.toUpperCase().startsWith('C')
|
||||||
|
? 'D:'
|
||||||
|
: 'C:'
|
||||||
|
|
||||||
|
// Preserves relative pathing
|
||||||
|
assertEnsureAbsoluteRoot('C:/foo', '.', `C:/foo\\.`)
|
||||||
|
assertEnsureAbsoluteRoot('C:/foo/..', 'bar', `C:/foo/..\\bar`)
|
||||||
|
assertEnsureAbsoluteRoot('C:/foo', 'bar/../baz', `C:/foo\\bar/../baz`)
|
||||||
|
|
||||||
|
// Already rooted - drive root
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'C:/', 'C:/')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'a:/hello', 'a:/hello')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'C:\\', 'C:\\')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'C:\\hello', 'C:\\hello')
|
||||||
|
|
||||||
|
// Already rooted - relative current drive root
|
||||||
|
expect(process.cwd().length).toBeGreaterThan(3) // sanity check not drive root
|
||||||
|
assertEnsureAbsoluteRoot(`${otherDrive}\\`, currentDrive, process.cwd())
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${otherDrive}\\`,
|
||||||
|
`${currentDrive}hello`,
|
||||||
|
`${process.cwd()}\\hello`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${otherDrive}\\`,
|
||||||
|
`${currentDrive}hello/world`,
|
||||||
|
`${process.cwd()}\\hello/world`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${otherDrive}\\`,
|
||||||
|
`${currentDrive}hello\\world`,
|
||||||
|
`${process.cwd()}\\hello\\world`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Already rooted - relative other drive root
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${currentDrive}\\`,
|
||||||
|
otherDrive,
|
||||||
|
`${otherDrive}\\`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${currentDrive}\\`,
|
||||||
|
`${otherDrive}hello`,
|
||||||
|
`${otherDrive}\\hello`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${currentDrive}\\`,
|
||||||
|
`${otherDrive}hello/world`,
|
||||||
|
`${otherDrive}\\hello/world`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${currentDrive}\\`,
|
||||||
|
`${otherDrive}hello\\world`,
|
||||||
|
`${otherDrive}\\hello\\world`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Already rooted - current drive root
|
||||||
|
assertEnsureAbsoluteRoot(`${otherDrive}\\`, '/', `${currentDrive}\\`)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${otherDrive}\\`,
|
||||||
|
'/hello',
|
||||||
|
`${currentDrive}\\hello`
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot(`${otherDrive}\\`, '\\', `${currentDrive}\\`)
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
`${otherDrive}\\`,
|
||||||
|
'\\hello',
|
||||||
|
`${currentDrive}\\hello`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Already rooted - UNC
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', '//machine/share', '//machine/share')
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'D:\\',
|
||||||
|
'\\\\machine\\share',
|
||||||
|
'\\\\machine\\share'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertEnsureAbsoluteRoot('D:/', 'hello', 'D:/hello')
|
||||||
|
assertEnsureAbsoluteRoot('D:/', 'hello/world', 'D:/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'hello', 'D:\\hello')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\', 'hello\\world', 'D:\\hello\\world')
|
||||||
|
assertEnsureAbsoluteRoot('D:/root', 'hello', 'D:/root\\hello')
|
||||||
|
assertEnsureAbsoluteRoot('D:/root', 'hello/world', 'D:/root\\hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\root', 'hello', 'D:\\root\\hello')
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'D:\\root',
|
||||||
|
'hello\\world',
|
||||||
|
'D:\\root\\hello\\world'
|
||||||
|
)
|
||||||
|
assertEnsureAbsoluteRoot('D:/root/', 'hello', 'D:/root/hello')
|
||||||
|
assertEnsureAbsoluteRoot('D:/root/', 'hello/world', 'D:/root/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('D:\\root\\', 'hello', 'D:\\root\\hello')
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'D:\\root\\',
|
||||||
|
'hello\\world',
|
||||||
|
'D:\\root\\hello\\world'
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Preserves relative pathing
|
||||||
|
assertEnsureAbsoluteRoot('/foo', '.', `/foo/.`)
|
||||||
|
assertEnsureAbsoluteRoot('/foo/..', 'bar', `/foo/../bar`)
|
||||||
|
assertEnsureAbsoluteRoot('/foo', 'bar/../baz', `/foo/bar/../baz`)
|
||||||
|
|
||||||
|
// Already rooted
|
||||||
|
assertEnsureAbsoluteRoot('/root', '/', '/')
|
||||||
|
assertEnsureAbsoluteRoot('/root', '/hello', '/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/root', '/hello/world', '/hello/world')
|
||||||
|
|
||||||
|
// Not already rooted - Windows style drive root
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'C:/', '/root/C:/')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'C:/hello', '/root/C:/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'C:\\', '/root/C:\\')
|
||||||
|
|
||||||
|
// Not already rooted - Windows style relative drive root
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'C:', '/root/C:')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'C:hello/world', '/root/C:hello/world')
|
||||||
|
|
||||||
|
// Not already rooted - Windows style current drive root
|
||||||
|
assertEnsureAbsoluteRoot('/root', '\\', '/root/\\')
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'/root',
|
||||||
|
'\\hello\\world',
|
||||||
|
'/root/\\hello\\world'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Not already rooted - Windows style UNC
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'/root',
|
||||||
|
'\\\\machine\\share',
|
||||||
|
'/root/\\\\machine\\share'
|
||||||
|
)
|
||||||
|
|
||||||
|
// Not already rooted - relative
|
||||||
|
assertEnsureAbsoluteRoot('/', 'hello', '/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/', 'hello/world', '/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('/', 'hello\\world', '/hello\\world')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'hello', '/root/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'hello/world', '/root/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('/root', 'hello\\world', '/root/hello\\world')
|
||||||
|
assertEnsureAbsoluteRoot('/root/', 'hello', '/root/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/root/', 'hello/world', '/root/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot('/root/', 'hello\\world', '/root/hello\\world')
|
||||||
|
assertEnsureAbsoluteRoot('/root\\', 'hello', '/root\\/hello')
|
||||||
|
assertEnsureAbsoluteRoot('/root\\', 'hello/world', '/root\\/hello/world')
|
||||||
|
assertEnsureAbsoluteRoot(
|
||||||
|
'/root\\',
|
||||||
|
'hello\\world',
|
||||||
|
'/root\\/hello\\world'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hasAbsoluteRoot detects absolute root', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Drive root
|
||||||
|
assertHasAbsoluteRoot('C:/', true)
|
||||||
|
assertHasAbsoluteRoot('a:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('c:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('z:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('A:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('C:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('Z:/hello', true)
|
||||||
|
assertHasAbsoluteRoot('C:\\', true)
|
||||||
|
assertHasAbsoluteRoot('C:\\hello', true)
|
||||||
|
|
||||||
|
// Relative drive root
|
||||||
|
assertHasAbsoluteRoot('C:', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello/world', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello\\world', false)
|
||||||
|
|
||||||
|
// Current drive root
|
||||||
|
assertHasAbsoluteRoot('/', false)
|
||||||
|
assertHasAbsoluteRoot('/hello', false)
|
||||||
|
assertHasAbsoluteRoot('/hello/world', false)
|
||||||
|
assertHasAbsoluteRoot('\\', false)
|
||||||
|
assertHasAbsoluteRoot('\\hello', false)
|
||||||
|
assertHasAbsoluteRoot('\\hello\\world', false)
|
||||||
|
|
||||||
|
// UNC
|
||||||
|
assertHasAbsoluteRoot('//machine/share', true)
|
||||||
|
assertHasAbsoluteRoot('//machine/share/', true)
|
||||||
|
assertHasAbsoluteRoot('//machine/share/hello', true)
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share', true)
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share\\', true)
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share\\hello', true)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertHasAbsoluteRoot('hello', false)
|
||||||
|
assertHasAbsoluteRoot('hello/world', false)
|
||||||
|
assertHasAbsoluteRoot('hello\\world', false)
|
||||||
|
} else {
|
||||||
|
// Root
|
||||||
|
assertHasAbsoluteRoot('/', true)
|
||||||
|
assertHasAbsoluteRoot('/hello', true)
|
||||||
|
assertHasAbsoluteRoot('/hello/world', true)
|
||||||
|
|
||||||
|
// Windows style drive root - false on OSX/Linux
|
||||||
|
assertHasAbsoluteRoot('C:/', false)
|
||||||
|
assertHasAbsoluteRoot('a:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('c:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('z:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('A:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('C:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('Z:/hello', false)
|
||||||
|
assertHasAbsoluteRoot('C:\\', false)
|
||||||
|
assertHasAbsoluteRoot('C:\\hello', false)
|
||||||
|
|
||||||
|
// Windows style relative drive root - false on OSX/Linux
|
||||||
|
assertHasAbsoluteRoot('C:', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello/world', false)
|
||||||
|
assertHasAbsoluteRoot('C:hello\\world', false)
|
||||||
|
|
||||||
|
// Windows style current drive root - false on OSX/Linux
|
||||||
|
assertHasAbsoluteRoot('\\', false)
|
||||||
|
assertHasAbsoluteRoot('\\hello', false)
|
||||||
|
assertHasAbsoluteRoot('\\hello\\world', false)
|
||||||
|
|
||||||
|
// Windows style UNC - false on OSX/Linux
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share', false)
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share\\', false)
|
||||||
|
assertHasAbsoluteRoot('\\\\machine\\share\\hello', false)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertHasAbsoluteRoot('hello', false)
|
||||||
|
assertHasAbsoluteRoot('hello/world', false)
|
||||||
|
assertHasAbsoluteRoot('hello\\world', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('hasRoot detects root', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Drive root
|
||||||
|
assertHasRoot('C:/', true)
|
||||||
|
assertHasRoot('a:/hello', true)
|
||||||
|
assertHasRoot('c:/hello', true)
|
||||||
|
assertHasRoot('z:/hello', true)
|
||||||
|
assertHasRoot('A:/hello', true)
|
||||||
|
assertHasRoot('C:/hello', true)
|
||||||
|
assertHasRoot('Z:/hello', true)
|
||||||
|
assertHasRoot('C:\\', true)
|
||||||
|
assertHasRoot('C:\\hello', true)
|
||||||
|
|
||||||
|
// Relative drive root
|
||||||
|
assertHasRoot('C:', true)
|
||||||
|
assertHasRoot('C:hello', true)
|
||||||
|
assertHasRoot('C:hello/world', true)
|
||||||
|
assertHasRoot('C:hello\\world', true)
|
||||||
|
|
||||||
|
// Current drive root
|
||||||
|
assertHasRoot('/', true)
|
||||||
|
assertHasRoot('/hello', true)
|
||||||
|
assertHasRoot('/hello/world', true)
|
||||||
|
assertHasRoot('\\', true)
|
||||||
|
assertHasRoot('\\hello', true)
|
||||||
|
assertHasRoot('\\hello\\world', true)
|
||||||
|
|
||||||
|
// UNC
|
||||||
|
assertHasRoot('//machine/share', true)
|
||||||
|
assertHasRoot('//machine/share/', true)
|
||||||
|
assertHasRoot('//machine/share/hello', true)
|
||||||
|
assertHasRoot('\\\\machine\\share', true)
|
||||||
|
assertHasRoot('\\\\machine\\share\\', true)
|
||||||
|
assertHasRoot('\\\\machine\\share\\hello', true)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertHasRoot('hello', false)
|
||||||
|
assertHasRoot('hello/world', false)
|
||||||
|
assertHasRoot('hello\\world', false)
|
||||||
|
} else {
|
||||||
|
// Root
|
||||||
|
assertHasRoot('/', true)
|
||||||
|
assertHasRoot('/hello', true)
|
||||||
|
assertHasRoot('/hello/world', true)
|
||||||
|
|
||||||
|
// Windows style drive root - false on OSX/Linux
|
||||||
|
assertHasRoot('C:/', false)
|
||||||
|
assertHasRoot('a:/hello', false)
|
||||||
|
assertHasRoot('c:/hello', false)
|
||||||
|
assertHasRoot('z:/hello', false)
|
||||||
|
assertHasRoot('A:/hello', false)
|
||||||
|
assertHasRoot('C:/hello', false)
|
||||||
|
assertHasRoot('Z:/hello', false)
|
||||||
|
assertHasRoot('C:\\', false)
|
||||||
|
assertHasRoot('C:\\hello', false)
|
||||||
|
|
||||||
|
// Windows style relative drive root - false on OSX/Linux
|
||||||
|
assertHasRoot('C:', false)
|
||||||
|
assertHasRoot('C:hello', false)
|
||||||
|
assertHasRoot('C:hello/world', false)
|
||||||
|
assertHasRoot('C:hello\\world', false)
|
||||||
|
|
||||||
|
// Windows style current drive root - false on OSX/Linux
|
||||||
|
assertHasRoot('\\', false)
|
||||||
|
assertHasRoot('\\hello', false)
|
||||||
|
assertHasRoot('\\hello\\world', false)
|
||||||
|
|
||||||
|
// Windows style UNC - false on OSX/Linux
|
||||||
|
assertHasRoot('\\\\machine\\share', false)
|
||||||
|
assertHasRoot('\\\\machine\\share\\', false)
|
||||||
|
assertHasRoot('\\\\machine\\share\\hello', false)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertHasRoot('hello', false)
|
||||||
|
assertHasRoot('hello/world', false)
|
||||||
|
assertHasRoot('hello\\world', false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizeSeparators normalizes slashes', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Drive-rooted
|
||||||
|
assertNormalizeSeparators('C:/', 'C:\\')
|
||||||
|
assertNormalizeSeparators('C:/hello', 'C:\\hello')
|
||||||
|
assertNormalizeSeparators('C:/hello/', 'C:\\hello\\')
|
||||||
|
assertNormalizeSeparators('C:\\', 'C:\\')
|
||||||
|
assertNormalizeSeparators('C:\\hello', 'C:\\hello')
|
||||||
|
assertNormalizeSeparators('C:', 'C:')
|
||||||
|
assertNormalizeSeparators('C:hello', 'C:hello')
|
||||||
|
assertNormalizeSeparators('C:hello/world', 'C:hello\\world')
|
||||||
|
assertNormalizeSeparators('C:hello\\world', 'C:hello\\world')
|
||||||
|
assertNormalizeSeparators('/', '\\')
|
||||||
|
assertNormalizeSeparators('/hello', '\\hello')
|
||||||
|
assertNormalizeSeparators('/hello/world', '\\hello\\world')
|
||||||
|
assertNormalizeSeparators('/hello//world', '\\hello\\world')
|
||||||
|
assertNormalizeSeparators('\\', '\\')
|
||||||
|
assertNormalizeSeparators('\\hello', '\\hello')
|
||||||
|
assertNormalizeSeparators('\\hello\\', '\\hello\\')
|
||||||
|
assertNormalizeSeparators('\\hello\\world', '\\hello\\world')
|
||||||
|
assertNormalizeSeparators('\\hello\\\\world', '\\hello\\world')
|
||||||
|
|
||||||
|
// UNC
|
||||||
|
assertNormalizeSeparators('//machine/share', '\\\\machine\\share')
|
||||||
|
assertNormalizeSeparators('//machine/share/', '\\\\machine\\share\\')
|
||||||
|
assertNormalizeSeparators(
|
||||||
|
'//machine/share/hello',
|
||||||
|
'\\\\machine\\share\\hello'
|
||||||
|
)
|
||||||
|
assertNormalizeSeparators('///machine/share', '\\\\machine\\share')
|
||||||
|
assertNormalizeSeparators('\\\\machine\\share', '\\\\machine\\share')
|
||||||
|
assertNormalizeSeparators('\\\\machine\\share\\', '\\\\machine\\share\\')
|
||||||
|
assertNormalizeSeparators(
|
||||||
|
'\\\\machine\\share\\hello',
|
||||||
|
'\\\\machine\\share\\hello'
|
||||||
|
)
|
||||||
|
assertNormalizeSeparators('\\\\\\machine\\share', '\\\\machine\\share')
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertNormalizeSeparators('hello', 'hello')
|
||||||
|
assertNormalizeSeparators('hello/world', 'hello\\world')
|
||||||
|
assertNormalizeSeparators('hello//world', 'hello\\world')
|
||||||
|
assertNormalizeSeparators('hello\\world', 'hello\\world')
|
||||||
|
assertNormalizeSeparators('hello\\\\world', 'hello\\world')
|
||||||
|
} else {
|
||||||
|
// Rooted
|
||||||
|
assertNormalizeSeparators('/', '/')
|
||||||
|
assertNormalizeSeparators('/hello', '/hello')
|
||||||
|
assertNormalizeSeparators('/hello/world', '/hello/world')
|
||||||
|
assertNormalizeSeparators('//hello/world/', '/hello/world/')
|
||||||
|
|
||||||
|
// Backslash not converted
|
||||||
|
assertNormalizeSeparators('C:\\', 'C:\\')
|
||||||
|
assertNormalizeSeparators('C:\\\\hello\\\\', 'C:\\\\hello\\\\')
|
||||||
|
assertNormalizeSeparators('\\', '\\')
|
||||||
|
assertNormalizeSeparators('\\hello', '\\hello')
|
||||||
|
assertNormalizeSeparators('\\hello\\world', '\\hello\\world')
|
||||||
|
assertNormalizeSeparators('hello\\world', 'hello\\world')
|
||||||
|
|
||||||
|
// UNC not converted
|
||||||
|
assertNormalizeSeparators('\\\\machine\\share', '\\\\machine\\share')
|
||||||
|
|
||||||
|
// UNC not preserved
|
||||||
|
assertNormalizeSeparators('//machine/share', '/machine/share')
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
assertNormalizeSeparators('hello', 'hello')
|
||||||
|
assertNormalizeSeparators('hello/////world', 'hello/world')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('safeTrimTrailingSeparator safely trims trailing separator', () => {
|
||||||
|
assertSafeTrimTrailingSeparator('', '')
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Removes redundant slashes
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'C:\\\\hello\\\\\\world\\\\',
|
||||||
|
'C:\\hello\\world'
|
||||||
|
)
|
||||||
|
assertSafeTrimTrailingSeparator('C://hello///world//', 'C:\\hello\\world')
|
||||||
|
// Relative root:
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'\\hello\\\\world\\\\again\\\\',
|
||||||
|
'\\hello\\world\\again'
|
||||||
|
)
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'/hello///world//again//',
|
||||||
|
'\\hello\\world\\again'
|
||||||
|
)
|
||||||
|
// UNC:
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello\\world\\', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'\\\\hello\\world\\\\',
|
||||||
|
'\\\\hello\\world'
|
||||||
|
)
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'\\\\hello\\\\\\world\\\\again\\',
|
||||||
|
'\\\\hello\\world\\again'
|
||||||
|
)
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world/', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world//', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator(
|
||||||
|
'//hello//world//again/',
|
||||||
|
'\\\\hello\\world\\again'
|
||||||
|
)
|
||||||
|
// Relative:
|
||||||
|
assertSafeTrimTrailingSeparator('hello\\world\\', 'hello\\world')
|
||||||
|
|
||||||
|
// Slash trimming
|
||||||
|
assertSafeTrimTrailingSeparator('a:/hello/', 'a:\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('z:/hello', 'z:\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('C:/', 'C:\\')
|
||||||
|
assertSafeTrimTrailingSeparator('C:\\', 'C:\\')
|
||||||
|
assertSafeTrimTrailingSeparator('C:/hello/world', 'C:\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('C:/hello/world/', 'C:\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('C:', 'C:')
|
||||||
|
assertSafeTrimTrailingSeparator('C:hello/', 'C:hello')
|
||||||
|
assertSafeTrimTrailingSeparator('/', '\\')
|
||||||
|
assertSafeTrimTrailingSeparator('/hello/', '\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('\\', '\\')
|
||||||
|
assertSafeTrimTrailingSeparator('\\hello\\', '\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/', '\\\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world/', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello', '\\\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello\\', '\\\\hello')
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello\\world', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello\\world\\', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('hello/world/', 'hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('hello/', 'hello')
|
||||||
|
assertSafeTrimTrailingSeparator('../../', '..\\..')
|
||||||
|
} else {
|
||||||
|
// Should not converts slashes
|
||||||
|
assertSafeTrimTrailingSeparator('/hello\\world', '/hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('/hello\\world/', '/hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('\\\\hello\\world/', '\\\\hello\\world')
|
||||||
|
assertSafeTrimTrailingSeparator('hello\\world/', 'hello\\world')
|
||||||
|
|
||||||
|
// Should remove redundant slashes (rooted paths; UNC format not special)
|
||||||
|
assertSafeTrimTrailingSeparator('//hello', '/hello')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world', '/hello/world')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello/world/', '/hello/world')
|
||||||
|
assertSafeTrimTrailingSeparator('//hello//world//', '/hello/world')
|
||||||
|
assertSafeTrimTrailingSeparator('///hello////world///', '/hello/world')
|
||||||
|
|
||||||
|
// Should remove redundant slashes (relative paths)
|
||||||
|
assertSafeTrimTrailingSeparator('hello//world//', 'hello/world')
|
||||||
|
assertSafeTrimTrailingSeparator('hello///world///', 'hello/world')
|
||||||
|
|
||||||
|
// Slash trimming (Windows drive root format not special)
|
||||||
|
assertSafeTrimTrailingSeparator('C:/', 'C:')
|
||||||
|
assertSafeTrimTrailingSeparator('C:/hello', 'C:/hello')
|
||||||
|
assertSafeTrimTrailingSeparator('C:/hello/', 'C:/hello')
|
||||||
|
assertSafeTrimTrailingSeparator('C:hello/', 'C:hello')
|
||||||
|
|
||||||
|
// Slash trimming (rooted paths)
|
||||||
|
assertSafeTrimTrailingSeparator('/', '/')
|
||||||
|
assertSafeTrimTrailingSeparator('/hello', '/hello')
|
||||||
|
assertSafeTrimTrailingSeparator('/hello/', '/hello')
|
||||||
|
assertSafeTrimTrailingSeparator('/hello/world/', '/hello/world')
|
||||||
|
|
||||||
|
// Slash trimming (relative paths)
|
||||||
|
assertSafeTrimTrailingSeparator('hello/world/', 'hello/world')
|
||||||
|
assertSafeTrimTrailingSeparator('../../', '../..')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function assertDirectoryName(itemPath: string, expected: string): void {
|
||||||
|
expect(pathHelper.dirname(itemPath)).toBe(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertEnsureAbsoluteRoot(
|
||||||
|
root: string,
|
||||||
|
itemPath: string,
|
||||||
|
expected: string
|
||||||
|
): void {
|
||||||
|
expect(pathHelper.ensureAbsoluteRoot(root, itemPath)).toBe(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertHasAbsoluteRoot(itemPath: string, expected: boolean): void {
|
||||||
|
expect(pathHelper.hasAbsoluteRoot(itemPath)).toBe(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertHasRoot(itemPath: string, expected: boolean): void {
|
||||||
|
expect(pathHelper.hasRoot(itemPath)).toBe(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNormalizeSeparators(itemPath: string, expected: string): void {
|
||||||
|
expect(pathHelper.normalizeSeparators(itemPath)).toBe(expected)
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertSafeTrimTrailingSeparator(
|
||||||
|
itemPath: string,
|
||||||
|
expected: string
|
||||||
|
): void {
|
||||||
|
expect(pathHelper.safeTrimTrailingSeparator(itemPath)).toBe(expected)
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import {Path} from '../src/internal-path'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
describe('path', () => {
|
||||||
|
it('constructs from rooted path', () => {
|
||||||
|
assertPath(`/`, `${path.sep}`, [path.sep])
|
||||||
|
assertPath(`/foo`, `${path.sep}foo`, [path.sep, 'foo'])
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
assertPath(`C:\\foo`, `C:\\foo`, ['C:\\', 'foo'])
|
||||||
|
assertPath(`C:foo`, `C:foo`, ['C:', 'foo'])
|
||||||
|
assertPath(`\\\\foo\\bar\\baz`, `\\\\foo\\bar\\baz`, [
|
||||||
|
'\\\\foo\\bar',
|
||||||
|
'baz'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('constructs from rooted segments', () => {
|
||||||
|
assertPath([`/`], `${path.sep}`, [path.sep])
|
||||||
|
assertPath([`/`, `foo`], `${path.sep}foo`, [path.sep, 'foo'])
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
assertPath([`C:\\`, `foo`], `C:\\foo`, ['C:\\', 'foo'])
|
||||||
|
assertPath([`C:`, `foo`], `C:foo`, ['C:', 'foo'])
|
||||||
|
assertPath([`\\\\foo\\bar`, `baz`], `\\\\foo\\bar\\baz`, [
|
||||||
|
'\\\\foo\\bar',
|
||||||
|
'baz'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('constructs from relative path', () => {
|
||||||
|
assertPath(`foo`, `foo`, ['foo'])
|
||||||
|
assertPath(`foo/bar`, `foo${path.sep}bar`, ['foo', 'bar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('constructs from relative segments', () => {
|
||||||
|
assertPath([`foo`], `foo`, ['foo'])
|
||||||
|
assertPath([`foo`, `bar`], `foo${path.sep}bar`, ['foo', 'bar'])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('normalizes slashes', () => {
|
||||||
|
assertPath(
|
||||||
|
`/foo///bar${path.sep}${path.sep}${path.sep}baz`,
|
||||||
|
`${path.sep}foo${path.sep}bar${path.sep}baz`,
|
||||||
|
[path.sep, 'foo', 'bar', 'baz']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('preserves relative pathing', () => {
|
||||||
|
assertPath(
|
||||||
|
'/foo/../bar/./baz',
|
||||||
|
`${path.sep}foo${path.sep}..${path.sep}bar${path.sep}.${path.sep}baz`,
|
||||||
|
[path.sep, 'foo', '..', 'bar', '.', 'baz']
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims unnecessary trailing slash', () => {
|
||||||
|
assertPath('/', path.sep, [path.sep])
|
||||||
|
assertPath('/foo/', `${path.sep}foo`, [path.sep, 'foo'])
|
||||||
|
assertPath('foo/', 'foo', ['foo'])
|
||||||
|
assertPath('foo/bar/', `foo${path.sep}bar`, ['foo', 'bar'])
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
assertPath('\\', '\\', ['\\'])
|
||||||
|
assertPath('C:\\', 'C:\\', ['C:\\'])
|
||||||
|
assertPath('C:\\foo\\', 'C:\\foo', ['C:\\', 'foo'])
|
||||||
|
assertPath('C:foo\\', 'C:foo', ['C:', 'foo'])
|
||||||
|
assertPath('\\\\computer\\share\\', '\\\\computer\\share', [
|
||||||
|
'\\\\computer\\share'
|
||||||
|
])
|
||||||
|
assertPath('\\\\computer\\share\\foo', '\\\\computer\\share\\foo', [
|
||||||
|
'\\\\computer\\share',
|
||||||
|
'foo'
|
||||||
|
])
|
||||||
|
assertPath('\\\\computer\\share\\foo\\', '\\\\computer\\share\\foo', [
|
||||||
|
'\\\\computer\\share',
|
||||||
|
'foo'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function assertPath(
|
||||||
|
itemPath: string | string[],
|
||||||
|
expectedPath: string,
|
||||||
|
expectedSegments: string[]
|
||||||
|
): void {
|
||||||
|
const actual = new Path(itemPath)
|
||||||
|
expect(actual.toString()).toBe(expectedPath)
|
||||||
|
expect(actual.segments).toEqual(expectedSegments)
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as patternHelper from '../src/internal-pattern-helper'
|
||||||
|
import {MatchKind} from '../src/internal-match-kind'
|
||||||
|
import {IS_WINDOWS} from '../../io/src/io-util'
|
||||||
|
|
||||||
|
describe('pattern-helper', () => {
|
||||||
|
it('getSearchPaths omits negate search paths', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
`${root}search1/foo/**`,
|
||||||
|
`${root}search2/bar/**`,
|
||||||
|
`!${root}search3/baz/**`
|
||||||
|
],
|
||||||
|
patternHelper.getOptions()
|
||||||
|
)
|
||||||
|
const searchPaths = patternHelper.getSearchPaths(patterns)
|
||||||
|
expect(searchPaths).toEqual([
|
||||||
|
`${root}search1${path.sep}foo`,
|
||||||
|
`${root}search2${path.sep}bar`
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('getSearchPaths omits search path when ancestor is also a search path', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
'C:\\Search1\\Foo\\**',
|
||||||
|
'C:\\sEARCH1\\fOO\\bar\\**',
|
||||||
|
'C:\\sEARCH1\\foo\\bar',
|
||||||
|
'C:\\Search2\\**',
|
||||||
|
'C:\\Search3\\Foo\\Bar\\**',
|
||||||
|
'C:\\sEARCH3\\fOO\\bAR\\**'
|
||||||
|
],
|
||||||
|
patternHelper.getOptions()
|
||||||
|
)
|
||||||
|
const searchPaths = patternHelper.getSearchPaths(patterns)
|
||||||
|
expect(searchPaths).toEqual([
|
||||||
|
'C:\\Search1\\Foo',
|
||||||
|
'C:\\Search2',
|
||||||
|
'C:\\Search3\\Foo\\Bar'
|
||||||
|
])
|
||||||
|
} else {
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
'/search1/foo/**',
|
||||||
|
'/search1/foo/bar/**',
|
||||||
|
'/search2/foo/bar',
|
||||||
|
'/search2/**',
|
||||||
|
'/search3/foo/bar/**',
|
||||||
|
'/search3/foo/bar/**'
|
||||||
|
],
|
||||||
|
patternHelper.getOptions()
|
||||||
|
)
|
||||||
|
const searchPaths = patternHelper.getSearchPaths(patterns)
|
||||||
|
expect(searchPaths).toEqual([
|
||||||
|
'/search1/foo',
|
||||||
|
'/search2',
|
||||||
|
'/search3/foo/bar'
|
||||||
|
])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('match supports interleaved exclude patterns', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const itemPaths = [
|
||||||
|
`${root}solution1/proj1/proj1.proj`,
|
||||||
|
`${root}solution1/proj1/README.txt`,
|
||||||
|
`${root}solution1/proj2/proj2.proj`,
|
||||||
|
`${root}solution1/proj2/README.txt`,
|
||||||
|
`${root}solution1/solution1.sln`,
|
||||||
|
`${root}solution2/proj1/proj1.proj`,
|
||||||
|
`${root}solution2/proj1/README.txt`,
|
||||||
|
`${root}solution2/proj2/proj2.proj`,
|
||||||
|
`${root}solution2/proj2/README.txt`,
|
||||||
|
`${root}solution2/solution2.sln`
|
||||||
|
]
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
`${root}**/*.proj`, // include all proj files
|
||||||
|
`${root}**/README.txt`, // include all README files
|
||||||
|
`!${root}**/solution2/**`, // exclude the solution 2 folder entirely
|
||||||
|
`${root}**/*.sln`, // include all sln files
|
||||||
|
`!${root}**/proj2/README.txt` // exclude proj2 README files
|
||||||
|
],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
const matched = itemPaths.filter(
|
||||||
|
x => patternHelper.match(patterns, x) === MatchKind.All
|
||||||
|
)
|
||||||
|
expect(matched).toEqual([
|
||||||
|
`${root}solution1/proj1/proj1.proj`,
|
||||||
|
`${root}solution1/proj1/README.txt`,
|
||||||
|
`${root}solution1/proj2/proj2.proj`,
|
||||||
|
`${root}solution1/solution1.sln`,
|
||||||
|
`${root}solution2/solution2.sln`
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('match supports excluding directories', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const itemPaths = [
|
||||||
|
root,
|
||||||
|
`${root}foo`,
|
||||||
|
`${root}foo/bar`,
|
||||||
|
`${root}foo/bar/baz`
|
||||||
|
]
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
`${root}foo/**`, // include all files and directories
|
||||||
|
`!${root}foo/**/` // exclude directories
|
||||||
|
],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
const matchKinds = itemPaths.map(x => patternHelper.match(patterns, x))
|
||||||
|
expect(matchKinds).toEqual([
|
||||||
|
MatchKind.None,
|
||||||
|
MatchKind.File,
|
||||||
|
MatchKind.File,
|
||||||
|
MatchKind.File
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('match supports including directories only', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const itemPaths = [
|
||||||
|
root,
|
||||||
|
`${root}foo/`,
|
||||||
|
`${root}foo/bar`,
|
||||||
|
`${root}foo/bar/baz`
|
||||||
|
]
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
`${root}foo/**/` // include directories only
|
||||||
|
],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
const matchKinds = itemPaths.map(x => patternHelper.match(patterns, x))
|
||||||
|
expect(matchKinds).toEqual([
|
||||||
|
MatchKind.None,
|
||||||
|
MatchKind.Directory,
|
||||||
|
MatchKind.Directory,
|
||||||
|
MatchKind.Directory
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parse skips comments', () => {
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
['# comment 1', ' # comment 2', '!#hello-world.txt'],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
expect(patterns).toHaveLength(1)
|
||||||
|
expect(patterns[0].negate).toBeTruthy()
|
||||||
|
expect(patterns[0].segments.reverse()[0]).toEqual('#hello-world.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('parse skips empty patterns', () => {
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
['', ' ', 'hello-world.txt'],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
expect(patterns).toHaveLength(1)
|
||||||
|
expect(patterns[0].segments.reverse()[0]).toEqual('hello-world.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('partialMatch skips negate patterns', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const patterns = patternHelper.parse(
|
||||||
|
[
|
||||||
|
`${root}search1/foo/**`,
|
||||||
|
`${root}search2/bar/**`,
|
||||||
|
`!${root}search2/bar/**`,
|
||||||
|
`!${root}search3/baz/**`
|
||||||
|
],
|
||||||
|
patternHelper.getOptions({implicitDescendants: false})
|
||||||
|
)
|
||||||
|
expect(patternHelper.partialMatch(patterns, `${root}search1`)).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
patternHelper.partialMatch(patterns, `${root}search1/foo`)
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(patternHelper.partialMatch(patterns, `${root}search2`)).toBeTruthy()
|
||||||
|
expect(
|
||||||
|
patternHelper.partialMatch(patterns, `${root}search2/bar`)
|
||||||
|
).toBeTruthy()
|
||||||
|
expect(patternHelper.partialMatch(patterns, `${root}search3`)).toBeFalsy()
|
||||||
|
expect(
|
||||||
|
patternHelper.partialMatch(patterns, `${root}search3/bar`)
|
||||||
|
).toBeFalsy()
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,366 @@
|
||||||
|
import * as io from '../../io/src/io'
|
||||||
|
import * as path from 'path'
|
||||||
|
import {MatchKind} from '../src/internal-match-kind'
|
||||||
|
import {promises as fs} from 'fs'
|
||||||
|
|
||||||
|
// Mock 'os' before importing Pattern
|
||||||
|
/* eslint-disable import/first */
|
||||||
|
/* eslint-disable @typescript-eslint/promise-function-async */
|
||||||
|
// Note, @typescript-eslint/promise-function-async is a false positive due to the
|
||||||
|
// mock factory delegate which returns any. Fixed in a future version of jest.
|
||||||
|
jest.mock('os', () => jest.requireActual('os'))
|
||||||
|
const os = jest.requireMock('os')
|
||||||
|
import {Pattern} from '../src/internal-pattern'
|
||||||
|
jest.resetModuleRegistry()
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
describe('pattern', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await io.rmRF(getTestTemp())
|
||||||
|
})
|
||||||
|
|
||||||
|
it('counts leading negate markers', () => {
|
||||||
|
const actual = [
|
||||||
|
'/initial-includes/*.txt',
|
||||||
|
'!!/hello/two-negate-markers.txt',
|
||||||
|
'!!!!/hello/four-negate-markers.txt',
|
||||||
|
'!/initial-includes/one-negate-markers.txt',
|
||||||
|
'!!!/initial-includes/three-negate-markers.txt'
|
||||||
|
].map(x => new Pattern(x).negate)
|
||||||
|
expect(actual).toEqual([false, false, false, true, true])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('escapes homedir', async () => {
|
||||||
|
const originalHomedir = os.homedir
|
||||||
|
const home = path.join(getTestTemp(), 'home-with-[and]')
|
||||||
|
await fs.mkdir(home, {recursive: true})
|
||||||
|
try {
|
||||||
|
os.homedir = () => home
|
||||||
|
const pattern = new Pattern('~/m*')
|
||||||
|
|
||||||
|
expect(pattern.searchPath).toBe(home)
|
||||||
|
expect(pattern.match(path.join(home, 'match'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(home, 'not-match'))).toBeFalsy()
|
||||||
|
} finally {
|
||||||
|
os.homedir = originalHomedir
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('escapes root', async () => {
|
||||||
|
const originalCwd = process.cwd()
|
||||||
|
const rootPath = path.join(getTestTemp(), 'cwd-with-[and]')
|
||||||
|
await fs.mkdir(rootPath, {recursive: true})
|
||||||
|
try {
|
||||||
|
process.chdir(rootPath)
|
||||||
|
|
||||||
|
// Relative
|
||||||
|
let pattern = new Pattern('m*')
|
||||||
|
expect(pattern.searchPath).toBe(rootPath)
|
||||||
|
expect(pattern.match(path.join(rootPath, 'match'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(rootPath, 'not-match'))).toBeFalsy()
|
||||||
|
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const currentDrive = process.cwd().substr(0, 2)
|
||||||
|
expect(currentDrive.match(/^[A-Z]:$/i)).toBeTruthy()
|
||||||
|
|
||||||
|
// Relative current drive letter, e.g. C:m*
|
||||||
|
pattern = new Pattern(`${currentDrive}m*`)
|
||||||
|
expect(pattern.searchPath).toBe(rootPath)
|
||||||
|
expect(pattern.match(path.join(rootPath, 'match'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(rootPath, 'not-match'))).toBeFalsy()
|
||||||
|
|
||||||
|
// Relative current drive, e.g. \path\to\cwd\m*
|
||||||
|
pattern = new Pattern(
|
||||||
|
`${Pattern.globEscape(process.cwd().substr(2))}\\m*`
|
||||||
|
)
|
||||||
|
expect(pattern.searchPath).toBe(rootPath)
|
||||||
|
expect(pattern.match(path.join(rootPath, 'match'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(rootPath, 'not-match'))).toBeFalsy()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
process.chdir(originalCwd)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('globstar matches immediately preceeding directory', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const pattern = new Pattern(`${root}foo/bar/**`)
|
||||||
|
const actual = [
|
||||||
|
root,
|
||||||
|
`${root}foo`,
|
||||||
|
`${root}foo/bar`,
|
||||||
|
`${root}foo/bar/baz`
|
||||||
|
].map(x => pattern.match(x))
|
||||||
|
expect(actual).toEqual([
|
||||||
|
MatchKind.None,
|
||||||
|
MatchKind.None,
|
||||||
|
MatchKind.All,
|
||||||
|
MatchKind.All
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is case insensitive match on Windows', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const pattern = new Pattern(`${root}Foo/**/Baz`)
|
||||||
|
expect(pattern.match(`${root}Foo/Baz`)).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(`${root}Foo/bAZ`)).toBe(
|
||||||
|
IS_WINDOWS ? MatchKind.All : MatchKind.None
|
||||||
|
)
|
||||||
|
expect(pattern.match(`${root}fOO/Baz`)).toBe(
|
||||||
|
IS_WINDOWS ? MatchKind.All : MatchKind.None
|
||||||
|
)
|
||||||
|
expect(pattern.match(`${root}fOO/bar/bAZ`)).toBe(
|
||||||
|
IS_WINDOWS ? MatchKind.All : MatchKind.None
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('is case insensitive partial match on Windows', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const pattern = new Pattern(`${root}Foo/Bar/**/Baz`)
|
||||||
|
expect(pattern.partialMatch(`${root}Foo`)).toBeTruthy()
|
||||||
|
expect(pattern.partialMatch(`${root}fOO`)).toBe(IS_WINDOWS ? true : false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('matches root', () => {
|
||||||
|
const pattern = new Pattern(IS_WINDOWS ? 'C:\\**' : '/**')
|
||||||
|
expect(pattern.match(IS_WINDOWS ? 'C:\\' : '/')).toBe(MatchKind.All)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('partial matches root', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
let pattern = new Pattern('C:\\foo\\**')
|
||||||
|
expect(pattern.partialMatch('c:\\')).toBeTruthy()
|
||||||
|
pattern = new Pattern('c:\\foo\\**')
|
||||||
|
expect(pattern.partialMatch('C:\\')).toBeTruthy()
|
||||||
|
} else {
|
||||||
|
const pattern = new Pattern('/foo/**')
|
||||||
|
expect(pattern.partialMatch('/')).toBeTruthy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces leading . segment', () => {
|
||||||
|
// Pattern is '.'
|
||||||
|
let pattern = new Pattern('.')
|
||||||
|
expect(pattern.match(process.cwd())).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBe(MatchKind.None)
|
||||||
|
|
||||||
|
// Pattern is './foo'
|
||||||
|
pattern = new Pattern('./foo')
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'bar'))).toBe(MatchKind.None)
|
||||||
|
|
||||||
|
// Pattern is '.foo'
|
||||||
|
pattern = new Pattern('.foo')
|
||||||
|
expect(pattern.match(path.join(process.cwd(), '.foo'))).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBe(MatchKind.None)
|
||||||
|
expect(pattern.match(`${process.cwd()}foo`)).toBe(MatchKind.None)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces leading ~ segment', async () => {
|
||||||
|
const homedir = os.homedir()
|
||||||
|
expect(homedir).toBeTruthy()
|
||||||
|
await fs.stat(homedir)
|
||||||
|
|
||||||
|
// Pattern is '~'
|
||||||
|
let pattern = new Pattern('~')
|
||||||
|
expect(pattern.match(homedir)).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(homedir, 'foo'))).toBe(MatchKind.None)
|
||||||
|
|
||||||
|
// Pattern is '~/foo'
|
||||||
|
pattern = new Pattern('~/foo')
|
||||||
|
expect(pattern.match(path.join(homedir, 'foo'))).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(homedir, 'bar'))).toBe(MatchKind.None)
|
||||||
|
|
||||||
|
// Pattern is '~foo'
|
||||||
|
pattern = new Pattern('~foo')
|
||||||
|
expect(pattern.match(path.join(process.cwd(), '~foo'))).toBe(MatchKind.All)
|
||||||
|
expect(pattern.match(path.join(homedir, 'foo'))).toBe(MatchKind.None)
|
||||||
|
expect(pattern.match(`${homedir}foo`)).toBe(MatchKind.None)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('replaces leading relative root', () => {
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
const currentDrive = process.cwd().substr(0, 2)
|
||||||
|
expect(currentDrive.match(/^[A-Z]:$/i)).toBeTruthy()
|
||||||
|
const otherDrive = currentDrive.toUpperCase().startsWith('C')
|
||||||
|
? 'D:'
|
||||||
|
: 'C:'
|
||||||
|
expect(process.cwd().length).toBeGreaterThan(3) // sanity check not drive root
|
||||||
|
|
||||||
|
// Pattern is 'C:'
|
||||||
|
let pattern = new Pattern(currentDrive)
|
||||||
|
expect(pattern.match(process.cwd())).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBeFalsy()
|
||||||
|
|
||||||
|
// Pattern is 'C:foo'
|
||||||
|
pattern = new Pattern(`${currentDrive}foo`)
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'bar'))).toBeFalsy()
|
||||||
|
expect(pattern.match(`${currentDrive}\\foo`)).toBeFalsy()
|
||||||
|
|
||||||
|
// Pattern is 'X:'
|
||||||
|
pattern = new Pattern(otherDrive)
|
||||||
|
expect(pattern.match(`${otherDrive}\\`)).toBeTruthy()
|
||||||
|
expect(pattern.match(`${otherDrive}\\foo`)).toBeFalsy()
|
||||||
|
|
||||||
|
// Pattern is 'X:foo'
|
||||||
|
pattern = new Pattern(`${otherDrive}foo`)
|
||||||
|
expect(pattern.match(`${otherDrive}\\foo`)).toBeTruthy()
|
||||||
|
expect(pattern.match(`${otherDrive}\\bar`)).toBeFalsy()
|
||||||
|
|
||||||
|
// Pattern is '\\path\\to\\cwd'
|
||||||
|
pattern = new Pattern(`${process.cwd().substr(2)}\\foo`)
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'foo'))).toBeTruthy()
|
||||||
|
expect(pattern.match(path.join(process.cwd(), 'bar'))).toBeFalsy()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('roots exclude pattern', () => {
|
||||||
|
const patternStrings = ['!hello.txt', '!**/world.txt']
|
||||||
|
const actual = patternStrings.map(x => new Pattern(x))
|
||||||
|
const expected = patternStrings
|
||||||
|
.map(x => x.substr(1))
|
||||||
|
.map(x => path.join(Pattern.globEscape(process.cwd()), x))
|
||||||
|
.map(x => `!${x}`)
|
||||||
|
.map(x => new Pattern(x))
|
||||||
|
expect(actual.map(x => x.negate)).toEqual([true, true])
|
||||||
|
expect(actual.map(x => x.segments)).toEqual(expected.map(x => x.segments))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('roots include pattern', () => {
|
||||||
|
const patternStrings = ['hello.txt', '**/world.txt']
|
||||||
|
const actual = patternStrings.map(x => new Pattern(x))
|
||||||
|
const expected = patternStrings.map(
|
||||||
|
x => new Pattern(path.join(Pattern.globEscape(process.cwd()), x))
|
||||||
|
)
|
||||||
|
expect(actual.map(x => x.segments)).toEqual(expected.map(x => x.segments))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('sets trailing separator', () => {
|
||||||
|
expect(new Pattern(' foo ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' /foo ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern('! /foo ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' /foo/* ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' /foo/** ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' \\foo ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern('! \\foo ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' \\foo\\* ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' \\foo\\** ').trailingSeparator).toBeFalsy()
|
||||||
|
expect(new Pattern(' foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' /foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' C:/foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' C:foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' D:foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern('! /foo/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' /foo/*/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' /foo/**/ ').trailingSeparator).toBeTruthy()
|
||||||
|
expect(new Pattern(' foo\\ ').trailingSeparator).toEqual(
|
||||||
|
IS_WINDOWS ? true : false
|
||||||
|
)
|
||||||
|
expect(new Pattern(' \\foo\\ ').trailingSeparator).toEqual(
|
||||||
|
IS_WINDOWS ? true : false
|
||||||
|
)
|
||||||
|
expect(new Pattern('! \\foo\\ ').trailingSeparator).toEqual(
|
||||||
|
IS_WINDOWS ? true : false
|
||||||
|
)
|
||||||
|
expect(new Pattern(' \\foo\\*\\ ').trailingSeparator).toEqual(
|
||||||
|
IS_WINDOWS ? true : false
|
||||||
|
)
|
||||||
|
expect(new Pattern(' \\foo\\**\\ ').trailingSeparator).toEqual(
|
||||||
|
IS_WINDOWS ? true : false
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('supports including directories only', () => {
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
const pattern = new Pattern(`${root}foo/**/`) // trailing slash
|
||||||
|
const actual = [
|
||||||
|
root,
|
||||||
|
`${root}foo/`,
|
||||||
|
`${root}foo/bar`,
|
||||||
|
`${root}foo/bar/baz`
|
||||||
|
].map(x => pattern.match(x))
|
||||||
|
expect(pattern.trailingSeparator).toBeTruthy()
|
||||||
|
expect(actual).toEqual([
|
||||||
|
MatchKind.None,
|
||||||
|
MatchKind.Directory,
|
||||||
|
MatchKind.Directory,
|
||||||
|
MatchKind.Directory
|
||||||
|
])
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims pattern', () => {
|
||||||
|
const pattern = new Pattern(' hello.txt ')
|
||||||
|
expect(pattern.segments.reverse()[0]).toBe('hello.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('trims whitespace after trimming negate markers', () => {
|
||||||
|
const pattern = new Pattern(' ! ! ! hello.txt ')
|
||||||
|
expect(pattern.negate).toBeTruthy()
|
||||||
|
expect(pattern.segments.reverse()[0]).toBe('hello.txt')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unescapes segments to narrow search path', () => {
|
||||||
|
// Positive
|
||||||
|
const root = IS_WINDOWS ? 'C:\\' : '/'
|
||||||
|
let pattern = new Pattern(`${root}foo/b[a]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}bar`)
|
||||||
|
expect(pattern.match(`${root}foo/bar/baz`)).toBeTruthy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[*]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b*r`)
|
||||||
|
expect(pattern.match(`${root}foo/b*r/baz`)).toBeTruthy()
|
||||||
|
expect(pattern.match(`${root}foo/bar/baz`)).toBeFalsy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[?]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b?r`)
|
||||||
|
expect(pattern.match(`${root}foo/b?r/baz`)).toBeTruthy()
|
||||||
|
expect(pattern.match(`${root}foo/bar/baz`)).toBeFalsy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[!]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b!r`)
|
||||||
|
expect(pattern.match(`${root}foo/b!r/baz`)).toBeTruthy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[[]ar/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b[ar`)
|
||||||
|
expect(pattern.match(`${root}foo/b[ar/baz`)).toBeTruthy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b[]r`)
|
||||||
|
expect(pattern.match(`${root}foo/b[]r/baz`)).toBeTruthy()
|
||||||
|
pattern = new Pattern(`${root}foo/b[r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b[r`)
|
||||||
|
expect(pattern.match(`${root}foo/b[r/baz`)).toBeTruthy()
|
||||||
|
pattern = new Pattern(`${root}foo/b]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo${path.sep}b]r`)
|
||||||
|
expect(pattern.match(`${root}foo/b]r/baz`)).toBeTruthy()
|
||||||
|
if (!IS_WINDOWS) {
|
||||||
|
pattern = new Pattern('/foo/b\\[a]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe(`${path.sep}foo${path.sep}b[a]r`)
|
||||||
|
expect(pattern.match('/foo/b[a]r/baz')).toBeTruthy()
|
||||||
|
pattern = new Pattern('/foo/b[\\!]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe(`${path.sep}foo${path.sep}b!r`)
|
||||||
|
expect(pattern.match('/foo/b!r/baz')).toBeTruthy()
|
||||||
|
pattern = new Pattern('/foo/b[\\]]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe(`${path.sep}foo${path.sep}b]r`)
|
||||||
|
expect(pattern.match('/foo/b]r/baz')).toBeTruthy()
|
||||||
|
pattern = new Pattern('/foo/b[\\a]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe(`${path.sep}foo${path.sep}bar`)
|
||||||
|
expect(pattern.match('/foo/bar/baz')).toBeTruthy()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negative
|
||||||
|
pattern = new Pattern(`${root}foo/b[aA]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo`)
|
||||||
|
pattern = new Pattern(`${root}foo/b[!a]r/b*`)
|
||||||
|
expect(pattern.searchPath).toBe(`${root}foo`)
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
pattern = new Pattern('C:/foo/b\\[a]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe(`C:\\foo\\b\\ar`)
|
||||||
|
expect(pattern.match('C:/foo/b/ar/baz')).toBeTruthy()
|
||||||
|
pattern = new Pattern('C:/foo/b[\\!]r/b*')
|
||||||
|
expect(pattern.searchPath).toBe('C:\\foo\\b[\\!]r')
|
||||||
|
expect(pattern.match('C:/foo/b[undefined/!]r/baz')).toBeTruthy() // Note, "undefined" substr to accommodate a bug in Minimatch when nocase=true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function getTestTemp(): string {
|
||||||
|
return path.join(__dirname, '_temp', 'internal-pattern')
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
{
|
||||||
|
"name": "@actions/glob",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 1,
|
||||||
|
"requires": true,
|
||||||
|
"dependencies": {
|
||||||
|
"balanced-match": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
|
||||||
|
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
|
||||||
|
},
|
||||||
|
"brace-expansion": {
|
||||||
|
"version": "1.1.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||||
|
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||||
|
"requires": {
|
||||||
|
"balanced-match": "^1.0.0",
|
||||||
|
"concat-map": "0.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"concat-map": {
|
||||||
|
"version": "0.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
|
||||||
|
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
|
||||||
|
},
|
||||||
|
"minimatch": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
|
||||||
|
"requires": {
|
||||||
|
"brace-expansion": "^1.1.7"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
{
|
||||||
|
"name": "@actions/glob",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"preview": true,
|
||||||
|
"description": "Actions glob lib",
|
||||||
|
"keywords": [
|
||||||
|
"github",
|
||||||
|
"actions",
|
||||||
|
"glob"
|
||||||
|
],
|
||||||
|
"homepage": "https://github.com/actions/toolkit/tree/master/packages/glob",
|
||||||
|
"license": "MIT",
|
||||||
|
"main": "lib/glob.js",
|
||||||
|
"types": "lib/glob.d.ts",
|
||||||
|
"directories": {
|
||||||
|
"lib": "lib",
|
||||||
|
"test": "__tests__"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"lib"
|
||||||
|
],
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/actions/toolkit.git",
|
||||||
|
"directory": "packages/glob"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: run tests from root\" && exit 1",
|
||||||
|
"tsc": "tsc"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/actions/toolkit/issues"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@actions/core": "^1.2.0",
|
||||||
|
"minimatch": "^3.0.4"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,183 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as fs from 'fs'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as patternHelper from './internal-pattern-helper'
|
||||||
|
import {IGlobOptions} from './internal-glob-options'
|
||||||
|
import {MatchKind} from './internal-match-kind'
|
||||||
|
import {Pattern} from './internal-pattern'
|
||||||
|
import {SearchState} from './internal-search-state'
|
||||||
|
|
||||||
|
export {IGlobOptions}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns files and directories matching the specified glob pattern.
|
||||||
|
*
|
||||||
|
* Order of the results is not guaranteed.
|
||||||
|
*/
|
||||||
|
export async function glob(
|
||||||
|
pattern: string,
|
||||||
|
options?: IGlobOptions
|
||||||
|
): Promise<string[]> {
|
||||||
|
const result: string[] = []
|
||||||
|
for await (const itemPath of globGenerator(pattern, options)) {
|
||||||
|
result.push(itemPath)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns files and directories matching the specified glob pattern.
|
||||||
|
*
|
||||||
|
* Order of the results is not guaranteed.
|
||||||
|
*/
|
||||||
|
export async function* globGenerator(
|
||||||
|
pattern: string,
|
||||||
|
options?: IGlobOptions
|
||||||
|
): AsyncGenerator<string, void> {
|
||||||
|
// Set defaults options
|
||||||
|
options = patternHelper.getOptions(options)
|
||||||
|
|
||||||
|
// Parse patterns
|
||||||
|
const patterns: Pattern[] = patternHelper.parse([pattern], options)
|
||||||
|
|
||||||
|
// Push the search paths
|
||||||
|
const stack: SearchState[] = []
|
||||||
|
for (const searchPath of patternHelper.getSearchPaths(patterns)) {
|
||||||
|
core.debug(`Search path '${searchPath}'`)
|
||||||
|
|
||||||
|
// Exists?
|
||||||
|
try {
|
||||||
|
// Intentionally using lstat. Detection for broken symlink
|
||||||
|
// will be performed later (if following symlinks).
|
||||||
|
await fs.promises.lstat(searchPath)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
|
||||||
|
stack.unshift(new SearchState(searchPath, 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search
|
||||||
|
const traversalChain: string[] = [] // used to detect cycles
|
||||||
|
while (stack.length) {
|
||||||
|
// Pop
|
||||||
|
const item = stack.pop() as SearchState
|
||||||
|
|
||||||
|
// Match?
|
||||||
|
const match = patternHelper.match(patterns, item.path)
|
||||||
|
const partialMatch =
|
||||||
|
!!match || patternHelper.partialMatch(patterns, item.path)
|
||||||
|
if (!match && !partialMatch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stat
|
||||||
|
const stats: fs.Stats | undefined = await stat(
|
||||||
|
item,
|
||||||
|
options,
|
||||||
|
traversalChain
|
||||||
|
)
|
||||||
|
|
||||||
|
// Broken symlink, or symlink cycle detected, or no longer exists
|
||||||
|
if (!stats) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory
|
||||||
|
if (stats.isDirectory()) {
|
||||||
|
// Matched
|
||||||
|
if (match & MatchKind.Directory) {
|
||||||
|
yield item.path
|
||||||
|
}
|
||||||
|
// Descend?
|
||||||
|
else if (!partialMatch) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the child items in reverse
|
||||||
|
const childLevel = item.level + 1
|
||||||
|
const childItems = (await fs.promises.readdir(item.path)).map(
|
||||||
|
x => new SearchState(path.join(item.path, x), childLevel)
|
||||||
|
)
|
||||||
|
stack.push(...childItems.reverse())
|
||||||
|
}
|
||||||
|
// File
|
||||||
|
else if (match & MatchKind.File) {
|
||||||
|
yield item.path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the search path preceeding the first segment that contains a pattern.
|
||||||
|
*
|
||||||
|
* For example, '/foo/bar*' returns '/foo'.
|
||||||
|
*/
|
||||||
|
export function getSearchPath(pattern: string): string {
|
||||||
|
const patterns: Pattern[] = patternHelper.parse(
|
||||||
|
[pattern],
|
||||||
|
patternHelper.getOptions()
|
||||||
|
)
|
||||||
|
const searchPaths: string[] = patternHelper.getSearchPaths(patterns)
|
||||||
|
return searchPaths.length > 0 ? searchPaths[0] : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function stat(
|
||||||
|
item: SearchState,
|
||||||
|
options: IGlobOptions,
|
||||||
|
traversalChain: string[]
|
||||||
|
): Promise<fs.Stats | undefined> {
|
||||||
|
// Note:
|
||||||
|
// `stat` returns info about the target of a symlink (or symlink chain)
|
||||||
|
// `lstat` returns info about a symlink itself
|
||||||
|
let stats: fs.Stats
|
||||||
|
if (options.followSymbolicLinks) {
|
||||||
|
try {
|
||||||
|
// Use `stat` (following symlinks)
|
||||||
|
stats = await fs.promises.stat(item.path)
|
||||||
|
} catch (err) {
|
||||||
|
if (err.code === 'ENOENT') {
|
||||||
|
if (options.omitBrokenSymbolicLinks) {
|
||||||
|
core.debug(`Broken symlink '${item.path}'`)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
`No information found for the path '${item.path}'. This may indicate a broken symbolic link.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Use `lstat` (not following symlinks)
|
||||||
|
stats = await fs.promises.lstat(item.path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note, isDirectory() returns false for the lstat of a symlink
|
||||||
|
if (stats.isDirectory() && options.followSymbolicLinks) {
|
||||||
|
// Get the realpath
|
||||||
|
const realPath: string = await fs.promises.realpath(item.path)
|
||||||
|
|
||||||
|
// Fixup the traversal chain to match the item level
|
||||||
|
while (traversalChain.length >= item.level) {
|
||||||
|
traversalChain.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test for a cycle
|
||||||
|
if (traversalChain.some((x: string) => x === realPath)) {
|
||||||
|
core.debug(
|
||||||
|
`Symlink cycle detected for path '${item.path}' and realpath '${realPath}'`
|
||||||
|
)
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the traversal chain
|
||||||
|
traversalChain.push(realPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
export interface IGlobOptions {
|
||||||
|
/**
|
||||||
|
* Indicates whether to follow symbolic links. Generally should be true
|
||||||
|
* unless deleting files.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
followSymbolicLinks?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether directories that match a glob pattern, should implicitly
|
||||||
|
* cause all descendant paths to be matched.
|
||||||
|
*
|
||||||
|
* For example, given the directory `my-dir`, the following glob patterns
|
||||||
|
* would produce the same results: `my-dir/**`, `my-dir/`, `my-dir`
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
implicitDescendants?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether broken symbolic should be ignored and omitted from the
|
||||||
|
* result set. Otherwise an error will be thrown.
|
||||||
|
*
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
omitBrokenSymbolicLinks?: boolean
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
/**
|
||||||
|
* Indicates whether a pattern matches a path
|
||||||
|
*/
|
||||||
|
export enum MatchKind {
|
||||||
|
/** Not matched */
|
||||||
|
None = 0,
|
||||||
|
|
||||||
|
/** Matched if the path is a directory */
|
||||||
|
Directory = 1,
|
||||||
|
|
||||||
|
/** Matched if the path is a regular file */
|
||||||
|
File = 2,
|
||||||
|
|
||||||
|
/** Matched */
|
||||||
|
All = Directory | File
|
||||||
|
}
|
|
@ -0,0 +1,206 @@
|
||||||
|
import * as assert from 'assert'
|
||||||
|
import * as path from 'path'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Similar to path.dirname except normalizes the path separators and slightly better handling for Windows UNC paths.
|
||||||
|
*
|
||||||
|
* For example, on Linux/macOS:
|
||||||
|
* - `/ => /`
|
||||||
|
* - `/hello => /`
|
||||||
|
*
|
||||||
|
* For example, on Windows:
|
||||||
|
* - `C:\ => C:\`
|
||||||
|
* - `C:\hello => C:\`
|
||||||
|
* - `C: => C:`
|
||||||
|
* - `C:hello => C:`
|
||||||
|
* - `\ => \`
|
||||||
|
* - `\hello => \`
|
||||||
|
* - `\\hello => \\hello`
|
||||||
|
* - `\\hello\world => \\hello\world`
|
||||||
|
*/
|
||||||
|
export function dirname(p: string): string {
|
||||||
|
// Normalize slashes and trim unnecessary trailing slash
|
||||||
|
p = safeTrimTrailingSeparator(p)
|
||||||
|
|
||||||
|
// Windows UNC root, e.g. \\hello or \\hello\world
|
||||||
|
if (IS_WINDOWS && /^\\\\[^\\]+(\\[^\\]+)?$/.test(p)) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get dirname
|
||||||
|
let result = path.dirname(p)
|
||||||
|
|
||||||
|
// Trim trailing slash for Windows UNC root, e.g. \\hello\world\
|
||||||
|
if (IS_WINDOWS && /^\\\\[^\\]+\\[^\\]+\\$/.test(result)) {
|
||||||
|
result = safeTrimTrailingSeparator(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roots the path if not already rooted. On Windows, relative roots like `\`
|
||||||
|
* or `C:` are expanded based on the current working directory.
|
||||||
|
*/
|
||||||
|
export function ensureAbsoluteRoot(root: string, itemPath: string): string {
|
||||||
|
assert(root, `ensureAbsoluteRoot parameter 'root' must not be empty`)
|
||||||
|
assert(itemPath, `ensureAbsoluteRoot parameter 'itemPath' must not be empty`)
|
||||||
|
|
||||||
|
// Already rooted
|
||||||
|
if (hasAbsoluteRoot(itemPath)) {
|
||||||
|
return itemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Check for itemPath like C: or C:foo
|
||||||
|
if (itemPath.match(/^[A-Z]:[^\\/]|^[A-Z]:$/i)) {
|
||||||
|
let cwd = process.cwd()
|
||||||
|
assert(
|
||||||
|
cwd.match(/^[A-Z]:\\/i),
|
||||||
|
`Expected current directory to start with an absolute drive root. Actual '${cwd}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Drive letter matches cwd? Expand to cwd
|
||||||
|
if (itemPath[0].toUpperCase() === cwd[0].toUpperCase()) {
|
||||||
|
// Drive only, e.g. C:
|
||||||
|
if (itemPath.length === 2) {
|
||||||
|
// Preserve specified drive letter case (upper or lower)
|
||||||
|
return `${itemPath[0]}:\\${cwd.substr(3)}`
|
||||||
|
}
|
||||||
|
// Drive + path, e.g. C:foo
|
||||||
|
else {
|
||||||
|
if (!cwd.endsWith('\\')) {
|
||||||
|
cwd += '\\'
|
||||||
|
}
|
||||||
|
// Preserve specified drive letter case (upper or lower)
|
||||||
|
return `${itemPath[0]}:\\${cwd.substr(3)}${itemPath.substr(2)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Different drive
|
||||||
|
else {
|
||||||
|
return `${itemPath[0]}:\\${itemPath.substr(2)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check for itemPath like \ or \foo
|
||||||
|
else if (normalizeSeparators(itemPath).match(/^\\$|^\\[^\\]/)) {
|
||||||
|
const cwd = process.cwd()
|
||||||
|
assert(
|
||||||
|
cwd.match(/^[A-Z]:\\/i),
|
||||||
|
`Expected current directory to start with an absolute drive root. Actual '${cwd}'`
|
||||||
|
)
|
||||||
|
|
||||||
|
return `${cwd[0]}:\\${itemPath.substr(1)}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(
|
||||||
|
hasAbsoluteRoot(root),
|
||||||
|
`ensureAbsoluteRoot parameter 'root' must have an absolute root`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Otherwise ensure root ends with a separator
|
||||||
|
if (root.endsWith('/') || (IS_WINDOWS && root.endsWith('\\'))) {
|
||||||
|
// Intentionally empty
|
||||||
|
} else {
|
||||||
|
// Append separator
|
||||||
|
root += path.sep
|
||||||
|
}
|
||||||
|
|
||||||
|
return root + itemPath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Linux/macOS, true if path starts with `/`. On Windows, true for paths like:
|
||||||
|
* `\\hello\share` and `C:\hello` (and using alternate separator).
|
||||||
|
*/
|
||||||
|
export function hasAbsoluteRoot(itemPath: string): boolean {
|
||||||
|
assert(itemPath, `hasAbsoluteRoot parameter 'itemPath' must not be empty`)
|
||||||
|
|
||||||
|
// Normalize separators
|
||||||
|
itemPath = normalizeSeparators(itemPath)
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// E.g. \\hello\share or C:\hello
|
||||||
|
return itemPath.startsWith('\\\\') || /^[A-Z]:\\/i.test(itemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// E.g. /hello
|
||||||
|
return itemPath.startsWith('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On Linux/macOS, true if path starts with `/`. On Windows, true for paths like:
|
||||||
|
* `\`, `\hello`, `\\hello\share`, `C:`, and `C:\hello` (and using alternate separator).
|
||||||
|
*/
|
||||||
|
export function hasRoot(itemPath: string): boolean {
|
||||||
|
assert(itemPath, `isRooted parameter 'itemPath' must not be empty`)
|
||||||
|
|
||||||
|
// Normalize separators
|
||||||
|
itemPath = normalizeSeparators(itemPath)
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// E.g. \ or \hello or \\hello
|
||||||
|
// E.g. C: or C:\hello
|
||||||
|
return itemPath.startsWith('\\') || /^[A-Z]:/i.test(itemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// E.g. /hello
|
||||||
|
return itemPath.startsWith('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes redundant slashes and converts `/` to `\` on Windows
|
||||||
|
*/
|
||||||
|
export function normalizeSeparators(p: string): string {
|
||||||
|
p = p || ''
|
||||||
|
|
||||||
|
// Windows
|
||||||
|
if (IS_WINDOWS) {
|
||||||
|
// Convert slashes on Windows
|
||||||
|
p = p.replace(/\//g, '\\')
|
||||||
|
|
||||||
|
// Remove redundant slashes
|
||||||
|
const isUnc = /^\\\\+[^\\]/.test(p) // e.g. \\hello
|
||||||
|
return (isUnc ? '\\' : '') + p.replace(/\\\\+/g, '\\') // preserve leading \\ for UNC
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove redundant slashes
|
||||||
|
return p.replace(/\/\/+/g, '/')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the path separators and trims the trailing separator (when safe).
|
||||||
|
* For example, `/foo/ => /foo` but `/ => /`
|
||||||
|
*/
|
||||||
|
export function safeTrimTrailingSeparator(p: string): string {
|
||||||
|
// Short-circuit if empty
|
||||||
|
if (!p) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize separators
|
||||||
|
p = normalizeSeparators(p)
|
||||||
|
|
||||||
|
// No trailing slash
|
||||||
|
if (!p.endsWith(path.sep)) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check '/' on Linux/macOS and '\' on Windows
|
||||||
|
if (p === path.sep) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// On Windows check if drive root. E.g. C:\
|
||||||
|
if (IS_WINDOWS && /^[A-Z]:\\$/i.test(p)) {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise trim trailing slash
|
||||||
|
return p.substr(0, p.length - 1)
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
import * as assert from 'assert'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as pathHelper from './internal-path-helper'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for parsing paths into segments
|
||||||
|
*/
|
||||||
|
export class Path {
|
||||||
|
segments: string[] = []
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a Path
|
||||||
|
* @param itemPath Path or array of segments
|
||||||
|
*/
|
||||||
|
constructor(itemPath: string | string[]) {
|
||||||
|
// String
|
||||||
|
if (typeof itemPath === 'string') {
|
||||||
|
assert(itemPath, `Parameter 'itemPath' must not be empty`)
|
||||||
|
|
||||||
|
// Normalize slashes and trim unnecessary trailing slash
|
||||||
|
itemPath = pathHelper.safeTrimTrailingSeparator(itemPath)
|
||||||
|
|
||||||
|
// Not rooted
|
||||||
|
if (!pathHelper.hasRoot(itemPath)) {
|
||||||
|
this.segments = itemPath.split(path.sep)
|
||||||
|
}
|
||||||
|
// Rooted
|
||||||
|
else {
|
||||||
|
// Add all segments, while not at the root
|
||||||
|
let remaining = itemPath
|
||||||
|
let dir = pathHelper.dirname(remaining)
|
||||||
|
while (dir !== remaining) {
|
||||||
|
// Add the segment
|
||||||
|
const basename = path.basename(remaining)
|
||||||
|
this.segments.unshift(basename)
|
||||||
|
|
||||||
|
// Truncate the last segment
|
||||||
|
remaining = dir
|
||||||
|
dir = pathHelper.dirname(remaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remainder is the root
|
||||||
|
this.segments.unshift(remaining)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Array
|
||||||
|
else {
|
||||||
|
// Must not be empty
|
||||||
|
assert(
|
||||||
|
itemPath.length > 0,
|
||||||
|
`Parameter 'itemPath' must not be an empty array`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Each segment
|
||||||
|
for (let i = 0; i < itemPath.length; i++) {
|
||||||
|
let segment = itemPath[i]
|
||||||
|
|
||||||
|
// Must not be empty
|
||||||
|
assert(
|
||||||
|
segment,
|
||||||
|
`Parameter 'itemPath' must not contain any empty segments`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Normalize slashes
|
||||||
|
segment = pathHelper.normalizeSeparators(itemPath[i])
|
||||||
|
|
||||||
|
// Root segment
|
||||||
|
if (i === 0 && pathHelper.hasRoot(segment)) {
|
||||||
|
segment = pathHelper.safeTrimTrailingSeparator(segment)
|
||||||
|
assert(
|
||||||
|
segment === pathHelper.dirname(segment),
|
||||||
|
`Parameter 'itemPath' root segment contains information for multiple segments`
|
||||||
|
)
|
||||||
|
this.segments.push(segment)
|
||||||
|
}
|
||||||
|
// All other segments
|
||||||
|
else {
|
||||||
|
// Must not contain slash
|
||||||
|
assert(
|
||||||
|
!segment.includes(path.sep),
|
||||||
|
`Parameter 'itemPath' contains unexpected path separators`
|
||||||
|
)
|
||||||
|
this.segments.push(segment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts the path to it's string representation
|
||||||
|
*/
|
||||||
|
toString(): string {
|
||||||
|
// First segment
|
||||||
|
let result = this.segments[0]
|
||||||
|
|
||||||
|
// All others
|
||||||
|
let skipSlash =
|
||||||
|
result.endsWith(path.sep) || (IS_WINDOWS && /^[A-Z]:$/i.test(result))
|
||||||
|
for (let i = 1; i < this.segments.length; i++) {
|
||||||
|
if (skipSlash) {
|
||||||
|
skipSlash = false
|
||||||
|
} else {
|
||||||
|
result += path.sep
|
||||||
|
}
|
||||||
|
|
||||||
|
result += this.segments[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
import * as core from '@actions/core'
|
||||||
|
import * as pathHelper from './internal-path-helper'
|
||||||
|
import {IGlobOptions} from './internal-glob-options'
|
||||||
|
import {MatchKind} from './internal-match-kind'
|
||||||
|
import {Pattern} from './internal-pattern'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a copy with defaults filled in
|
||||||
|
*/
|
||||||
|
export function getOptions(copy?: IGlobOptions): IGlobOptions {
|
||||||
|
const result: IGlobOptions = {
|
||||||
|
followSymbolicLinks: true,
|
||||||
|
implicitDescendants: true,
|
||||||
|
omitBrokenSymbolicLinks: true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (copy) {
|
||||||
|
if (typeof copy.followSymbolicLinks === 'boolean') {
|
||||||
|
result.followSymbolicLinks = copy.followSymbolicLinks
|
||||||
|
core.debug(`followSymbolicLinks '${result.followSymbolicLinks}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof copy.implicitDescendants === 'boolean') {
|
||||||
|
result.implicitDescendants = copy.implicitDescendants
|
||||||
|
core.debug(`implicitDescendants '${result.implicitDescendants}'`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof copy.omitBrokenSymbolicLinks === 'boolean') {
|
||||||
|
result.omitBrokenSymbolicLinks = copy.omitBrokenSymbolicLinks
|
||||||
|
core.debug(`omitBrokenSymbolicLinks '${result.omitBrokenSymbolicLinks}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of patterns, returns an array of paths to search.
|
||||||
|
* Duplicates and paths under other included paths are filtered out.
|
||||||
|
*/
|
||||||
|
export function getSearchPaths(patterns: Pattern[]): string[] {
|
||||||
|
// Ignore negate patterns
|
||||||
|
patterns = patterns.filter(x => !x.negate)
|
||||||
|
|
||||||
|
// Create a map of all search paths
|
||||||
|
const searchPathMap: {[key: string]: string} = {}
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
const key = IS_WINDOWS
|
||||||
|
? pattern.searchPath.toUpperCase()
|
||||||
|
: pattern.searchPath
|
||||||
|
searchPathMap[key] = 'candidate'
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: string[] = []
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
// Check if already included
|
||||||
|
const key = IS_WINDOWS
|
||||||
|
? pattern.searchPath.toUpperCase()
|
||||||
|
: pattern.searchPath
|
||||||
|
if (searchPathMap[key] === 'included') {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for an ancestor search path
|
||||||
|
let foundAncestor = false
|
||||||
|
let tempKey = key
|
||||||
|
let parent = pathHelper.dirname(tempKey)
|
||||||
|
while (parent !== tempKey) {
|
||||||
|
if (searchPathMap[parent]) {
|
||||||
|
foundAncestor = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
tempKey = parent
|
||||||
|
parent = pathHelper.dirname(tempKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include the search pattern in the result
|
||||||
|
if (!foundAncestor) {
|
||||||
|
result.push(pattern.searchPath)
|
||||||
|
searchPathMap[key] = 'included'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the patterns against the path
|
||||||
|
*/
|
||||||
|
export function match(patterns: Pattern[], itemPath: string): MatchKind {
|
||||||
|
let result: MatchKind = MatchKind.None
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.negate) {
|
||||||
|
result &= ~pattern.match(itemPath)
|
||||||
|
} else {
|
||||||
|
result |= pattern.match(itemPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the pattern strings into Pattern objects
|
||||||
|
*/
|
||||||
|
export function parse(patterns: string[], options: IGlobOptions): Pattern[] {
|
||||||
|
const result: Pattern[] = []
|
||||||
|
|
||||||
|
for (const patternString of patterns.map(x => x.trim())) {
|
||||||
|
// Skip empty or comment
|
||||||
|
if (!patternString || patternString.startsWith('#')) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push
|
||||||
|
const pattern = new Pattern(patternString)
|
||||||
|
result.push(pattern)
|
||||||
|
|
||||||
|
// Implicit descendants?
|
||||||
|
if (
|
||||||
|
options.implicitDescendants &&
|
||||||
|
(pattern.trailingSeparator ||
|
||||||
|
pattern.segments[pattern.segments.length - 1] !== '**')
|
||||||
|
) {
|
||||||
|
// Push
|
||||||
|
result.push(new Pattern(pattern.negate, pattern.segments.concat('**')))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether to descend further into the directory
|
||||||
|
*/
|
||||||
|
export function partialMatch(patterns: Pattern[], itemPath: string): boolean {
|
||||||
|
return patterns.some(x => !x.negate && x.partialMatch(itemPath))
|
||||||
|
}
|
|
@ -0,0 +1,321 @@
|
||||||
|
import * as assert from 'assert'
|
||||||
|
import * as os from 'os'
|
||||||
|
import * as path from 'path'
|
||||||
|
import * as pathHelper from './internal-path-helper'
|
||||||
|
import {Minimatch, IMinimatch, IOptions as IMinimatchOptions} from 'minimatch'
|
||||||
|
import {MatchKind} from './internal-match-kind'
|
||||||
|
import {Path} from './internal-path'
|
||||||
|
|
||||||
|
const IS_WINDOWS = process.platform === 'win32'
|
||||||
|
|
||||||
|
export class Pattern {
|
||||||
|
/**
|
||||||
|
* Indicates whether matches should be excluded from the result set
|
||||||
|
*/
|
||||||
|
readonly negate: boolean = false
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The directory to search. The literal path prior to the first glob segment.
|
||||||
|
*/
|
||||||
|
readonly searchPath: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path/pattern segments. Note, only the first segment (the root directory)
|
||||||
|
* may contain a directory separator charactor. Use the trailingSeparator field
|
||||||
|
* to determine whether the pattern ended with a trailing slash.
|
||||||
|
*/
|
||||||
|
readonly segments: string[]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates the pattern should only match directories, not regular files.
|
||||||
|
*/
|
||||||
|
readonly trailingSeparator: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Minimatch object used for matching
|
||||||
|
*/
|
||||||
|
private readonly minimatch: IMinimatch
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to workaround a limitation with Minimatch when determining a partial
|
||||||
|
* match and the path is a root directory. For example, when the pattern is
|
||||||
|
* `/foo/**` or `C:\foo\**` and the path is `/` or `C:\`.
|
||||||
|
*/
|
||||||
|
private readonly rootRegExp: RegExp
|
||||||
|
|
||||||
|
/* eslint-disable no-dupe-class-members */
|
||||||
|
// Disable no-dupe-class-members due to false positive for method overload
|
||||||
|
// https://github.com/typescript-eslint/typescript-eslint/issues/291
|
||||||
|
|
||||||
|
constructor(pattern: string)
|
||||||
|
constructor(negate: boolean, segments: string[])
|
||||||
|
constructor(patternOrNegate: string | boolean, segments?: string[]) {
|
||||||
|
// Pattern overload
|
||||||
|
let pattern: string
|
||||||
|
if (typeof patternOrNegate === 'string') {
|
||||||
|
pattern = patternOrNegate.trim()
|
||||||
|
}
|
||||||
|
// Segments overload
|
||||||
|
else {
|
||||||
|
// Convert to pattern
|
||||||
|
segments = segments || []
|
||||||
|
assert(segments.length, `Parameter 'segments' must not empty`)
|
||||||
|
const root = Pattern.getLiteral(segments[0])
|
||||||
|
assert(
|
||||||
|
root && pathHelper.hasAbsoluteRoot(root),
|
||||||
|
`Parameter 'segments' first element must be a root path`
|
||||||
|
)
|
||||||
|
pattern = new Path(segments).toString().trim()
|
||||||
|
if (patternOrNegate) {
|
||||||
|
pattern = `!${pattern}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Negate
|
||||||
|
while (pattern.startsWith('!')) {
|
||||||
|
this.negate = !this.negate
|
||||||
|
pattern = pattern.substr(1).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normalize slashes and ensures absolute root
|
||||||
|
pattern = Pattern.fixupPattern(pattern)
|
||||||
|
|
||||||
|
// Segments
|
||||||
|
this.segments = new Path(pattern).segments
|
||||||
|
|
||||||
|
// Trailing slash indicates the pattern should only match directories, not regular files
|
||||||
|
this.trailingSeparator = pathHelper
|
||||||
|
.normalizeSeparators(pattern)
|
||||||
|
.endsWith(path.sep)
|
||||||
|
pattern = pathHelper.safeTrimTrailingSeparator(pattern)
|
||||||
|
|
||||||
|
// Search path (literal path prior to the first glob segment)
|
||||||
|
let foundGlob = false
|
||||||
|
const searchSegments = this.segments
|
||||||
|
.map(x => Pattern.getLiteral(x))
|
||||||
|
.filter(x => !foundGlob && !(foundGlob = x === ''))
|
||||||
|
this.searchPath = new Path(searchSegments).toString()
|
||||||
|
|
||||||
|
// Root RegExp (required when determining partial match)
|
||||||
|
this.rootRegExp = new RegExp(
|
||||||
|
Pattern.regExpEscape(searchSegments[0]),
|
||||||
|
IS_WINDOWS ? 'i' : ''
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create minimatch
|
||||||
|
const minimatchOptions: IMinimatchOptions = {
|
||||||
|
dot: true,
|
||||||
|
nobrace: true,
|
||||||
|
nocase: IS_WINDOWS,
|
||||||
|
nocomment: true,
|
||||||
|
noext: true,
|
||||||
|
nonegate: true
|
||||||
|
}
|
||||||
|
pattern = IS_WINDOWS ? pattern.replace(/\\/g, '/') : pattern
|
||||||
|
this.minimatch = new Minimatch(pattern, minimatchOptions)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Matches the pattern against the specified path
|
||||||
|
*/
|
||||||
|
match(itemPath: string): MatchKind {
|
||||||
|
// Last segment is globstar?
|
||||||
|
if (this.segments[this.segments.length - 1] === '**') {
|
||||||
|
// Normalize slashes
|
||||||
|
itemPath = pathHelper.normalizeSeparators(itemPath)
|
||||||
|
|
||||||
|
// Append a trailing slash. Otherwise Minimatch will not match the directory immediately
|
||||||
|
// preceeding the globstar. For example, given the pattern `/foo/**`, Minimatch returns
|
||||||
|
// false for `/foo` but returns true for `/foo/`. Append a trailing slash to handle that quirk.
|
||||||
|
if (!itemPath.endsWith(path.sep)) {
|
||||||
|
// Note, this is safe because the constructor ensures the pattern has an absolute root.
|
||||||
|
// For example, formats like C: and C:foo on Windows are resolved to an aboslute root.
|
||||||
|
itemPath = `${itemPath}${path.sep}`
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normalize slashes and trim unnecessary trailing slash
|
||||||
|
itemPath = pathHelper.safeTrimTrailingSeparator(itemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match
|
||||||
|
if (this.minimatch.match(itemPath)) {
|
||||||
|
return this.trailingSeparator ? MatchKind.Directory : MatchKind.All
|
||||||
|
}
|
||||||
|
|
||||||
|
return MatchKind.None
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the pattern may match descendants of the specified path
|
||||||
|
*/
|
||||||
|
partialMatch(itemPath: string): boolean {
|
||||||
|
// Normalize slashes and trim unnecessary trailing slash
|
||||||
|
itemPath = pathHelper.safeTrimTrailingSeparator(itemPath)
|
||||||
|
|
||||||
|
// matchOne does not handle root path correctly
|
||||||
|
if (pathHelper.dirname(itemPath) === itemPath) {
|
||||||
|
return this.rootRegExp.test(itemPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.minimatch.matchOne(
|
||||||
|
itemPath.split(IS_WINDOWS ? /\\+/ : /\/+/),
|
||||||
|
this.minimatch.set[0],
|
||||||
|
true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes glob patterns within a path
|
||||||
|
*/
|
||||||
|
static globEscape(s: string): string {
|
||||||
|
return (IS_WINDOWS ? s : s.replace(/\\/g, '\\\\')) // escape '\' on Linux/macOS
|
||||||
|
.replace(/(\[)(?=[^/]+\])/g, '[[]') // escape '[' when ']' follows within the path segment
|
||||||
|
.replace(/\?/g, '[?]') // escape '?'
|
||||||
|
.replace(/\*/g, '[*]') // escape '*'
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes slashes and ensures absolute root
|
||||||
|
*/
|
||||||
|
private static fixupPattern(pattern: string): string {
|
||||||
|
// Empty
|
||||||
|
assert(pattern, 'pattern cannot be empty')
|
||||||
|
|
||||||
|
// Must not contain `.` segment, unless first segment
|
||||||
|
// Must not contain `..` segment
|
||||||
|
const literalSegments = new Path(pattern).segments.map(x =>
|
||||||
|
Pattern.getLiteral(x)
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
literalSegments.every((x, i) => (x !== '.' || i === 0) && x !== '..'),
|
||||||
|
`Invalid pattern '${pattern}'. Relative pathing '.' and '..' is not allowed.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Must not contain globs in root, e.g. Windows UNC path \\foo\b*r
|
||||||
|
assert(
|
||||||
|
!pathHelper.hasRoot(pattern) || literalSegments[0],
|
||||||
|
`Invalid pattern '${pattern}'. Root segment must not contain globs.`
|
||||||
|
)
|
||||||
|
|
||||||
|
// Normalize slashes
|
||||||
|
pattern = pathHelper.normalizeSeparators(pattern)
|
||||||
|
|
||||||
|
// Replace leading `.` segment
|
||||||
|
if (pattern === '.' || pattern.startsWith(`.${path.sep}`)) {
|
||||||
|
pattern = Pattern.globEscape(process.cwd()) + pattern.substr(1)
|
||||||
|
}
|
||||||
|
// Replace leading `~` segment
|
||||||
|
else if (pattern === '~' || pattern.startsWith(`~${path.sep}`)) {
|
||||||
|
const homedir = os.homedir()
|
||||||
|
assert(homedir, 'Unable to determine HOME directory')
|
||||||
|
assert(
|
||||||
|
pathHelper.hasAbsoluteRoot(homedir),
|
||||||
|
`Expected HOME directory to be a rooted path. Actual '${homedir}'`
|
||||||
|
)
|
||||||
|
pattern = Pattern.globEscape(homedir) + pattern.substr(1)
|
||||||
|
}
|
||||||
|
// Replace relative drive root, e.g. pattern is C: or C:foo
|
||||||
|
else if (
|
||||||
|
IS_WINDOWS &&
|
||||||
|
(pattern.match(/^[A-Z]:$/i) || pattern.match(/^[A-Z]:[^\\]/i))
|
||||||
|
) {
|
||||||
|
let root = pathHelper.ensureAbsoluteRoot(
|
||||||
|
'C:\\dummy-root',
|
||||||
|
pattern.substr(0, 2)
|
||||||
|
)
|
||||||
|
if (pattern.length > 2 && !root.endsWith('\\')) {
|
||||||
|
root += '\\'
|
||||||
|
}
|
||||||
|
pattern = Pattern.globEscape(root) + pattern.substr(2)
|
||||||
|
}
|
||||||
|
// Replace relative root, e.g. pattern is \ or \foo
|
||||||
|
else if (IS_WINDOWS && (pattern === '\\' || pattern.match(/^\\[^\\]/))) {
|
||||||
|
let root = pathHelper.ensureAbsoluteRoot('C:\\dummy-root', '\\')
|
||||||
|
if (!root.endsWith('\\')) {
|
||||||
|
root += '\\'
|
||||||
|
}
|
||||||
|
pattern = Pattern.globEscape(root) + pattern.substr(1)
|
||||||
|
}
|
||||||
|
// Otherwise ensure absolute root
|
||||||
|
else {
|
||||||
|
pattern = pathHelper.ensureAbsoluteRoot(
|
||||||
|
Pattern.globEscape(process.cwd()),
|
||||||
|
pattern
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathHelper.normalizeSeparators(pattern)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to unescape a pattern segment to create a literal path segment.
|
||||||
|
* Otherwise returns empty string.
|
||||||
|
*/
|
||||||
|
private static getLiteral(segment: string): string {
|
||||||
|
let literal = ''
|
||||||
|
for (let i = 0; i < segment.length; i++) {
|
||||||
|
const c = segment[i]
|
||||||
|
// Escape
|
||||||
|
if (c === '\\' && !IS_WINDOWS && i + 1 < segment.length) {
|
||||||
|
literal += segment[++i]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Wildcard
|
||||||
|
else if (c === '*' || c === '?') {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
// Character set
|
||||||
|
else if (c === '[' && i + 1 < segment.length) {
|
||||||
|
let set = ''
|
||||||
|
let closed = -1
|
||||||
|
for (let i2 = i + 1; i2 < segment.length; i2++) {
|
||||||
|
const c2 = segment[i2]
|
||||||
|
// Escape
|
||||||
|
if (c2 === '\\' && !IS_WINDOWS && i2 + 1 < segment.length) {
|
||||||
|
set += segment[++i2]
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Closed
|
||||||
|
else if (c2 === ']') {
|
||||||
|
closed = i2
|
||||||
|
break
|
||||||
|
}
|
||||||
|
// Otherwise
|
||||||
|
else {
|
||||||
|
set += c2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Closed?
|
||||||
|
if (closed >= 0) {
|
||||||
|
// Cannot convert
|
||||||
|
if (set.length > 1) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to literal
|
||||||
|
if (set) {
|
||||||
|
literal += set
|
||||||
|
i = closed
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise fall thru
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append
|
||||||
|
literal += c
|
||||||
|
}
|
||||||
|
|
||||||
|
return literal
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Escapes regexp special characters
|
||||||
|
* https://javascript.info/regexp-escaping
|
||||||
|
*/
|
||||||
|
private static regExpEscape(s: string): string {
|
||||||
|
return s.replace(/[[\\^$.|?*+()]/g, '\\$&')
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
export class SearchState {
|
||||||
|
readonly path: string
|
||||||
|
readonly level: number
|
||||||
|
|
||||||
|
constructor(path: string, level: number) {
|
||||||
|
this.path = path
|
||||||
|
this.level = level
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": "./",
|
||||||
|
"outDir": "./lib",
|
||||||
|
"rootDir": "./src"
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"./src"
|
||||||
|
]
|
||||||
|
}
|
Loading…
Reference in New Issue