From ef98ee92c515d4543b8a717afbbbce168ba1aef6 Mon Sep 17 00:00:00 2001 From: Mathias Wulff Date: Fri, 23 Oct 2026 21:03:13 +1100 Subject: [PATCH] Add --literal-replacement to use the replacement as it is --- README.md | 3 +- src/cli.ts | 12 ++++++-- src/engine.ts | 11 +++++++ test/cli/nl.test.js | 7 ++--- test/cli/run.sh | 4 +++ test/engine/literal-replacement.test.js | 41 +++++++++++++++++++++++++ 6 files changed, 69 insertions(+), 9 deletions(-) create mode 100644 test/engine/literal-replacement.test.js diff --git a/README.md b/README.md index 2255212a..680f5e0d 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ If planned renames clash, the process is halted and no change is applied to any | `-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] | +| `-l` | **`--literal-replacement`** Use the replacement as it is. Without it, `$1`, `$&` and the other `$` signs in the replacement mean the groups and the match, as in javascript. [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] | @@ -310,7 +311,7 @@ If planned renames clash, the process is halted and no change is applied to any | | **`--name-delimiter-tail`** Print the name delimiter after the last name. Defaults to true. Use --no-name-delimiter-tail or --name-delimiter-tail=false to not print the delimiter after the last name. [boolean] | | | **`--content-delimiter-tail`** Print the content delimiter after the last content. Defaults to true, but to false when piped data is printed, so it is left as it came in. Matches from --output-match always get it, from a file or a pipe. [boolean] | | | **`--tail`** Shorthand for setting both --name-delimiter-tail and --content-delimiter-tail. Use --no-tail to not print the delimiter after the last name and the last content. The specific tail flags win over --tail. [boolean] | -| `-l` | **`--nl`** Let what is printed to stdout and stderr (the names, or the content from -o, -m or piped data) always end with a new line. No extra new line if it already ends with a new line, or if nothing was printed. [boolean] | +| | **`--nl`** Let what is printed to stdout and stderr (the names, or the content from -o, -m or piped data) always end with a new line. No extra new line if it already ends with a new line, or if nothing was printed. [boolean] | | `-H` | **`--halt`**, **`--bail`** Halt on first error [boolean] [default: false] | | `-c` | **`--status-compact`** As the last thing before ending any execution a status report will be printed as one line of JSON. Will print even in combination with `--quiet-total`. Will incude extra info about the inner workings of the engine in combination with the `--verbose` flag. [boolean] | | | **`--status`** Same as `--status-compact`, but always starting with a `\n` followed by pretty printed JSON. [boolean] | diff --git a/src/cli.ts b/src/cli.ts index 4fc796c7..d9899bc4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -75,6 +75,13 @@ const yargs = require('yargs') .describe('L', 'Literal string search (no regex used when searching)') .alias('L', 'literal') + .boolean('l') + .describe( + 'l', + 'Use the replacement as it is. Without it, `$1`, `$&` and the other `$` signs in the replacement mean the groups and the match, as in javascript.' + ) + .alias('l', 'literal-replacement') + .boolean('I') .describe('I', 'Void case insensitive search. Applies to the pattern and to all globs.') .alias('I', 'void-ignore-case') @@ -186,12 +193,11 @@ const yargs = require('yargs') 'Shorthand for setting both --name-delimiter-tail and --content-delimiter-tail. Use --no-tail to not print the delimiter after the last name and the last content. The specific tail flags win over --tail.' ) - .boolean('l') + .boolean('nl') .describe( - 'l', + 'nl', 'Let what is printed to stdout and stderr (the names, or the content from -o, -m or piped data) always end with a new line. No extra new line if it already ends with a new line, or if nothing was printed.' ) - .alias('l', 'nl') .boolean('H') .describe('H', 'Halt on first error') diff --git a/src/engine.ts b/src/engine.ts index 22edce16..be0466aa 100644 --- a/src/engine.ts +++ b/src/engine.ts @@ -146,6 +146,17 @@ async function run(config) { }; } + // --literal-replacement: the value of a function is used as it is, so `$1` and `$&` are just text. + // The code of --js is a string here too, but its value is already used as it is. + if ( + config.literalReplacement && + !config.replacementJs && + 'string' === typeof config.replacement + ) { + const text = config.replacement; + config.replacement = () => text; + } + const piped = handlePipedData(config); if (null !== config.pipedData) { diff --git a/test/cli/nl.test.js b/test/cli/nl.test.js index 602a026c..9dc3b57a 100644 --- a/test/cli/nl.test.js +++ b/test/cli/nl.test.js @@ -1,19 +1,16 @@ -// --nl (-l) ends stdout and stderr with a line break, unless they already end with one +// --nl ends stdout and stderr with a line break, unless they already end with one import {describe, test, expect} from 'bun:test'; import fs from 'fs'; import {$} from 'bun'; import {cli, sh} from '../helper.js'; describe('--nl', () => { - test('adds a line break after the last match, also as -l', async () => { + test('adds a line break after the last match', async () => { fs.writeFileSync('my.file', 'foobar\n'); fs.writeFileSync('your.file', 'abc123\n'); expect( (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail --nl`)).out ).toBe('b,b,c\n'); - expect( - (await sh($`bun ${cli} '[bc]' _ my.file your.file -m --delimiter ',' --no-tail -l`)).out - ).toBe('b,b,c\n'); }); test('comes after the tail', async () => { diff --git a/test/cli/run.sh b/test/cli/run.sh index e2130616..152f6319 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -178,6 +178,10 @@ reset assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar' +# --literal-replacement (-l) uses the replacement as it is, so `$1` is just text +reset +assert "rexreplace '(foo)' '€1' my.file -o --literal-replacement" '$1bar' + # --replacement-js: the code gives the replacement reset assert "rexreplace 'foo' '2+2' my.file -o --replacement-js" '4bar' diff --git a/test/engine/literal-replacement.test.js b/test/engine/literal-replacement.test.js new file mode 100644 index 00000000..7aea166e --- /dev/null +++ b/test/engine/literal-replacement.test.js @@ -0,0 +1,41 @@ +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {rr} from '../helper.js'; + +describe('--literal-replacement', () => { + test('uses the replacement as it is, with no meaning for $', async () => { + const config = {pipedData: 'PRICE', literalReplacement: true}; + expect((await rr('(PRICE)', 'only $1.99', config)).out).toBe('only $1.99'); + expect((await rr('(PRICE)', "$& $$ $` $'", config)).out).toBe("$& $$ $` $'"); + }); + + test('without it $1 is the group', async () => { + expect((await rr('(PRICE)', 'only $1.99', {pipedData: 'PRICE'})).out).toBe('only PRICE.99'); + }); + + test('works the same for a replacement from --replacement-pipe', async () => { + fs.writeFileSync('my.file', 'PRICE'); + const config = { + includeGlob: ['my.file'], + pipedData: '$1.99', + replacementPipe: true, + literalReplacement: true, + }; + await rr('(PRICE)', '_', config); + expect(fs.readFileSync('my.file', 'utf8')).toBe('$1.99'); + }); + + test('changes nothing for --js, as the value of the code is already used as it is', async () => { + const config = {pipedData: 'foo', replacementJs: true}; + expect((await rr('(o)', '"[$1]" + $1', config)).out).toBe('f[$1]o[$1]o'); + expect((await rr('(o)', '"[$1]" + $1', {...config, literalReplacement: true})).out).toBe( + 'f[$1]o[$1]o' + ); + }); + + test('works with --rename', async () => { + fs.writeFileSync('a.md', 'foo'); + const config = {includeGlob: ['a.md'], rename: true, literalReplacement: true}; + expect((await rr('^(a)', '$1-$$', config)).out).toBe('$1-$$.md\n'); + }); +});