Repository navigation
Add Glob module and portable FileSystem.glob #9438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit
Hold shift + click to select a range
f5f8f8c
Add Glob module and portable FileSystem.glob
jpowersdev f0e843d
Reduce glob tests to the accepted behavior
effect-bot 48e8646
Add glob regressions for backtracking, dot segments after **, braces …
effect-bot 8cf7d6d
Match glob wildcards without backtracking, keep braces in classes lit…
jpowersdev 63ed76e
Match nothing for UNC glob patterns and paths
jpowersdev 7702c5e
Compile glob segments to tokens and share class parsing with brace ex…
effect-bot f1b5ece
test: cover UNC path rejection and directory dot glob
effect-bot 5a107cc
Add glob regressions for probe errors and defects
effect-bot c0d28ef
Preserve unexpected glob probe failures and clarify pattern docs
effect-bot da4aef1
Add tests for FileSystem.readDirectoryEntries and the glob literal-pr…
effect-bot f2b6b8e
Add FileSystem.readDirectoryEntries and fail glob on unreadable named…
effect-bot 9935162
Cover glob literal probes and dangling symlinks
effect-bot 1968da3
Clarify glob documentation and release note
tim-smart d28883b
Apply suggestion from @tim-smart
tim-smart b8c934c
Simplify glob matcher and walker
effect-bot b62157b
Cover directory-only glob excludes
effect-bot 73c6ffd
Keep FileSystem unchanged and build glob with FileSystem.makeGlob
effect-bot 01cc839
Keep readDirectoryEntries on FileSystem and derive glob in make
effect-bot 2ff3e24
docs: clarify portable glob migration
effect-bot f60b6b7
chore: release portable glob as minor
effect-bot 8561e09
Simplify glob traversal with Effect.fnUntraced
effect-bot File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| --- | ||
| "effect": minor | ||
| "@effect/platform-node-shared": minor | ||
| "@effect/platform-deno": minor | ||
| --- | ||
|
|
||
| Add experimental `Glob` path matching and a shared `FileSystem.glob` implementation for Node, Bun, and Deno. | ||
|
|
||
| - Breaking: custom `FileSystem` implementations, including inputs to `FileSystem.make`, must provide `readDirectoryEntries`; mapping `readDirectory` names to `{ name, type: "Unknown" }` is supported. Wrappers must apply directory-listing changes to both methods. | ||
| - `FileSystem.make` now derives `glob` instead of accepting it. Apply custom overrides afterward with `FileSystem.of({ ...fs, glob: customGlob })`. | ||
| - Glob patterns use `/` separators and backslash escapes; Node extglobs no longer expand. Results use `/` separators and are relative to `root` unless the pattern is absolute. Deno wildcards now skip dotfiles unless explicitly named or `dot: true`; unreadable wildcard-reached directories are skipped, while read failures at the root or a leading literal directory propagate. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| /** | ||
| * Matches `/`-separated paths without accessing the file system, using the | ||
| * same pattern syntax as `FileSystem.glob`. | ||
| * | ||
| * @stability experimental | ||
| * @since 4.0.0 | ||
| */ | ||
| import { format } from "./Formatter.ts" | ||
| import { dual } from "./Function.ts" | ||
| import { type Inspectable, NodeInspectSymbol } from "./Inspectable.ts" | ||
| import * as internal from "./internal/glob.ts" | ||
| import { type Pipeable, pipeArguments } from "./Pipeable.ts" | ||
|
|
||
| const TypeId = "~effect/Glob" | ||
|
|
||
| /** | ||
| * A compiled glob pattern. | ||
| * | ||
| * **Details** | ||
| * | ||
| * `pattern` is the source text and `dot` records whether wildcards match names | ||
| * that start with `.`. Match paths against it with {@link match}. | ||
| * | ||
| * @see {@link make} for compiling a pattern | ||
| * | ||
| * @stability experimental | ||
| * @category models | ||
| * @since 4.0.0 | ||
| */ | ||
| export interface Glob extends Pipeable, Inspectable { | ||
| readonly [TypeId]: typeof TypeId | ||
| readonly pattern: string | ||
| readonly dot: boolean | ||
| } | ||
|
|
||
| interface GlobImpl extends Glob { | ||
| readonly compiled: internal.Compiled | ||
| } | ||
|
|
||
| const GlobProto = { | ||
| [TypeId]: TypeId, | ||
| toJSON(this: Glob) { | ||
| return { _id: "Glob", pattern: this.pattern, dot: this.dot } | ||
| }, | ||
| toString(this: Glob) { | ||
| return `Glob(${format(this.toJSON())})` | ||
| }, | ||
| [NodeInspectSymbol](this: Glob) { | ||
| return this.toJSON() | ||
| }, | ||
| pipe() { | ||
| return pipeArguments(this, arguments) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Compiles a glob pattern into a reusable `Glob`. | ||
| * | ||
| * **Details** | ||
| * | ||
| * Patterns use `/` as the separator. `*` matches any run of characters within | ||
| * one path segment, `?` matches one character, and `[...]` matches one | ||
| * character from a set, with `!` or `^` negating it and POSIX classes such as | ||
| * `[[:digit:]]` allowed. `{a,b}` expands to each alternative and may nest. A | ||
| * segment that is exactly `**` matches zero or more whole segments. A trailing | ||
| * `/` matches only directories, and `\` makes the next character literal. | ||
| * | ||
| * **Gotchas** | ||
| * | ||
| * Wildcards and `**` skip names that start with `.` unless the pattern segment | ||
| * itself starts with `.` or `dot` is `true`; they never match `.` or `..`. | ||
| * `x/.` is the same as `x/`, and a `.` or `..` segment after `**` makes the | ||
| * pattern match nothing, as does the empty pattern. UNC patterns and paths | ||
| * such as `//server/share` are not supported and never match. Braces and | ||
| * commas inside `[...]` are class members. Extended glob syntax and leading | ||
| * `!` negation are not interpreted, but ordinary wildcards still apply within | ||
| * them (for example, `?(a)` matches `x(a)`). Malformed syntax, such as an | ||
| * unclosed `[`, matches literally. Braces also stay literal if expansion would | ||
| * exceed 10,000 alternatives. | ||
| * | ||
| * **Example** (Matching source files) | ||
| * | ||
| * ```ts import.meta.vitest | ||
| * import { Glob } from "effect" | ||
| * | ||
| * const sources = Glob.make("src/**\/*.{ts,tsx}") | ||
| * | ||
| * Glob.match(sources, "src/index.ts") // => true | ||
| * Glob.match(sources, "src/ui/Button.tsx") // => true | ||
| * Glob.match(sources, "src/.cache/index.ts") // => false | ||
| * Glob.match(sources, "test/index.ts") // => false | ||
| * ``` | ||
| * | ||
| * @stability experimental | ||
| * @category constructors | ||
| * @since 4.0.0 | ||
| */ | ||
| export const make = (pattern: string, options?: { | ||
| /** Whether wildcards and `**` match names that start with `.`. */ | ||
| readonly dot?: boolean | undefined | ||
| }): Glob => { | ||
| const dot = options?.dot ?? false | ||
| const self = Object.create(GlobProto) | ||
| self.pattern = pattern | ||
| self.dot = dot | ||
| self.compiled = internal.compile([pattern], dot) | ||
| return self | ||
| } | ||
|
|
||
| /** | ||
| * Tests whether a `/`-separated path matches a `Glob`. | ||
| * | ||
| * **Details** | ||
| * | ||
| * Empty segments and `./` segments in the path are ignored. A trailing `/` | ||
| * marks the path as a directory, which a pattern ending in `/` requires. | ||
| * | ||
| * **Example** (Filtering paths) | ||
| * | ||
| * ```ts import.meta.vitest | ||
| * import { Glob, pipe } from "effect" | ||
| * | ||
| * const tests = Glob.make("**\/*.test.ts") | ||
| * const paths = ["src/a.ts", "src/a.test.ts", "test/b.test.ts"] | ||
| * | ||
| * paths.filter((path) => Glob.match(tests, path)) // => ["src/a.test.ts", "test/b.test.ts"] | ||
| * pipe(tests, Glob.match("./src/a.test.ts")) // => true | ||
| * ``` | ||
| * | ||
| * @stability experimental | ||
| * @category predicates | ||
| * @since 4.0.0 | ||
| */ | ||
| export const match: { | ||
| (path: string): (self: Glob) => boolean | ||
| (self: Glob, path: string): boolean | ||
| } = dual(2, (self: Glob, path: string): boolean => internal.match((self as GlobImpl).compiled, path)) | ||
|
|
||
| /** | ||
| * Escapes glob syntax so a string matches only itself. | ||
| * | ||
| * **Example** (Matching a literal file name) | ||
| * | ||
| * ```ts import.meta.vitest | ||
| * import { Glob } from "effect" | ||
| * | ||
| * const name = "report[1].txt" | ||
| * | ||
| * Glob.match(Glob.make(name), name) // => false | ||
| * Glob.match(Glob.make(Glob.escape(name)), name) // => true | ||
| * ``` | ||
| * | ||
| * @stability experimental | ||
| * @category utils | ||
| * @since 4.0.0 | ||
| */ | ||
| export const escape = (input: string): string => input.replace(/[\\*?[\]{}]/g, "\\$&") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.