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
33 changes: 27 additions & 6 deletions src/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ const re = {
// The numbers of #791 that count what came before: $i, $c, $m and $M with their cousins, like $i00 or $M_
runNumbers: /\$[icmM](?![A-Za-z])/,
matchNumbers: /\$[mM](?![A-Za-z])/,
// A path that is not clear as it is printed: a space at the start or the end, or a char that can not be seen,
// like a line break from echo or a zero-width space
unclearPath: /^\s|\s$|[\x00-\x1f\x7f-\x9f\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff]/,
// The chars of unclearPath that JSON.stringify does not escape
invisibleChars: /[\x7f-\x9f\u200b-\u200f\u2028-\u202e\u2060-\u2064\ufeff]/g,
// The end of the message of a failed file call, like `, open 'nope.txt'`
fileCallInMessage: /, \w+( '.*')?$/s,
};

// Only the version ends up in the bundle
Expand Down Expand Up @@ -696,13 +703,27 @@ function getFinalReplacement(replacement, conf: any) {
try {
return readReplacementFile(replacement, conf);
} catch (e) {
return panic('The replacement file can not be read:', replacement);
return panic('The replacement file can not be read:', pathAndReason(replacement, e));
}
}

return replacement;
}

// The path and why it can not be read, like `nope.txt (ENOENT: no such file or directory)`.
// A path that is not clear as it is printed is shown as JSON, with each char that can not be seen escaped.
function pathAndReason(filepath, e) {
let shownPath = filepath;
if (re.unclearPath.test(filepath)) {
shownPath = JSON.stringify(filepath).replace(
re.invisibleChars,
char => '\\u' + char.charCodeAt(0).toString(16).padStart(4, '0')
);
}
// The call and the path at the end of the message are told already
return shownPath + ' (' + e.message.replace(re.fileCallInMessage, '') + ')';
}

// 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();
Expand All @@ -720,16 +741,16 @@ function codeReplacement(file, config, text, numbers) {
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();
// A path that can not be read is an error once in the run, and its matches are left as they are
config.unreadableFiles ??= new Set();
return (...args) => {
const filepath = String(code(...args));
if (!failed.has(filepath)) {
if (!config.unreadableFiles.has(filepath)) {
try {
return readReplacementFile(filepath, config);
} catch (e) {
failed.add(filepath);
error('The replacement file can not be read:', filepath);
config.unreadableFiles.add(filepath);
error('The replacement file can not be read:', pathAndReason(filepath, e));
}
}
return args[0];
Expand Down
6 changes: 4 additions & 2 deletions test/engine/replacement-file.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ describe('--replacement-file', () => {
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.err).toStartWith(
' ❌ The replacement file can not be read: nope.txt (ENOENT: no such file or directory)'
);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foo');
});
Expand All @@ -68,7 +70,7 @@ describe('--replacement-file', () => {
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',
err: ' ❌ The replacement file can not be read: nope.txt (ENOENT: no such file or directory)\n\n',
exit: 2,
});
});
Expand Down
43 changes: 43 additions & 0 deletions test/fix/818.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// https://github.com/mathiasrw/rexreplace/issues/818
// A replacement file that could not be read gave no reason, and with --js the error came once for each file.
import {describe, test, expect} from 'bun:test';
import fs from 'fs';
import {rr, rrStatus} from '../helper.js';

describe('#818 a replacement file that can not be read', () => {
test.each([
['is not there', 'nope.txt', 'nope.txt (ENOENT: no such file or directory)'],
['is a folder', 'adir', 'adir (EISDIR: illegal operation on a directory)'],
[
'has a zero-width space',
'no\u200bpe.txt',
'"no\\u200bpe.txt" (ENOENT: no such file or directory)',
],
['has a C1 char', 'no\u0085pe.txt', '"no\\u0085pe.txt" (ENOENT: no such file or directory)'],
[
'has a line break at the end, like from echo',
'a.txt\n',
'"a.txt\\n" (ENOENT: no such file or directory)',
],
[
'has a backslash, like a Windows path',
'my\\dir/nope.txt',
'my\\dir/nope.txt (ENOENT: no such file or directory)',
],
])('tells the reason when the path %s', async (name, filepath, text) => {
fs.mkdirSync('adir');
fs.writeFileSync('a.txt', 'X');
const result = await rr('X', filepath, {includeGlob: ['a.txt'], replacementFile: true});
expect(result.err).toStartWith(' ❌ The replacement file can not be read: ' + text);
});

test('with --js is told once in the run, not once for each file', async () => {
fs.writeFileSync('a.txt', 'X');
fs.writeFileSync('b.txt', 'X');
const config = {includeGlob: ['*.txt'], replacementJs: true, replacementFile: true};
const status = await rrStatus('X', '"nope.txt"', config);
expect(status.errors).toEqual([
'The replacement file can not be read: nope.txt (ENOENT: no such file or directory)',
]);
});
});
Loading