From 2eb04a5722cffccebf1e9e7a5306596d0eb1ca7e Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 12:23:53 +1100 Subject: [PATCH] Make globs case-insensitive like the search to close #716 --- README.md | 3 ++- src/cli.ts | 2 +- src/engine.ts | 11 +++++++---- test/cli/run.sh | 7 +++++++ test/js/fix/716.test.js | 39 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 test/js/fix/716.test.js diff --git a/README.md b/README.md index 9e16866f..d88508b2 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ Use --js-pipe to access the pipe data in your javascript code. | `-v` | **`--version`** Print rexreplace version (can be given as only argument) [boolean] | | `-V` | **`--verbose`** More chatty output [boolean] | | `-L` | **`--literal`** Literal string search (no regex used when searching) [boolean] | -| `-I` | **`--void-ignore-case`** Void case insensitive search pattern. [boolean] | +| `-I` | **`--void-ignore-case`** Void case insensitive search. Applies to the pattern and to all globs. [boolean] | | `-G` | **`--void-global`** Void global search (stop looking after the first match). [boolean] | | `-s` | **`--dot-all`** Have `.` also match newline. [boolean] | | `-M` | **`--void-multiline`** Void multiline search pattern. Makes ^ and $ match start/end of whole content rather than each line. [boolean] | @@ -269,6 +269,7 @@ Files identified for potential processing are identified by: - Group C: All `-X` values are globbed as a pool (meaning you can use the `!` notation) - Subtract group C from group A and B (the files are compared by their full path) - Remove any file paths that matches any `-x` provided regex. Use `--full-path` to look for the full path. +- All globs are case-insensitive, like the search, unless `--void-ignore-case` is set. ### Features diff --git a/src/cli.ts b/src/cli.ts index ae2081b6..b6c4564c 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -70,7 +70,7 @@ const yargs = require('yargs') .alias('L', 'literal') .boolean('I') - .describe('I', 'Void case insensitive search pattern.') + .describe('I', 'Void case insensitive search. Applies to the pattern and to all globs.') .alias('I', 'void-ignore-case') .boolean('G') diff --git a/src/engine.ts b/src/engine.ts index fe63131c..d5b3e34f 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -727,7 +727,10 @@ function localTimeString(dateObj = new Date()) { function getFilePaths(conf) { const {includeGlob, excludeGlob, excludeRe} = conf; - let files = findFiles(includeGlob); + // All globs follow the search: case-insensitive, unless --void-ignore-case + const globOptions = {caseSensitiveMatch: Boolean(conf.voidIgnoreCase)}; + + let files = findFiles(includeGlob, globOptions); // With --full-path each file is known by its full path from here on. That is what --exclude-re sees, and the name that is printed. if (conf.fullPath) { @@ -736,7 +739,7 @@ function getFilePaths(conf) { // --exclude-glob is always globs, also for the name of a file. They are used together, and the files they find are taken away. if (excludeGlob.length) { - const excluded = new Set(globs.sync(excludeGlob).map(file => path.resolve(file))); + const excluded = new Set(globs.sync(excludeGlob, globOptions).map(file => path.resolve(file))); files = files.filter(file => !excluded.has(path.resolve(file))); } @@ -751,7 +754,7 @@ function getFilePaths(conf) { // A path that is a file is used as it is, so a name with glob characters, like `a[1].md` or `my (1).md`, gives only that file. // The other paths are globbed together, so a pattern that starts with `!` can take files away from the other patterns. -function findFiles(paths) { +function findFiles(paths, globOptions) { const files = []; const patterns = []; paths.forEach(file => { @@ -764,7 +767,7 @@ function findFiles(paths) { // A file given by its name and also found by a glob is only handled once const seen = new Set(); - return files.concat(patterns.length ? globs.sync(patterns) : []).filter(file => { + return files.concat(patterns.length ? globs.sync(patterns, globOptions) : []).filter(file => { const fullPath = path.resolve(file); if (seen.has(fullPath)) { return false; diff --git a/test/cli/run.sh b/test/cli/run.sh index 5b2e01f2..200860da 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -451,6 +451,13 @@ assert "rexreplace 'b' 'x' my.file --content-delimiter '\x4' 2>&1 | grep -c assert "rexreplace 'b' 'x' my.file --delimiter '\x4' 2>&1 | grep -c 'Invalid escape sequence in --delimiter'" '1' assert "cat my.file" 'foobar' +# Globs are case-insensitive like the search, unless --void-ignore-case (#716) +reset +assert "rexreplace 'oo' 'x' '*.FILE' -X 'YOUR*'" 'my.file' +assert "cat my.file" 'fxbar' +reset +assert_raises "rexreplace 'oo' 'x' '*.FILE' --void-ignore-case" 1 + # --simulate: the files that would be changed are named, but nothing is written (#710) reset assert "rexreplace 'oo' 'x' my.file your.file --simulate" 'my.file' diff --git a/test/js/fix/716.test.js b/test/js/fix/716.test.js new file mode 100644 index 00000000..ba77a265 --- /dev/null +++ b/test/js/fix/716.test.js @@ -0,0 +1,39 @@ +// https://github.com/mathiasrw/rexreplace/issues/716 +// The search was case-insensitive by default, but the globs were case-sensitive. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {rr} from '../helper.js'; + +// The names differ in more than case, so they can both be there on a file system that is case-insensitive +function makeFiles() { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('B.FILE', 'foo'); +} + +describe('#716 the globs are case-insensitive like the search', () => { + test('a glob finds a file whatever the case of its name', async () => { + makeFiles(); + expect(await rr('o', 'x', {includeGlob: ['*.file']})).toEqual({ + out: 'B.FILE\na.file\n', + err: '', + exit: null, + }); + }); + + test('--exclude-glob leaves out a file whatever the case of its name', async () => { + makeFiles(); + // `*.*` finds both files also when globs are case-sensitive, so only --exclude-glob is tested + const config = {includeGlob: ['*.*'], excludeGlob: ['b.*']}; + expect((await rr('o', 'x', config)).out).toBe('a.file\n'); + expect(fs.readFileSync('B.FILE', 'utf8')).toBe('foo'); + }); + + test('--void-ignore-case makes all globs case-sensitive', async () => { + makeFiles(); + const config = {includeGlob: ['*.file'], voidIgnoreCase: true}; + expect((await rr('o', 'x', config)).out).toBe('a.file\n'); + fs.writeFileSync('a.file', 'foo'); + const excluded = {includeGlob: ['*.*'], excludeGlob: ['b.*'], voidIgnoreCase: true}; + expect((await rr('o', 'x', excluded)).out).toBe('B.FILE\na.file\n'); + }); +});