Skip to content

Commit

Permalink
feat: introduce .convertPathToPattern method
Browse files Browse the repository at this point in the history
  • Loading branch information
mrmlnc committed May 9, 2023
1 parent 00cf05b commit 6b0624a
Show file tree
Hide file tree
Showing 5 changed files with 211 additions and 27 deletions.
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ This package provides methods for traversing the file system and returning pathn
* [generateTasks](#generatetaskspatterns-options)
* [isDynamicPattern](#isdynamicpatternpattern-options)
* [escapePath](#escapepathpath)
* [convertPathToPattern](#convertpathtopatternpath)
* [Options](#options-3)
* [Common](#common)
* [concurrency](#concurrency)
Expand Down Expand Up @@ -291,6 +292,31 @@ fg.win32.escapePath('C:\\Program Files (x86)\\**\\*');
// Windows: C:\\Program Files \\(x86\\)\\**\\*
```

#### `convertPathToPattern(path)`

Converts a path to a pattern depending on the platform, including special character escaping.

* Posix. Works similarly to the `fg.posix.escapePath` method.
* Windows. Works similarly to the `fg.win32.escapePath` method, additionally converting backslashes to forward slashes in cases where they are not escape characters (`!()+@{}`).

```js
fg.convertPathToPattern('[OpenSource] mrmlnc – fast-glob (Deluxe Edition) 2014') + '/*.flac';
// \\[OpenSource\\] mrmlnc – fast-glob \\(Deluxe Edition\\) 2014/*.flac

fg.convertPathToPattern('C:/Program Files (x86)/**/*');
// Posix: C:/Program Files \\(x86\\)/\\*\\*/\\*
// Windows: C:/Program Files \\(x86\\)/**/*

fg.convertPathToPattern('C:\\Program Files (x86)\\**\\*');
// Posix: C:\\\\Program Files \\(x86\\)\\*\\*\\*
// Windows: C:/Program Files \\(x86\\)/**/*

fg.posix.convertPathToPattern('\\\\?\\c:\\Program Files (x86)') + '/**/*';
// Posix: \\\\\\?\\\\c:\\\\Program Files \\(x86\\)/**/* (broken pattern)
fg.win32.convertPathToPattern('\\\\?\\c:\\Program Files (x86)') + '/**/*';
// Windows: //?/c:/Program Files \\(x86\\)/**/*
```

## Options

### Common options
Expand Down Expand Up @@ -680,11 +706,11 @@ Always use forward-slashes in glob expressions (patterns and [`ignore`](#ignore)
```ts
[
'directory/*',
path.join(process.cwd(), '**').replace(/\\/g, '/')
fg.convertPathToPattern(process.cwd()) + '/**'
]
```

> :book: Use the [`normalize-path`][npm_normalize_path] or the [`unixify`][npm_unixify] package to convert Windows-style path to a Unix-style path.
> :book: Use the [`.convertPathToPattern`](#convertpathtopatternpath) package to convert Windows-style path to a Unix-style path.
Read more about [matching with backslashes][micromatch_backslashes].

Expand All @@ -705,7 +731,7 @@ Refers to Bash. You need to escape special characters:
fg.sync(['\\(special-*file\\).txt']) // ['(special-*file).txt']
```

Read more about [matching special characters as literals][picomatch_matching_special_characters_as_literals].
Read more about [matching special characters as literals][picomatch_matching_special_characters_as_literals]. Or use the [`.escapePath`](#escapepathpath).

## How to exclude directory from reading?

Expand All @@ -731,11 +757,15 @@ You have to understand that if you write the pattern to exclude directories, the

## How to use UNC path?

You cannot use [Uniform Naming Convention (UNC)][unc_path] paths as patterns (due to syntax), but you can use them as [`cwd`](#cwd) directory.
You cannot use [Uniform Naming Convention (UNC)][unc_path] paths as patterns (due to syntax) directly, but you can use them as [`cwd`](#cwd) directory or use the `fg.convertPathToPattern` method.

```ts
// cwd
fg.sync('*', { cwd: '\\\\?\\C:\\Python27' /* or //?/C:/Python27 */ });
fg.sync('Python27/*', { cwd: '\\\\?\\C:\\' /* or //?/C:/ */ });

// .convertPathToPattern
fg.sync(fg.convertPathToPattern('\\\\?\\c:\\Python27') + '/*');
```

## Compatible with `node-glob`?
Expand Down
37 changes: 35 additions & 2 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,20 @@ describe('Package', () => {
});
});

describe('.posix', () => {
describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
// In posix system \\ is a escaping character and it will be escaped before non-special characters.
const posix = 'C:\\\\Program Files \\(x86\\)\\*\\*\\*';
const windows = 'C:/Program Files \\(x86\\)/**/*';
const expected = tests.platform.isWindows() ? windows : posix;

const actual = fg.convertPathToPattern('C:\\Program Files (x86)\\**\\*');

assert.strictEqual(actual, expected);
});
});

describe('posix', () => {
describe('.escapePath', () => {
it('should return escaped path', () => {
const expected = '/directory/\\*\\*/\\*';
Expand All @@ -231,9 +244,19 @@ describe('Package', () => {
assert.strictEqual(actual, expected);
});
});

describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
const expected = 'a\\*.txt';

const actual = fg.posix.convertPathToPattern('a\\*.txt');

assert.strictEqual(actual, expected);
});
});
});

describe('.win32', () => {
describe('win32', () => {
describe('.escapePath', () => {
it('should return escaped path', () => {
const expected = 'C:\\Program Files \\(x86\\)\\**\\*';
Expand All @@ -243,5 +266,15 @@ describe('Package', () => {
assert.strictEqual(actual, expected);
});
});

describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
const expected = 'C:/Program Files \\(x86\\)/**/*';

const actual = fg.win32.convertPathToPattern('C:\\Program Files (x86)\\**\\*');

assert.strictEqual(actual, expected);
});
});
});
});
18 changes: 18 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,24 @@ namespace FastGlob {
return utils.path.escape(source);
}

export function convertPathToPattern(source: string): PatternInternal {
assertPatternsInput(source);

return utils.path.convertPathToPattern(source);
}

export namespace posix {
export function escapePath(source: string): PatternInternal {
assertPatternsInput(source);

return utils.path.escapePosixPath(source);
}

export function convertPathToPattern(source: string): PatternInternal {
assertPatternsInput(source);

return utils.path.convertPosixPathToPattern(source);
}
}

export namespace win32 {
Expand All @@ -93,6 +105,12 @@ namespace FastGlob {

return utils.path.escapeWindowsPath(source);
}

export function convertPathToPattern(source: string): PatternInternal {
assertPatternsInput(source);

return utils.path.convertWindowsPathToPattern(source);
}
}
}

Expand Down
120 changes: 100 additions & 20 deletions src/utils/path.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,26 +24,7 @@ describe('Utils → Path', () => {
});
});

