diff --git a/README.md b/README.md index 56ad926a..b1061590 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/devguide.md b/devguide.md index c393494e..e02049e6 100644 --- a/devguide.md +++ b/devguide.md @@ -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 diff --git a/src/cli.ts b/src/cli.ts index 0287c387..adc11505 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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 ' + @@ -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) { @@ -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 @@ -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) diff --git a/test/cli/debug.test.js b/test/cli/debug.test.js index 3cba492f..f5754ab6 100644 --- a/test/cli/debug.test.js +++ b/test/cli/debug.test.js @@ -38,12 +38,12 @@ 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'); }); @@ -51,7 +51,7 @@ describe('--debug', () => { 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'); }); }); diff --git a/test/cli/delimiter.test.js b/test/cli/delimiter.test.js index 5c00b819..215df651 100644 --- a/test/cli/delimiter.test.js +++ b/test/cli/delimiter.test.js @@ -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'); } ); diff --git a/test/cli/encoding.test.js b/test/cli/encoding.test.js index 8c8b5465..bcf31009 100644 --- a/test/cli/encoding.test.js +++ b/test/cli/encoding.test.js @@ -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'); }); diff --git a/test/cli/glob-pipe.test.js b/test/cli/glob-pipe.test.js index 9b7315d3..959e175a 100644 --- a/test/cli/glob-pipe.test.js +++ b/test/cli/glob-pipe.test.js @@ -21,7 +21,7 @@ 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'); } ); @@ -29,7 +29,7 @@ describe('--glob-pipe', () => { 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'); }); }); diff --git a/test/cli/run.sh b/test/cli/run.sh index 06596d6e..b4ac8284 100644 --- a/test/cli/run.sh +++ b/test/cli/run.sh @@ -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 diff --git a/test/engine/basics.test.js b/test/engine/basics.test.js index 6dd1a679..d4c68c6d 100644 --- a/test/engine/basics.test.js +++ b/test/engine/basics.test.js @@ -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'); diff --git a/test/engine/encoding.test.js b/test/engine/encoding.test.js index 69ec6b00..e18fdd2e 100644 --- a/test/engine/encoding.test.js +++ b/test/engine/encoding.test.js @@ -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 () => { diff --git a/test/engine/engine.test.js b/test/engine/engine.test.js index 84d2fd5e..7e8d71d8 100644 --- a/test/engine/engine.test.js +++ b/test/engine/engine.test.js @@ -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' ); @@ -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', () => { @@ -54,7 +54,7 @@ describe('--engine', () => { engine: 'nope', quietTotal: true, }); - expect(result).toEqual({out: '', err: '', exit: 1}); + expect(result).toEqual({out: '', err: '', exit: 3}); }); }); }); diff --git a/test/engine/halt.test.js b/test/engine/halt.test.js index 6b62e10a..8df1823a 100644 --- a/test/engine/halt.test.js +++ b/test/engine/halt.test.js @@ -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'); }); @@ -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'); }); @@ -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', () => { @@ -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 () => { diff --git a/test/engine/quiet-total.test.js b/test/engine/quiet-total.test.js index 80c5cf56..80ac758d 100644 --- a/test/engine/quiet-total.test.js +++ b/test/engine/quiet-total.test.js @@ -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'); }); @@ -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 () => { diff --git a/test/engine/quiet.test.js b/test/engine/quiet.test.js index d0ffdd88..bbc9fd8d 100644 --- a/test/engine/quiet.test.js +++ b/test/engine/quiet.test.js @@ -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'); }); diff --git a/test/engine/replacement-pipe.test.js b/test/engine/replacement-pipe.test.js index ca544000..6ce19b0e 100644 --- a/test/engine/replacement-pipe.test.js +++ b/test/engine/replacement-pipe.test.js @@ -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'); }); diff --git a/test/engine/unicode.test.js b/test/engine/unicode.test.js index 5510a18c..0b412258 100644 --- a/test/engine/unicode.test.js +++ b/test/engine/unicode.test.js @@ -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(''); }); diff --git a/test/engine/void-section.test.js b/test/engine/void-section.test.js index 3ba95863..5bab9a63 100644 --- a/test/engine/void-section.test.js +++ b/test/engine/void-section.test.js @@ -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'); }); diff --git a/test/fix/681.test.js b/test/fix/681.test.js index 8a07a794..733c505c 100644 --- a/test/fix/681.test.js +++ b/test/fix/681.test.js @@ -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 () => { @@ -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}); }); }); diff --git a/test/fix/682.test.js b/test/fix/682.test.js index cd2f45b1..44fb8f3a 100644 --- a/test/fix/682.test.js +++ b/test/fix/682.test.js @@ -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'); }); @@ -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(''); }); diff --git a/test/fix/683.test.js b/test/fix/683.test.js index e73ab08c..25310011 100644 --- a/test/fix/683.test.js +++ b/test/fix/683.test.js @@ -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'); }); }); diff --git a/test/fix/691.test.js b/test/fix/691.test.js index 75f818a1..a97a9882 100644 --- a/test/fix/691.test.js +++ b/test/fix/691.test.js @@ -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([ diff --git a/test/fix/692.test.js b/test/fix/692.test.js new file mode 100644 index 00000000..decbc4fc --- /dev/null +++ b/test/fix/692.test.js @@ -0,0 +1,31 @@ +// https://github.com/mathiasrw/rexreplace/issues/692 +// A single argument containing -v or -h printed the version or help. The checks live in the CLI, so the CLI is run. +import {describe, test, expect} from 'bun:test'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +describe('#692 a single argument', () => { + test.each(['-v', '-version', '--version'])('%s prints only the version', async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result).toEqual({out: (await sh($`bun ${cli} --version`)).out, err: '', exit: 0}); + expect(result.out).toMatch(/^\S+\n$/); + }); + + test.each(['-h', '-help', '--help'])('%s prints the help to stdout', async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result.out).toContain('Options:'); + expect(result.err).toBe(''); + expect(result.exit).toBe(0); + }); + + // -V is verbose and -H is halt: other flags, not a request for the version or the help + test.each(['my-vile', '-V', 'o-ho', '-H', 'a-v', 'a-h', 'xversion', 'xhelp'])( + '%s is wrong use: the help goes to stderr, nothing to stdout, and the exit code is 3', + async arg => { + const result = await sh($`bun ${cli} ${arg}`); + expect(result.out).toBe(''); + expect(result.err).toContain('Options:'); + expect(result.exit).toBe(3); + } + ); +}); diff --git a/test/fix/700.test.js b/test/fix/700.test.js index 173d9050..474fa405 100644 --- a/test/fix/700.test.js +++ b/test/fix/700.test.js @@ -40,7 +40,7 @@ describe('#700 § is a full substitute for \\ in the replacement', () => { expect(await replaceB('§x4')).toEqual({ out: '', err: ' ❌ Invalid escape sequence in replacement: §x4\n No further processing.\n\n', - exit: 1, + exit: 3, }); }); diff --git a/test/fix/703.test.js b/test/fix/703.test.js index 3792ce60..f5470505 100644 --- a/test/fix/703.test.js +++ b/test/fix/703.test.js @@ -204,7 +204,7 @@ describe('#703 the names of affected files', () => { }); expect(result.out).toBe('a.file'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); } finally { fs.chmodSync('locked', 0o755); } @@ -223,7 +223,7 @@ describe('#703 the names of affected files', () => { nl: true, }); expect(result.out).toBe('a.file\n'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); } finally { fs.chmodSync('locked', 0o755); } diff --git a/test/fix/705.test.js b/test/fix/705.test.js index cfe7bed3..fe3a43a3 100644 --- a/test/fix/705.test.js +++ b/test/fix/705.test.js @@ -81,7 +81,7 @@ describe('#705 the engine returns a promise that resolves when all files are don const result = await rr('o', 'x', {includeGlob: ['*.file'], ...flags}); expect(result.err).toContain('ENAMETOOLONG'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(content(['a.file', tooLong, 'c.file'])).toEqual(['fxx', 'foo', 'fxx']); expect(fs.readdirSync('.')).toHaveLength(3); }); @@ -90,7 +90,7 @@ describe('#705 the engine returns a promise that resolves when all files are don fs.writeFileSync(tooLong, 'foo'); const result = await rr('o', 'x', {includeGlob: ['*.file'], halt: true, ...flags}); expect(result.err).toContain('ENAMETOOLONG'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); }); diff --git a/test/fix/708.test.js b/test/fix/708.test.js index f1ddf837..a613de2d 100644 --- a/test/fix/708.test.js +++ b/test/fix/708.test.js @@ -79,7 +79,7 @@ describe('#708 the number of files in work at the same time is limited', () => { expect(result.out).toBe('my.file\n'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('my.file', 'utf8')).toBe('fxx'); }); }); diff --git a/test/fix/711.test.js b/test/fix/711.test.js index d847e0b3..fa8bd22d 100644 --- a/test/fix/711.test.js +++ b/test/fix/711.test.js @@ -83,7 +83,7 @@ describe('#711 --glob-pipe', () => { ])('can not be used with %s, as the piped data can only be one thing', async (name, flags) => { fs.writeFileSync('a.file', 'foo'); const result = await rr('o', 'x', {pipedData: 'a.file', globPipe: true, ...flags}); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); expect(result.err).toContain('--glob-pipe can not be used with'); expect(content('a.file')).toBe('foo'); }); diff --git a/test/fix/720.test.js b/test/fix/720.test.js index e00e78c5..bda2f839 100644 --- a/test/fix/720.test.js +++ b/test/fix/720.test.js @@ -15,7 +15,7 @@ describe('#720 every error starts with ❌', () => { expect(result.err).toStartWith(' ❌ '); expect(result.err).toContain('EACCES'); expect(result.err).toEndWith('\n\n'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); }); test('an error that stops the run', async () => { diff --git a/test/fix/728.test.js b/test/fix/728.test.js index 4bfb7289..ea429d62 100644 --- a/test/fix/728.test.js +++ b/test/fix/728.test.js @@ -10,7 +10,7 @@ const flows = [ ['--void-async', {voidAsync: true}], ]; -describe('#728 the exit code is 1 when a file gives an error', () => { +describe('#728 the exit code is not 0 when a file gives an error', () => { test.each(flows)('and the other files are still changed with %s', async (name, flags) => { fs.writeFileSync('a.file', 'foo'); fs.writeFileSync('b.file', 'foo'); @@ -18,7 +18,7 @@ describe('#728 the exit code is 1 when a file gives an error', () => { const result = await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], ...flags}); expect(result.out).toBe('a.file\n'); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('a.file', 'utf8')).toBe('fxx'); }); @@ -31,7 +31,7 @@ describe('#728 the exit code is 1 when a file gives an error', () => { const result = await rr('o', 'x', {includeGlob: ['locked/my.file']}); expect(result.out).toBe(''); expect(result.err).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); expect(fs.readFileSync('locked/my.file', 'utf8')).toBe('foo'); } finally { // The folder must be writable again so the test folder can be removed @@ -43,6 +43,6 @@ describe('#728 the exit code is 1 when a file gives an error', () => { fs.writeFileSync('a.file', 'foo'); fs.writeFileSync('b.file', 'foo'); fs.chmodSync('b.file', 0); - expect((await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], [flag]: true})).exit).toBe(1); + expect((await rr('o', 'x', {includeGlob: ['a.file', 'b.file'], [flag]: true})).exit).toBe(2); }); }); diff --git a/test/fix/742.test.js b/test/fix/742.test.js index 7320568f..fd346f15 100644 --- a/test/fix/742.test.js +++ b/test/fix/742.test.js @@ -96,7 +96,7 @@ describe('#742 a panic', () => { fs.writeFileSync('c.file', 'foo'); fs.chmodSync('b.file', 0); const config = {includeGlob: ['*.file'], halt: true}; - expect((await rr('o', 'x', config)).exit).toBe(1); + expect((await rr('o', 'x', config)).exit).toBe(3); fs.writeFileSync('a.file', 'foo'); const status = await rrStatus('o', 'x', config); expect(status.files).toEqual({searched: ['a.file', 'b.file'], updated: ['a.file']}); @@ -104,7 +104,7 @@ describe('#742 a panic', () => { }); test('is thrown when the run can not go on, with the error in the status', async () => { - expect(await rr('(', 'x', {pipedData: 'foo'})).toMatchObject({out: '', exit: 1}); + expect(await rr('(', 'x', {pipedData: 'foo'})).toMatchObject({out: '', exit: 3}); expect((await rrStatus('(', 'x', {pipedData: 'foo'})).errors).toHaveLength(1); }); }); @@ -213,18 +213,18 @@ describe('#742 the CLI', () => { } ); - test('sets the exit code to 1 when a file gives an error, and prints the status report', async () => { + test('sets the exit code to 2 when a file gives an error, and prints the status report', async () => { fs.writeFileSync('a.file', 'foo'); fs.chmodSync('a.file', 0); const result = await sh($`bun ${cli} o x a.file --status-compact --quiet-total`); expect(JSON.parse(result.out).errors[0]).toContain('EACCES'); - expect(result.exit).toBe(1); + expect(result.exit).toBe(2); }); test('ends the process on a panic, and still prints the status report', async () => { fs.writeFileSync('a.file', 'foo'); const result = await sh($`bun ${cli} '(' x a.file --status-compact --quiet-total`); expect(JSON.parse(result.out).errors).toHaveLength(1); - expect(result.exit).toBe(1); + expect(result.exit).toBe(3); }); }); diff --git a/test/fix/783.test.js b/test/fix/783.test.js new file mode 100644 index 00000000..de4abf5f --- /dev/null +++ b/test/fix/783.test.js @@ -0,0 +1,42 @@ +// https://github.com/mathiasrw/rexreplace/issues/783 +// The exit code was 1 for an error in a file, for a run that stopped and for wrong flags, so a script could not tell them apart. +import {describe, test, expect} from 'bun:test'; +import fs from 'fs'; +import {$} from 'bun'; +import {cli, sh} from '../helper.js'; + +describe('#783 the exit code tells what happened', () => { + test('0 when it is done, also with a warning', async () => { + fs.writeFileSync('my.file', 'foobar'); + expect((await sh($`bun ${cli} o x my.file`)).exit).toBe(0); + expect((await sh($`bun ${cli} o x nope.file`)).exit).toBe(0); + }); + + test('0 for the help that was asked for', async () => { + expect((await sh($`bun ${cli} -h`)).exit).toBe(0); + }); + + test('2 when a file gives an error and the run goes on', async () => { + fs.writeFileSync('a.file', 'foo'); + fs.writeFileSync('b.file', 'foo'); + fs.chmodSync('a.file', 0); + expect((await sh($`bun ${cli} o x a.file b.file`)).exit).toBe(2); + expect(fs.readFileSync('b.file', 'utf8')).toBe('fxx'); + }); + + test('3 when the run stops', async () => { + fs.writeFileSync('my.file', 'foobar'); + expect((await sh($`bun ${cli} '(' x my.file`)).exit).toBe(3); + expect((await sh($`bun ${cli} o x my.file --encoding nope`)).exit).toBe(3); + }); + + test('3 for wrong flags and too few arguments, with no stray line on stdout', async () => { + fs.writeFileSync('my.file', 'foobar'); + const result = await sh($`bun ${cli} o x my.file --nope`); + expect(result.exit).toBe(3); + expect(result.out).toBe(''); + expect(result.err).toEndWith('Unknown argument: nope\n'); + expect((await sh($`bun ${cli} o`)).exit).toBe(3); + expect(fs.readFileSync('my.file', 'utf8')).toBe('foobar'); + }); +}); diff --git a/test/helper.js b/test/helper.js index ed2ccfd9..2b932fb0 100644 --- a/test/helper.js +++ b/test/helper.js @@ -69,17 +69,17 @@ async function run(pattern, replacement, config) { }; alias(options); status = await engine(options); - // Like the CLI, an error gives the exit code 1 when the run is done + // Like the CLI, an error gives the exit code 2 when the run is done if (status.errors.length) { - result.exit = 1; + result.exit = 2; } } catch (e) { - // Like the CLI, a panic stops the run with the exit code 1 + // Like the CLI, a panic stops the run with the exit code 3 if (!e?.panic) { throw e; } status = e.status; - result.exit = 1; + result.exit = 3; } finally { spies.forEach(spy => spy.mockRestore()); }