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
12 changes: 11 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,20 @@ Files identified for potential processing are identified by:

- Piped data that is printed is left as it came in, so no delimiter is added after the end of it. Matches from `--output-match` are different: they are new output, so they get the delimiter whether they come from a file or from piped data.

- Finding no files is a warning (🟡) and not an error, so the exit code is 0. Nothing went wrong, there was just nothing to do. Errors start with ❌ and give the exit code 1.
- Finding no files is a warning (🟡) and not an error, so the exit code is 0. Nothing went wrong, there was just nothing to do.



### Exit codes

| Code | Meaning |
| --- | --- |
| 0 | Done. Warnings (🟡) are ok. |
| 2 | One or more files gave an error (❌), and the run went on. |
| 3 | The run stopped (❌ … `No further processing.`): a bad pattern, an unknown encoding, wrong flags, or any error with `--halt`. |

Code 1 is kept for a check that says no, like `grep` does when nothing is found.

### Limitations

- RexReplace reads each file fully into memory, so working on your 8Gb log files will probably not be ideal.
Expand Down
5 changes: 3 additions & 2 deletions devguide.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ All output goes through `src/output.ts`. Pick the function by who the message is
| `printDone()` | At the end of a run, also when it is stopped | Ends stdout and stderr with a line break when `--nl` is set | |
| `info(name)` | Unless `--quiet` | The name of an affected file. stdout, or stderr when stdout has content. | `a.md` |
| `warn(msg)` | Unless `--quiet` | Something is likely a mistake, but the run goes on. Also added to `status.warnings`. | `Data is piped in, but no flag tells what to do with it` |
| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. Starts with ❌. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 1. | `File not found: a.md` |
| `panic(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Starts with ❌ and ends with `No further processing.` Throws a panic, and the CLI exits with 1. Also added to `status.errors`. | `No data piped into replacement` |
| `error(msg, data)` | Unless `--quiet-total` | Something went wrong. Starts with ❌. The run goes on, unless `--halt` is set: then a panic is thrown. Also added to `status.errors`, so the CLI gives the exit code 2. | `File not found: a.md` |
| `panic(msg, data, help)` | Unless `--quiet-total` | Something went wrong and the run can not go on. Starts with ❌ and ends with `No further processing.` Throws a panic, and the CLI exits with 3. Also added to `status.errors`. | `No data piped into replacement` |
| `chat(msg)` | `--verbose`, also with `--quiet`, but not with `--quiet-total` | The user: what happens to each file. Only ever to stderr. Also kept in `engine.steps` of the status report. | `Open sync: a.md`, `Nothing changed in: a.md` |

- What the run did is in the status report: the engine resolves with it, and `--status` prints it. So there is no extra debug output to print along the way.
- `--debug` prints only the status report, with what went into the engine and what it did.
- `warn()`, `error()` and `panic()` add to the status report as they go, also when the quiet flags hide what they print.
- The exit code is 0 when the run is done, 2 when a file gave an error, and 3 for a panic or wrong flags. 1 is kept for a check that says no, like `grep`. See Exit codes in the README.

## Tests

Expand Down
16 changes: 12 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ const yargs = require('yargs')
// A file can be named with only digits, like `2024`, so each glob is kept as the string that was given
.parserConfiguration({'parse-positional-numbers': false})
.strict()
// Wrong flags are a panic, so the exit code is 3. The help and the message go to stderr, as yargs does it.
.fail((msg, err, yargs) => {
yargs.showHelp();
console.error(msg || err.message);
process.exit(3);
})

.usage(
'RexReplace ' +
Expand Down Expand Up @@ -466,7 +472,7 @@ function exitOnPanic(config, e) {
if (e.status) {
printStatus(config, e.status);
}
process.exit(1);
process.exit(3);
}

function backOut(exitcode = 1) {
Expand All @@ -492,8 +498,9 @@ function unescapeString(str = '') {
return new Function(`return '${source}'`)();
}

// Help that was asked for is not an error. Too few arguments are wrong flags, so a panic.
if (0 < needHelp) {
backOut(needHelp - 1);
backOut(1 === needHelp ? 0 : 3);
}

// All options into one big config object for the rexreplace core
Expand Down Expand Up @@ -562,13 +569,14 @@ if (config.contentDelimiter) {
unescape('contentDelimiter', '--content-delimiter');
}

// The exit code is only set, so the process ends by itself when all output is written
// The exit code is only set, so the process ends by itself when all output is written.
// 0: done, 2: an error in a file and the run went on, 3: a panic (the run stopped)
const run = () =>
rexreplace.engine(config).then(
status => {
printStatus(config, status);
if (status.errors.length) {
process.exitCode = 1;
process.exitCode = 2;
}
},
e => exitOnPanic(config, e)
Expand Down
6 changes: 3 additions & 3 deletions test/cli/debug.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,20 +38,20 @@ describe('--debug', () => {
expect(JSON.parse(result.out.slice('fxxbar\n'.length)).files.updated).toEqual(['my.file']);
});

test('gives the errors in the status report, and the exit code 1', async () => {
test('gives the errors in the status report, and the exit code 2', async () => {
fs.writeFileSync('a.file', 'foo');
fs.chmodSync('a.file', 0);
const result = await sh($`bun ${cli} o x a.file --debug`);
expect(result.err).toBe('');
expect(result.exit).toBe(1);
expect(result.exit).toBe(2);
expect(JSON.parse(result.out).errors[0]).toContain('EACCES');
});

test('gives the error of an invalid pattern in the status report', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await sh($`bun ${cli} '(' x my.file --debug`);
expect(result.err).toBe('');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(JSON.parse(result.out).errors[0]).toContain('Invalid regular expression');
});
});
2 changes: 1 addition & 1 deletion test/cli/delimiter.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ describe('an invalid escape sequence in a delimiter', () => {
fs.writeFileSync('my.file', 'foobar\n');
const result = await sh($`bun ${cli} b x my.file ${flag} '\x4'`);
expect(result.err).toContain(`Invalid escape sequence in ${flag}`);
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n');
}
);
Expand Down
4 changes: 2 additions & 2 deletions test/cli/encoding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ describe('--encoding', () => {
test('an unknown encoding stops the run with an error, and the file is not changed', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await sh($`bun ${cli} o x my.file --encoding nope`);
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(lastLine(result.err)).toBe('Unknown encoding: nope');
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});

test('an unknown encoding also stops the run for piped data', async () => {
const result = await sh($`echo -n foo | bun ${cli} o x --encoding nope`);
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(lastLine(result.err)).toBe('Unknown encoding: nope');
});

Expand Down
4 changes: 2 additions & 2 deletions test/cli/glob-pipe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ describe('--glob-pipe', () => {
fs.writeFileSync('my.file', 'foobar\n');
const result = await sh($`echo -n my.file | bun ${cli} b x --glob-pipe ${flag}`);
expect(result.err).toContain('are mutually exclusive');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n');
}
);

test('--null-in can only be used with --glob-pipe', async () => {
fs.writeFileSync('my.file', 'foobar\n');
const result = await sh($`echo -n my.file | bun ${cli} b x --null-in`);
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar\n');
});
});
7 changes: 5 additions & 2 deletions test/cli/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,15 @@ assert_failure "rexreplace my-vile"
assert_failure "rexreplace o-ho"


# The exit code: 0 when done, 2 when a file gave an error and the run went on, 3 when the run stopped
# --halt (or --bail) stops at the first error, here that my.file can not be read
reset
chmod 000 my.file
assert_raises "rexreplace b x my.file your.file --halt" 1
assert_raises "rexreplace b x my.file your.file --bail" 1
assert_raises "rexreplace b x my.file your.file --halt" 3
assert_raises "rexreplace b x my.file your.file --bail" 3
assert "cat your.file" "abc123"
assert_raises "rexreplace b x my.file your.file" 2
assert "cat your.file" "axc123"

# Finding no files is a warning and not an error
assert_raises "rexreplace o x nope.file" 0
Expand Down
2 changes: 1 addition & 1 deletion test/engine/basics.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ describe('pattern', () => {
test('stops with an error and leaves the files alone if it is not a valid regex', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await rr('(', 'x', {includeGlob: ['my.file']});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain('Invalid regular expression');
expect(result.out).toBe('');
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
Expand Down
2 changes: 1 addition & 1 deletion test/engine/encoding.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe('--encoding', () => {
const unknownEncoding = {
out: '',
err: ' ❌ Unknown encoding: nope\n No further processing.\n\n',
exit: 1,
exit: 3,
};

test('stops the run with an error if it is unknown (#690)', async () => {
Expand Down
8 changes: 4 additions & 4 deletions test/engine/engine.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('--engine', () => {
test('stops with an error if the engine is the one of another runtime', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await rr('o', 'x', {includeGlob: ['my.file'], engine: 'V8'});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain(
'When running via the Bun runtime you can only use default or JSC as engine'
);
Expand All @@ -25,13 +25,13 @@ describe('--engine', () => {
test('stops with an error if the engine is unknown', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await rr('o', 'x', {includeGlob: ['my.file'], engine: 'nope'});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain('Engine nope not supported');
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});

test('is case sensitive', async () => {
expect((await rr('o', 'x', {pipedData: 'foobar', engine: 'jsc'})).exit).toBe(1);
expect((await rr('o', 'x', {pipedData: 'foobar', engine: 'jsc'})).exit).toBe(3);
});

describe('combined', () => {
Expand All @@ -54,7 +54,7 @@ describe('--engine', () => {
engine: 'nope',
quietTotal: true,
});
expect(result).toEqual({out: '', err: '', exit: 1});
expect(result).toEqual({out: '', err: '', exit: 3});
});
});
});
10 changes: 5 additions & 5 deletions test/engine/halt.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('--halt', () => {
fs.chmodSync('a.file', 0);
const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], halt: true});
expect(result.err).toContain('EACCES');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('b.file', 'utf8')).toBe('foo');
});

Expand All @@ -20,7 +20,7 @@ describe('--halt', () => {
fs.chmodSync('a.file', 0);
const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file']});
expect(result.err).toContain('EACCES');
expect(result.exit).toBe(1);
expect(result.exit).toBe(2);
expect(fs.readFileSync('b.file', 'utf8')).toBe('fxx');
});

Expand All @@ -34,8 +34,8 @@ describe('--halt', () => {
test('stops with a number as exit code', async () => {
fs.writeFileSync('a.file', 'foo');
fs.chmodSync('a.file', 0);
expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).exit).toBe(1);
expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(1);
expect((await rr('o', 'x', {includeGlob: ['a.file'], halt: true})).exit).toBe(3);
expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).toBe(3);
});

describe('combined', () => {
Expand All @@ -52,7 +52,7 @@ describe('--halt', () => {

test('with an invalid pattern it stops with or without it', async () => {
expect((await rr('(', 'x', {pipedData: 'foobar', halt: true})).exit).not.toBe(null);
expect((await rr('(', 'x', {pipedData: 'foobar'})).exit).toBe(1);
expect((await rr('(', 'x', {pipedData: 'foobar'})).exit).toBe(3);
});

test('with piped data and several flags when all goes well', async () => {
Expand Down
4 changes: 2 additions & 2 deletions test/engine/quiet-total.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe('--quiet-total', () => {

test('still stops on an error', async () => {
fs.writeFileSync('my.file', 'foobar');
expect((await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).exit).toBe(1);
expect((await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});

Expand All @@ -50,7 +50,7 @@ describe('--quiet-total', () => {
test('with --replacement-pipe and nothing piped it stops in silence', async () => {
fs.writeFileSync('my.file', 'foobar');
const config = {includeGlob: ['my.file'], quietTotal: true, replacementPipe: true};
expect(await rr('o', '_', config)).toEqual({out: '', err: '', exit: 1});
expect(await rr('o', '_', config)).toEqual({out: '', err: '', exit: 3});
});

test('with --halt it stops in silence', async () => {
Expand Down
2 changes: 1 addition & 1 deletion test/engine/quiet.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ describe('--quiet', () => {

test('still stops on an invalid pattern', async () => {
fs.writeFileSync('my.file', 'foobar');
expect((await rr('(', 'x', {includeGlob: ['my.file'], quiet: true})).exit).toBe(1);
expect((await rr('(', 'x', {includeGlob: ['my.file'], quiet: true})).exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});

Expand Down
2 changes: 1 addition & 1 deletion test/engine/replacement-pipe.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ describe('--replacement-pipe', () => {
test('stops with an error if nothing is piped in', async () => {
fs.writeFileSync('my.file', 'foobar');
const result = await rr('o', '_', {includeGlob: ['my.file'], replacementPipe: true});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain('No data piped into replacement');
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});
Expand Down
2 changes: 1 addition & 1 deletion test/engine/unicode.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ describe('--unicode', () => {
test('is strict about escapes that have no meaning', async () => {
expect((await rr('\\-', 'x', {pipedData: 'a-b'})).out).toBe('axb');
const result = await rr('\\-', 'x', {pipedData: 'a-b', unicode: true});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain('Invalid regular expression');
expect(result.out).toBe('');
});
Expand Down
2 changes: 1 addition & 1 deletion test/engine/void-section.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('--void-section', () => {

test('a lone § at the end of the pattern is an unfinished escape', async () => {
const result = await rr('a§', 'x', {pipedData: 'a§'});
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(result.err).toContain('Invalid regular expression');
expect((await rr('a§', 'x', {pipedData: 'a§', voidSection: true})).out).toBe('x');
});
Expand Down
6 changes: 3 additions & 3 deletions test/fix/681.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ describe('#681 --halt exits with a numeric non-zero code', () => {
fs.chmodSync('a.file', 0);
const result = await rr('o', 'x', {includeGlob: ['a.file'], halt: true});
expect(result.err).toContain('EACCES');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
});

test('invalid regular expression', async () => {
const result = await rr('(', 'x', {pipedData: 'foobar', halt: true});
expect(result.err).toContain('Invalid regular expression');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
});

test('--quiet-total still halts with the exit code but shows no message', async () => {
Expand All @@ -27,6 +27,6 @@ describe('#681 --halt exits with a numeric non-zero code', () => {
halt: true,
quietTotal: true,
});
expect(result).toEqual({out: '', err: '', exit: 1});
expect(result).toEqual({out: '', err: '', exit: 3});
});
});
4 changes: 2 additions & 2 deletions test/fix/682.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ describe('#682 --quiet still displays errors', () => {
fs.writeFileSync('my.file', 'foobar');
const result = await rr('(', 'x', {includeGlob: ['my.file'], quiet: true});
expect(result.err).toContain('Invalid regular expression');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar');
});

Expand All @@ -35,7 +35,7 @@ describe('#682 --quiet still displays errors', () => {
expect(await rr('(', 'x', {includeGlob: ['my.file'], quietTotal: true})).toEqual({
out: '',
err: '',
exit: 1,
exit: 3,
});
expect((await rr('o', 'x', {includeGlob: ['nope.file'], quietTotal: true})).err).toBe('');
});
Expand Down
2 changes: 1 addition & 1 deletion test/fix/683.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ describe('#683 --exclude-re follows the regex flags of the main search', () => {
fs.writeFileSync('my.file', 'foo');
const result = await rr('foo', 'x', {includeGlob: ['my.file'], excludeRe: ['(']});
expect(result.err).toContain('Invalid regular expression');
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(fs.readFileSync('my.file', 'utf8')).toBe('foo');
});
});
4 changes: 2 additions & 2 deletions test/fix/691.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,10 @@ describe('#691 replacement that used to crash the CLI', () => {
expect(result.err).toBe(
' ❌ Invalid escape sequence in replacement: \\x4\n No further processing.\n\n'
);
expect(result.exit).toBe(1);
expect(result.exit).toBe(3);
expect(
await sh($`bun ${cli} o ${'\\x4'} my.file --output --no-content-delimiter-tail --quiet-total`)
).toEqual({out: '', err: '', exit: 1});
).toEqual({out: '', err: '', exit: 3});
});

test.each([
Expand Down
Loading
Loading