From d73877a5bb924717ab86fdf74619f1c93e13a73a Mon Sep 17 00:00:00 2001 From: mrmlnc Date: Mon, 8 May 2023 18:27:39 +0300 Subject: [PATCH] feat: introduce .convertPathToPattern method --- README.md | 38 +++++++++++-- src/index.spec.ts | 37 ++++++++++++- src/index.ts | 18 +++++++ src/utils/path.spec.ts | 120 ++++++++++++++++++++++++++++++++++------- src/utils/path.ts | 25 ++++++++- 5 files changed, 211 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index f174db43..f80fbf20 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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]. @@ -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? @@ -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`? diff --git a/src/index.spec.ts b/src/index.spec.ts index 9e73a43a..856948de 100644 --- a/src/index.spec.ts +++ b/src/index.spec.ts @@ -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/\\*\\*/\\*'; @@ -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\\)\\**\\*'; @@ -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); + }); + }); }); }); diff --git a/src/index.ts b/src/index.ts index 2aaaf44a..3f62bb49 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 { @@ -93,6 +105,12 @@ namespace FastGlob { return utils.path.escapeWindowsPath(source); } + + export function convertPathToPattern(source: string): PatternInternal { + assertPatternsInput(source); + + return utils.path.convertWindowsPathToPattern(source); + } } } diff --git a/src/utils/path.spec.ts b/src/utils/path.spec.ts index 83496944..770ad317 100644 --- a/src/utils/path.spec.ts +++ b/src/utils/path.spec.ts @@ -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('\\*'), '\\*'); @@ -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'); + }); + }); }); diff --git a/src/utils/path.ts b/src/utils/path.ts index 465db707..11dda077 100644 --- a/src/utils/path.ts +++ b/src/utils/path.ts @@ -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`. @@ -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); +}