Repository: denoland/std (TypeScript)

Issue:
expandGlob/expandGlobSync does not handle escaped paths well
**Describe the bug**

`expandGlob`/`expandGlobSync` do not match non-glob path segments with escaped glob characters.

**Steps to Reproduce**

1. File is `[file].txt`
2. Try to use `globSync`/`glob` to find `\[file\].txt`

**Expected behavior**

Should match, but it doesn't.

**Environment**

- std version: 1.1.1



Discussion:

Patch:
diff --git a/fs/expand_glob.ts b/fs/expand_glob.ts
index e0d9378200cc..d13757f0f498 100644
--- a/fs/expand_glob.ts
+++ b/fs/expand_glob.ts
@@ -303,7 +303,7 @@ export async function* expandGlob(
   let fixedRoot = isGlobAbsolute ? winRoot ?? "/" : absRoot;
   while (segments.length > 0 && !isGlob(segments[0]!)) {
     const seg = segments.shift()!;
-    fixedRoot = joinGlobs([fixedRoot, seg], globOptions);
+    fixedRoot = joinGlobs([fixedRoot, unescapeGlobSegment(seg)], globOptions);
   }
 
   let fixedRootInfo: WalkEntry;
@@ -460,7 +460,7 @@ export function* expandGlobSync(
   let fixedRoot = isGlobAbsolute ? winRoot ?? "/" : absRoot;
   while (segments.length > 0 && !isGlob(segments[0]!)) {
     const seg = segments.shift()!;
-    fixedRoot = joinGlobs([fixedRoot, seg], globOptions);
+    fixedRoot = joinGlobs([fixedRoot, unescapeGlobSegment(seg)], globOptions);
   }
 
   let fixedRootInfo: WalkEntry;
@@ -532,3 +532,29 @@ export function* expandGlobSync(
   }
   yield* currentMatches;
 }
+
+const globEscapeChar = Deno.build.os === "windows" ? "`" : `\\`;
+const globMetachars = "*?{}[]()|+@!";
+function unescapeGlobSegment(segment: string): string {
+  let result = "";
+  let lastIndex = 0;
+  for (let i = 0; i < segment.length; i++) {
+    const char = segment[i];
+    if (char === globEscapeChar) {
+      const nextChar = segment[i + 1];
+      if (nextChar && globMetachars.includes(nextChar)) {
+        // append the slice before the escape char, then the metachar
+        result += segment.slice(lastIndex, i) + nextChar;
+        i++; // skip next char since we already processed it
+        lastIndex = i + 1;
+      }
+    }
+  }
+  // no escaped, return the original segment
+  if (lastIndex === 0) {
+    return segment;
+  }
+  // append any remaining characters
+  result += segment.slice(lastIndex);
+  return result;
+}


Test patch:
diff --git a/fs/expand_glob_test.ts b/fs/expand_glob_test.ts
index 1abdb755df52..7c4f1b5ede1c 100644
--- a/fs/expand_glob_test.ts
+++ b/fs/expand_glob_test.ts
@@ -448,3 +448,27 @@ Deno.test(
     );
   },
 );
+
+const escapeChar = Deno.build.os === "windows" ? "`" : "\\";
+
+Deno.test("expandGlob() finds directory with escaped brackets", async function () {
+  assertEquals(
+    await expandGlobArray(`a${escapeChar}[b${escapeChar}]c`, EG_OPTIONS),
+    ["a[b]c"],
+  );
+  assertEquals(
+    await expandGlobArray(`a${escapeChar}[b${escapeChar}]c/fo[o]`, EG_OPTIONS),
+    [join("a[b]c", "foo")],
+  );
+});
+
+Deno.test("expandGlobSync() finds directory with escaped brackets", function () {
+  assertEquals(
+    expandGlobSyncArray(`a${escapeChar}[b${escapeChar}]c`, EG_OPTIONS),
+    ["a[b]c"],
+  );
+  assertEquals(
+    expandGlobSyncArray(`a${escapeChar}[b${escapeChar}]c/fo[o]`, EG_OPTIONS),
+    [join("a[b]c", "foo")],
+  );
+});