Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions README.md

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ const yargs = require('yargs')
)
.alias('l', 'literal-replacement')

.boolean('replacement-file')
.describe(
'replacement-file',
'The replacement is the path to a file, and the content of the file is used as the replacement. With --js the code gives the path.'
)

.boolean('trim-file')
.describe('trim-file', 'Trim the content of the replacement file.')

.boolean('trim')
.describe('trim', 'Shorthand for --trim-pipe and --trim-file.')

.boolean('I')
.describe('I', 'Void case insensitive search. Applies to the pattern and to all globs.')
.alias('I', 'void-ignore-case')
Expand Down Expand Up @@ -547,6 +559,10 @@ Object.keys(yargs.argv).forEach(key => {
}
});

if (config.trim) {
config.trimPipe = config.trimFile = true;
}

if (config.jsPipe) {
config.replacementJs = config.pipe = true;
}
Expand Down
56 changes: 50 additions & 6 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ function doReplacement(_file_rr: string, _config_rr: any, _data_rr: string) {
const result = _data_rr.replace(
_config_rr.regex,
_config_rr.replacementJs && !_config_rr.outputMatch
? dynamicReplacement(_file_rr, _config_rr, _data_rr)
? codeReplacement(_file_rr, _config_rr, _data_rr)
: _config_rr.replacement
);

Expand Down Expand Up @@ -342,7 +342,7 @@ function renameFiles(files, config) {
chat('Work on the ' + (config.renamePath ? 'path' : 'name') + ' of: ' + file);
const replacement =
config.replacementJs && !config.outputMatch
? dynamicReplacement(file, config, text)
? codeReplacement(file, config, text)
: config.replacement;
const result = text.replace(config.regex, replacement);
if (result === text) {
Expand Down Expand Up @@ -565,10 +565,6 @@ function getFinalPattern(pattern, conf: any) {
}

function getFinalReplacement(replacement, conf: any) {
/*if(config.replacementFile){
return oneLinerFromFile(fs.readFileSync(replacement,'utf8'));
}*/

if (conf.replacementPipe) {
conf.pipedDataUsed = true;
if (null === conf.pipedData) {
Expand All @@ -578,6 +574,11 @@ function getFinalReplacement(replacement, conf: any) {
}

if (conf.outputMatch) {
if (conf.replacementFile) {
warn(
'The replacement file is not read with --output-match, as only the matches are printed.'
);
}
if (parseInt(process.versions.node) < 6) {
return panic('outputMatch is only supported in node 6+');
}
Expand Down Expand Up @@ -605,9 +606,52 @@ function getFinalReplacement(replacement, conf: any) {
return panic('Captured groups for javascript replacement is only supported in node 6+');
}

// --replacement-file: the replacement is the path to the file with the replacement.
// With --js the code gives the path, so the file is read when the code has run.
if (conf.replacementFile && !conf.replacementJs) {
try {
return readReplacementFile(replacement, conf);
} catch (e) {
return panic('The replacement file can not be read:', replacement);
}
}

return replacement;
}

// Each replacement file is read once, also when the code of --js gives the same path for many matches
function readReplacementFile(filepath, conf) {
conf.replacementFiles ??= new Map();
if (!conf.replacementFiles.has(filepath)) {
chat('Read the replacement file: ' + filepath);
const content = fs.readFileSync(filepath, conf.encoding);
conf.replacementFiles.set(filepath, conf.trimFile ? content.trim() : content);
}
return conf.replacementFiles.get(filepath);
}

// The code of --js. With --replacement-file its value is the path to the file with the replacement.
function codeReplacement(file, config, text) {
const code = dynamicReplacement(file, config, text);
if (!config.replacementFile) {
return code;
}
// A path that can not be read is an error once, and its matches are left as they are
const failed = new Set();
return (...args) => {
const filepath = String(code(...args));
if (!failed.has(filepath)) {
try {
return readReplacementFile(filepath, config);
} catch (e) {
failed.add(filepath);
error('The replacement file can not be read:', filepath);
}
}
return args[0];
};
}

/*function oneLinerFromFile(str){
let lines = str.split("\n");
if(lines.length===1){
Expand Down
5 changes: 5 additions & 0 deletions test/cli/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ reset
assert "rexreplace '*+*' 'b' my.file -o --literal" 'foobar'


# --replacement-file: the replacement is the path to a file, and its content goes in
reset
echo 'baz' > rep.file
assert "rexreplace 'foo' rep.file my.file -o --replacement-file --trim-file" 'bazbar'

# --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'
Expand Down
14 changes: 14 additions & 0 deletions test/cli/trim.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// --trim is --trim-pipe and --trim-file together
import {describe, test, expect} from 'bun:test';
import fs from 'fs';
import {$} from 'bun';
import {cli, sh} from '../helper.js';

describe('--trim', () => {
test('trims the piped data and the content of the replacement file', async () => {
fs.writeFileSync('footer.html', '<footer>\n');
fs.writeFileSync('page.html', '{{footer}}\n');
const command = $`echo ' footer.html ' | bun ${cli} '\{\{footer\}\}' _ page.html -R --replacement-file --trim -o`;
expect((await sh(command)).out).toBe('<footer>\n');
});
});
90 changes: 88 additions & 2 deletions test/engine/replacement-file.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,91 @@
import {describe, test} from 'bun:test';
import {describe, test, expect} from 'bun:test';
import fs from 'fs';
import {rr} from '../helper.js';

describe('--replacement-file', () => {
test.todo('is not implemented yet');
test('uses the content of the file at the path as the replacement', async () => {
fs.writeFileSync('footer.html', '<footer>');
const config = {pipedData: 'a {{footer}}', replacementFile: true};
expect((await rr('\\{\\{footer\\}\\}', 'footer.html', config)).out).toBe('a <footer>');
});

test('gives $1 in the content its meaning, unless --literal-replacement', async () => {
fs.writeFileSync('price.txt', 'only $1.99');
const config = {pipedData: 'PRICE', replacementFile: true};
expect((await rr('(PRICE)', 'price.txt', config)).out).toBe('only PRICE.99');
expect((await rr('(PRICE)', 'price.txt', {...config, literalReplacement: true})).out).toBe(
'only $1.99'
);
});

test('does not use the € and § aliases in the content, as they are only for what is typed', async () => {
fs.writeFileSync('cost.txt', '5€');
expect((await rr('x', 'cost.txt', {pipedData: 'x', replacementFile: true})).out).toBe('5€');
});

test('takes the path from --replacement-pipe', async () => {
fs.writeFileSync('footer.html', '<footer>');
fs.writeFileSync('page.html', '{{footer}}');
const config = {
includeGlob: ['page.html'],
pipedData: 'footer.html',
replacementPipe: true,
replacementFile: true,
};
await rr('\\{\\{footer\\}\\}', '_', config);
expect(fs.readFileSync('page.html', 'utf8')).toBe('<footer>');
});

test('with --js the code gives the path, and each file is read once', async () => {
fs.mkdirSync('partials');
fs.writeFileSync('partials/a.html', 'A');
fs.writeFileSync('partials/b.html', 'B');
const config = {
pipedData: '{{a}}{{b}}{{a}}',
replacementJs: true,
replacementFile: true,
verbose: true,
};
const result = await rr('\\{\\{(\\w+)\\}\\}', '"partials/" + $1 + ".html"', config);
expect(result.out).toBe('ABA');
expect(
result.err.split('\n').filter(line => line.startsWith('Read the replacement file'))
).toEqual([
'Read the replacement file: partials/a.html',
'Read the replacement file: partials/b.html',
]);
});

test('that can not be read stops the run before any file is changed', async () => {
fs.writeFileSync('my.file', 'foo');
const result = await rr('foo', 'nope.txt', {includeGlob: ['my.file'], replacementFile: true});
expect(result.err).toStartWith(' ❌ The replacement file can not be read: nope.txt');
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foo');
});

test('with --js a path that can not be read is an error, and the match is left as it is', async () => {
const config = {pipedData: 'foo', replacementJs: true, replacementFile: true};
expect(await rr('o', '"nope.txt"', config)).toEqual({
out: 'foo',
err: ' ❌ The replacement file can not be read: nope.txt\n\n',
exit: 2,
});
});

test('is not read with --output-match, and a warning tells so', async () => {
const config = {pipedData: 'foo', replacementFile: true, outputMatch: true};
expect(await rr('o', 'nope.txt', config)).toEqual({
out: 'o\no\n',
err: ' 🟡 The replacement file is not read with --output-match, as only the matches are printed.\n\n',
exit: null,
});
});

test('gives the new name with --rename', async () => {
fs.writeFileSync('a.md', 'foo');
fs.writeFileSync('name.txt', 'b');
const config = {includeGlob: ['a.md'], rename: true, replacementFile: true};
expect((await rr('^a', 'name.txt', config)).out).toBe('b.md\n');
});
});
20 changes: 20 additions & 0 deletions test/engine/trim-file.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {describe, test, expect} from 'bun:test';
import fs from 'fs';
import {rr} from '../helper.js';

describe('--trim-file', () => {
test('trims the content of the replacement file, like the line break at the end', async () => {
fs.writeFileSync('footer.html', '<footer>\n');
const config = {pipedData: 'a {{footer}} b', replacementFile: true};
expect((await rr('\\{\\{footer\\}\\}', 'footer.html', config)).out).toBe('a <footer>\n b');
expect((await rr('\\{\\{footer\\}\\}', 'footer.html', {...config, trimFile: true})).out).toBe(
'a <footer> b'
);
});

test('does not trim the piped data', async () => {
fs.writeFileSync('footer.html', '<footer>\n');
const config = {pipedData: ' {{footer}} \n', replacementFile: true, trimFile: true};
expect((await rr('\\{\\{footer\\}\\}', 'footer.html', config)).out).toBe(' <footer> \n');
});
});
Loading