describe('.removeLeadingDotCharacters', () => {
it('should return path without changes', () => {
assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b');
assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b');
assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b');
assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b');

assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b');
});

it('should return path without leading dit characters', () => {
assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b');
assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b');
});
});

describe('.escapePattern', () => {
describe('.escape', () => {
it('should return pattern without additional escape characters', () => {
assert.strictEqual(util.escape('\\!abc'), '\\!abc');
assert.strictEqual(util.escape('\\*'), '\\*');
Expand Down Expand Up @@ -87,4 +68,103 @@ describe('Utils → Path', () => {
assert.strictEqual(util.escapeWindowsPath('+('), '\\+\\(');
});
});

describe('.removeLeadingDotCharacters', () => {
it('should return path without changes', () => {
assert.strictEqual(util.removeLeadingDotSegment('../a/b'), '../a/b');
assert.strictEqual(util.removeLeadingDotSegment('~/a/b'), '~/a/b');
assert.strictEqual(util.removeLeadingDotSegment('/a/b'), '/a/b');
assert.strictEqual(util.removeLeadingDotSegment('a/b'), 'a/b');

assert.strictEqual(util.removeLeadingDotSegment('..\\a\\b'), '..\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('~\\a\\b'), '~\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('\\a\\b'), '\\a\\b');
assert.strictEqual(util.removeLeadingDotSegment('a\\b'), 'a\\b');
});

it('should return path without leading dit characters', () => {
assert.strictEqual(util.removeLeadingDotSegment('./a/b'), 'a/b');
assert.strictEqual(util.removeLeadingDotSegment('.\\a\\b'), 'a\\b');
});
});

describe('.convertPathToPattern', () => {
it('should return a pattern', () => {
assert.strictEqual(util.convertPathToPattern('.{directory}'), '.\\{directory\\}');
});
});

describe('.convertPosixPathToPattern', () => {
it('should escape special characters', () => {
assert.strictEqual(util.convertPosixPathToPattern('./**\\*'), './\\*\\*\\*');
});
});

describe('.convertWindowsPathToPattern', () => {
it('should escape special characters', () => {
assert.strictEqual(util.convertPosixPathToPattern('.{directory}'), '.\\{directory\\}');
});

it('should do nothing with escaped glob symbols', () => {
assert.strictEqual(util.convertWindowsPathToPattern('\\!\\'), '\\!/');
assert.strictEqual(util.convertWindowsPathToPattern('\\+\\'), '\\+/');
assert.strictEqual(util.convertWindowsPathToPattern('\\@\\'), '\\@/');
assert.strictEqual(util.convertWindowsPathToPattern('\\(\\'), '\\(/');
assert.strictEqual(util.convertWindowsPathToPattern('\\)\\'), '\\)/');
assert.strictEqual(util.convertWindowsPathToPattern('\\{\\'), '\\{/');
assert.strictEqual(util.convertWindowsPathToPattern('\\}\\'), '\\}/');

assert.strictEqual(util.convertWindowsPathToPattern('.\\*'), './*');
assert.strictEqual(util.convertWindowsPathToPattern('.\\**'), './**');
assert.strictEqual(util.convertWindowsPathToPattern('.\\**\\*'), './**/*');

assert.strictEqual(util.convertWindowsPathToPattern('a\\{b,c\\d,{b,c}}'), 'a\\{b,c/d,\\{b,c\\}\\}');
});

it('should convert slashes', () => {
assert.strictEqual(util.convertWindowsPathToPattern('/'), '/');
assert.strictEqual(util.convertWindowsPathToPattern('\\'), '/');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\'), '//');
assert.strictEqual(util.convertWindowsPathToPattern('\\/'), '//');
assert.strictEqual(util.convertWindowsPathToPattern('\\/\\'), '///');
});

it('should convert relative paths', () => {
assert.strictEqual(util.convertWindowsPathToPattern('file.txt'), 'file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('./file.txt'), './file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('.\\file.txt'), './file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('../file.txt'), '../file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('..\\file.txt'), '../file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('.\\file.txt'), './file.txt');
});

it('should convert absolute paths', () => {
assert.strictEqual(util.convertWindowsPathToPattern('/.file.txt'), '/.file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('/root/.file.txt'), '/root/.file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\.file.txt'), '/.file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\root\\.file.txt'), '/root/.file.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\root/.file.txt'), '/root/.file.txt');
});

it('should convert traditional DOS paths', () => {
assert.strictEqual(util.convertWindowsPathToPattern('D:ShipId.txt'), 'D:ShipId.txt');
assert.strictEqual(util.convertWindowsPathToPattern('D:/ShipId.txt'), 'D:/ShipId.txt');
assert.strictEqual(util.convertWindowsPathToPattern('D://ShipId.txt'), 'D://ShipId.txt');

assert.strictEqual(util.convertWindowsPathToPattern('D:\\ShipId.txt'), 'D:/ShipId.txt');
assert.strictEqual(util.convertWindowsPathToPattern('D:\\\\ShipId.txt'), 'D://ShipId.txt');
assert.strictEqual(util.convertWindowsPathToPattern('D:\\/ShipId.txt'), 'D://ShipId.txt');
});

it('should convert UNC paths', () => {
assert.strictEqual(util.convertWindowsPathToPattern('\\\\system07\\'), '//system07/');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\system07\\c$\\'), '//system07/c$/');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\Server02\\Share\\Foo.txt'), '//Server02/Share/Foo.txt');

assert.strictEqual(util.convertWindowsPathToPattern('\\\\127.0.0.1\\c$\\File.txt'), '//127.0.0.1/c$/File.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\.\\c:\\File.txt'), '//./c:/File.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\?\\c:\\File.txt'), '//?/c:/File.txt');
assert.strictEqual(util.convertWindowsPathToPattern('\\\\.\\UNC\\LOCALHOST\\c$\\File.txt'), '//./UNC/LOCALHOST/c$/File.txt');
});
});
});
25 changes: 24 additions & 1 deletion src/utils/path.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,24 @@ import { Pattern } from '../types';

const IS_WINDOWS_PLATFORM = os.platform() === 'win32';
const LEADING_DOT_SEGMENT_CHARACTERS_COUNT = 2; // ./ or .\\
/*
/**
* All non-escaped special characters.
* Posix: ()*?[\]{|}, !+@ before (, ! at the beginning, \\ before non-special characters.
* Windows: (){}, !+@ before (, ! at the beginning.
*/
const POSIX_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([()*?[\]{|}]|^!|[!+@](?=\()|\\(?![!()*+?@[\]{|}]))/g;
const WINDOWS_UNESCAPED_GLOB_SYMBOLS_RE = /(\\?)([(){}]|^!|[!+@](?=\())/g;
/**
* The device path (\\.\ or \\?\).
* https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths
*/
const DOS_DEVICE_PATH_RE = /^\\\\([.?])/;
/**
* All backslashes except those escaping special characters.
* Windows: !()+@{}
* https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions
*/
const WINDOWS_BACKSLASHES_RE = /\\(?![!()+@{}])/g;

/**
* Designed to work only with simple paths: `dir\\file`.
Expand Down Expand Up @@ -47,3 +58,15 @@ export function escapeWindowsPath(pattern: Pattern): Pattern {
export function escapePosixPath(pattern: Pattern): Pattern {
return pattern.replace(POSIX_UNESCAPED_GLOB_SYMBOLS_RE, '\\$2');
}

export const convertPathToPattern = IS_WINDOWS_PLATFORM ? convertWindowsPathToPattern : convertPosixPathToPattern;

export function convertWindowsPathToPattern(filepath: string): Pattern {
return escapeWindowsPath(filepath)
.replace(DOS_DEVICE_PATH_RE, '//$1')
.replace(WINDOWS_BACKSLASHES_RE, '/');
}

export function convertPosixPathToPattern(filepath: string): Pattern {
return escapePosixPath(filepath);
}

0 comments on commit 6b0624a

Please sign in to comment.