Skip to content

Commit 05c25ba

Browse files
feat: parse error messages to suggest correct flag value usage
1 parent b6df968 commit 05c25ba

File tree

2 files changed

+104
-0
lines changed

2 files changed

+104
-0
lines changed

src/sfCommand.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,30 @@ export abstract class SfCommand<T> extends Command {
376376
const sfCommandError = SfCommandError.from(error, this.statics.name, this.warnings);
377377
process.exitCode = sfCommandError.exitCode;
378378

379+
// no var args (strict = true || undefined), and unexpected arguments when parsing
380+
if (
381+
this.statics.strict !== false &&
382+
sfCommandError.exitCode === 2 &&
383+
error.message.includes('Unexpected argument')
384+
) {
385+
// @ts-expect-error error's causes aren't typed, this is what's returned from flag parsing errors
386+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
387+
const output =
388+
(sfCommandError.cause?.parse?.output?.raw as Array<{ flag: string; input: string; type: 'flag' | 'arg' }>) ??
389+
[];
390+
391+
// find the extra arguments causing issues
392+
const extras = output
393+
.filter((f) => f.type === 'arg')
394+
.flatMap((f) => f.input)
395+
.join(' ');
396+
// find the flag before the 'args' block that's valid, to append the args with its value as a suggestion
397+
const target = output.find((flag, index) => flag.type === 'flag' && output[index + 1]?.type === 'arg');
398+
399+
sfCommandError.actions ??= [];
400+
sfCommandError.actions.push(`--${target?.flag} "${target?.input} ${extras}"`);
401+
}
402+
379403
if (this.jsonEnabled()) {
380404
this.logJson(sfCommandError.toJson());
381405
} else {

test/unit/sfCommand.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ class NonJsonCommand extends SfCommand<void> {
110110
}
111111
}
112112

113+
class SuggestionCommand extends SfCommand<void> {
114+
public static enableJsonFlag = false;
115+
public static readonly flags = {
116+
first: Flags.string({
117+
default: 'My first flag',
118+
required: true,
119+
}),
120+
second: Flags.string({
121+
default: 'My second',
122+
required: true,
123+
}),
124+
};
125+
public async run(): Promise<void> {
126+
await this.parse(SuggestionCommand);
127+
}
128+
}
129+
113130
describe('jsonEnabled', () => {
114131
afterEach(() => {
115132
delete process.env.SF_CONTENT_TYPE;
@@ -375,6 +392,69 @@ describe('error standardization', () => {
375392
}
376393
});
377394

395+
it('should log correct suggestion when user doesnt wrap with quotes', async () => {
396+
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
397+
try {
398+
await SuggestionCommand.run(['--first', 'my', 'alias', 'with', 'spaces', '--second', 'my second value']);
399+
expect(false, 'error should have been thrown').to.be.true;
400+
} catch (e: unknown) {
401+
expect(e).to.be.instanceOf(SfCommandError);
402+
const err = e as SfCommand.Error;
403+
404+
// Ensure the error was logged to the console
405+
expect(logToStderrStub.callCount).to.equal(1);
406+
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);
407+
408+
// Ensure the error has expected properties
409+
expect(err).to.have.property('actions');
410+
expect(err.actions).to.deep.equal(['--first "my alias with spaces"']);
411+
expect(err).to.have.property('exitCode', 2);
412+
expect(err).to.have.property('context', 'SuggestionCommand');
413+
expect(err).to.have.property('data', undefined);
414+
expect(err).to.have.property('cause');
415+
expect(err).to.have.property('code', '2');
416+
expect(err).to.have.property('status', 2);
417+
expect(err).to.have.property('stack').and.be.ok;
418+
expect(err).to.have.property('skipOclifErrorHandling', true);
419+
expect(err).to.have.deep.property('oclif', { exit: 2 });
420+
421+
// Ensure a sfCommandError event was emitted with the expected data
422+
expect(sfCommandErrorData[0]).to.equal(err);
423+
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
424+
}
425+
});
426+
it('should log correct suggestion when user doesnt wrap with quotes without flag order', async () => {
427+
const logToStderrStub = $$.SANDBOX.stub(SfCommand.prototype, 'logToStderr');
428+
try {
429+
await SuggestionCommand.run(['--second', 'my second value', '--first', 'my', 'alias', 'with', 'spaces']);
430+
expect(false, 'error should have been thrown').to.be.true;
431+
} catch (e: unknown) {
432+
expect(e).to.be.instanceOf(SfCommandError);
433+
const err = e as SfCommand.Error;
434+
435+
// Ensure the error was logged to the console
436+
expect(logToStderrStub.callCount).to.equal(1);
437+
expect(logToStderrStub.firstCall.firstArg).to.contain(err.message);
438+
439+
// Ensure the error has expected properties
440+
expect(err).to.have.property('actions');
441+
expect(err.actions).to.deep.equal(['--first "my alias with spaces"']);
442+
expect(err).to.have.property('exitCode', 2);
443+
expect(err).to.have.property('context', 'SuggestionCommand');
444+
expect(err).to.have.property('data', undefined);
445+
expect(err).to.have.property('cause');
446+
expect(err).to.have.property('code', '2');
447+
expect(err).to.have.property('status', 2);
448+
expect(err).to.have.property('stack').and.be.ok;
449+
expect(err).to.have.property('skipOclifErrorHandling', true);
450+
expect(err).to.have.deep.property('oclif', { exit: 2 });
451+
452+
// Ensure a sfCommandError event was emitted with the expected data
453+
expect(sfCommandErrorData[0]).to.equal(err);
454+
expect(sfCommandErrorData[1]).to.equal('suggestioncommand');
455+
}
456+
});
457+
378458
it('should log correct error when command throws an SfError --json', async () => {
379459
const logJsonStub = $$.SANDBOX.stub(SfCommand.prototype, 'logJson');
380460
try {

0 commit comments

Comments
 (0)