Skip to content

Commit 706c4d6

Browse files
gustavohenkepaescuj
andauthoredMay 15, 2022
Add command-{name|index} and !command-{name|index} condition to --success (#318)
Co-authored-by: Pascal Jufer <pascal-jufer@bluewin.ch>
1 parent c5231ea commit 706c4d6

File tree

4 files changed

+174
-24
lines changed

4 files changed

+174
-24
lines changed
 

‎README.md

+12-6
Original file line numberDiff line numberDiff line change
@@ -146,14 +146,20 @@ General
146146
--name-separator The character to split <names> on. Example usage:
147147
concurrently -n "styles|scripts|server"
148148
--name-separator "|" [default: ","]
149-
-s, --success Return exit code of zero or one based on the
150-
success or failure of the "first" child to
151-
terminate, the "last child", or succeed only if
152-
"all" child processes succeed.
153-
[choices: "first", "last", "all"] [default: "all"]
149+
-s, --success Which command(s) must exit with code 0 in order
150+
for concurrently exit with code 0 too. Options
151+
are:
152+
- "first" for the first command to exit;
153+
- "last" for the last command to exit;
154+
- "all" for all commands;
155+
- "command-{name}"/"command-{index}" for the
156+
commands with that name or index;
157+
- "!command-{name}"/"!command-{index}" for all
158+
commands but the ones with that name or index.
159+
[default: "all"]
154160
-r, --raw Output only raw output of processes, disables
155161
prettifying and concurrently coloring. [boolean]
156-
--no-color Disables colors from logging. [boolean]
162+
--no-color Disables colors from logging [boolean]
157163
--hide Comma-separated list of processes to hide the
158164
output.
159165
The processes can be identified by their name or

‎bin/concurrently.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,15 @@ const args = yargs(argsBeforeSep)
4848
'success': {
4949
alias: 's',
5050
describe:
51-
'Return exit code of zero or one based on the success or failure ' +
52-
'of the "first" child to terminate, the "last child", or succeed ' +
53-
'only if "all" child processes succeed.',
54-
choices: ['first', 'last', 'all'] as const,
51+
'Which command(s) must exit with code 0 in order for concurrently exit with ' +
52+
'code 0 too. Options are:\n' +
53+
'- "first" for the first command to exit;\n' +
54+
'- "last" for the last command to exit;\n' +
55+
'- "all" for all commands;\n' +
56+
// Note: not a typo. Multiple commands can have the same name.
57+
'- "command-{name}"/"command-{index}" for the commands with that name or index;\n' +
58+
'- "!command-{name}"/"!command-{index}" for all commands but the ones with that ' +
59+
'name or index.\n',
5560
default: defaults.success,
5661
},
5762
'raw': {

‎src/completion-listener.spec.ts

+123-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { TestScheduler } from 'rxjs/testing';
2+
import { CloseEvent } from './command';
23
import { CompletionListener, SuccessCondition } from './completion-listener';
34
import { createFakeCloseEvent, FakeCommand } from './fixtures/fake-command';
45

56
let commands: FakeCommand[];
67
let scheduler: TestScheduler;
78
beforeEach(() => {
8-
commands = [new FakeCommand('foo'), new FakeCommand('bar')];
9+
commands = [
10+
new FakeCommand('foo', 'echo', 0),
11+
new FakeCommand('bar', 'echo', 1),
12+
new FakeCommand('baz', 'echo', 2),
13+
];
914
scheduler = new TestScheduler(() => true);
1015
});
1116

@@ -15,12 +20,18 @@ const createController = (successCondition?: SuccessCondition) =>
1520
scheduler,
1621
});
1722

23+
const emitFakeCloseEvent = (
24+
command: FakeCommand,
25+
event?: Partial<CloseEvent>,
26+
) => command.close.next(createFakeCloseEvent({ ...event, command, index: command.index }));
27+
1828
describe('with default success condition set', () => {
1929
it('succeeds if all processes exited with code 0', () => {
2030
const result = createController().listen(commands);
2131

2232
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
2333
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
34+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
2435

2536
scheduler.flush();
2637

@@ -32,6 +43,7 @@ describe('with default success condition set', () => {
3243

3344
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
3445
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
46+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
3547

3648
scheduler.flush();
3749

@@ -45,6 +57,7 @@ describe('with success condition set to first', () => {
4557

4658
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
4759
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
60+
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
4861

4962
scheduler.flush();
5063

@@ -56,6 +69,7 @@ describe('with success condition set to first', () => {
5669

5770
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
5871
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
72+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
5973

6074
scheduler.flush();
6175

@@ -69,6 +83,7 @@ describe('with success condition set to last', () => {
6983

7084
commands[1].close.next(createFakeCloseEvent({ exitCode: 1 }));
7185
commands[0].close.next(createFakeCloseEvent({ exitCode: 0 }));
86+
commands[2].close.next(createFakeCloseEvent({ exitCode: 0 }));
7287

7388
scheduler.flush();
7489

@@ -80,10 +95,117 @@ describe('with success condition set to last', () => {
8095

8196
commands[1].close.next(createFakeCloseEvent({ exitCode: 0 }));
8297
commands[0].close.next(createFakeCloseEvent({ exitCode: 1 }));
98+
commands[2].close.next(createFakeCloseEvent({ exitCode: 1 }));
8399

84100
scheduler.flush();
85101

86102
return expect(result).rejects.toEqual(expect.anything());
87103
});
88104

89105
});
106+
107+
describe.each([
108+
// Use the middle command for both cases to make it more difficult to make a mess up
109+
// in the implementation cause false passes.
110+
['command-bar' as const, 'bar'],
111+
['command-1' as const, 1],
112+
])('with success condition set to %s', (condition, nameOrIndex) => {
113+
it(`succeeds if command ${nameOrIndex} exits with code 0`, () => {
114+
const result = createController(condition).listen(commands);
115+
116+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
117+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
118+
emitFakeCloseEvent(commands[2], { exitCode: 1 });
119+
120+
scheduler.flush();
121+
122+
return expect(result).resolves.toEqual(expect.anything());
123+
});
124+
125+
it(`succeeds if all commands ${nameOrIndex} exit with code 0`, () => {
126+
commands = [commands[0], commands[1], commands[1]];
127+
const result = createController(condition).listen(commands);
128+
129+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
130+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
131+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
132+
133+
scheduler.flush();
134+
135+
return expect(result).resolves.toEqual(expect.anything());
136+
});
137+
138+
it(`fails if command ${nameOrIndex} exits with non-0 code`, () => {
139+
const result = createController(condition).listen(commands);
140+
141+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
142+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
143+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
144+
145+
scheduler.flush();
146+
147+
return expect(result).rejects.toEqual(expect.anything());
148+
});
149+
150+
it(`fails if some commands ${nameOrIndex} exit with non-0 code`, () => {
151+
commands = [commands[0], commands[1], commands[1]];
152+
const result = createController(condition).listen(commands);
153+
154+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
155+
emitFakeCloseEvent(commands[1], { exitCode: 0 });
156+
emitFakeCloseEvent(commands[2], { exitCode: 1 });
157+
158+
scheduler.flush();
159+
160+
return expect(result).resolves.toEqual(expect.anything());
161+
});
162+
163+
it(`fails if command ${nameOrIndex} doesn't exist`, () => {
164+
const result = createController(condition).listen([commands[0]]);
165+
166+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
167+
scheduler.flush();
168+
169+
return expect(result).rejects.toEqual(expect.anything());
170+
});
171+
});
172+
173+
describe.each([
174+
// Use the middle command for both cases to make it more difficult to make a mess up
175+
// in the implementation cause false passes.
176+
['!command-bar' as const, 'bar'],
177+
['!command-1' as const, 1],
178+
])('with success condition set to %s', (condition, nameOrIndex) => {
179+
it(`succeeds if all commands but ${nameOrIndex} exit with code 0`, () => {
180+
const result = createController(condition).listen(commands);
181+
182+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
183+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
184+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
185+
186+
scheduler.flush();
187+
188+
return expect(result).resolves.toEqual(expect.anything());
189+
});
190+
191+
it(`fails if any commands but ${nameOrIndex} exit with non-0 code`, () => {
192+
const result = createController(condition).listen(commands);
193+
194+
emitFakeCloseEvent(commands[0], { exitCode: 1 });
195+
emitFakeCloseEvent(commands[1], { exitCode: 1 });
196+
emitFakeCloseEvent(commands[2], { exitCode: 0 });
197+
198+
scheduler.flush();
199+
200+
return expect(result).rejects.toEqual(expect.anything());
201+
});
202+
203+
it(`succeeds if command ${nameOrIndex} doesn't exist`, () => {
204+
const result = createController(condition).listen([commands[0]]);
205+
206+
emitFakeCloseEvent(commands[0], { exitCode: 0 });
207+
scheduler.flush();
208+
209+
return expect(result).resolves.toEqual(expect.anything());
210+
});
211+
});

‎src/completion-listener.ts

+30-13
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { CloseEvent, Command } from './command';
88
* - `first`: only the first specified command;
99
* - `last`: only the last specified command;
1010
* - `all`: all commands.
11+
* - `command-{name|index}`: only the commands with the specified names or index.
12+
* - `!command-{name|index}`: all commands but the ones with the specified names or index.
1113
*/
12-
export type SuccessCondition = 'first' | 'last' | 'all';
14+
export type SuccessCondition = 'first' | 'last' | 'all' | `command-${string|number}` | `!command-${string|number}`;
1315

1416
/**
1517
* Provides logic to determine whether lists of commands ran successfully.
@@ -36,19 +38,34 @@ export class CompletionListener {
3638
this.scheduler = scheduler;
3739
}
3840

39-
private isSuccess(exitCodes: (string | number)[]) {
40-
switch (this.successCondition) {
41-
/* eslint-disable indent */
42-
case 'first':
43-
return exitCodes[0] === 0;
44-
45-
case 'last':
46-
return exitCodes[exitCodes.length - 1] === 0;
41+
private isSuccess(events: CloseEvent[]) {
42+
if (this.successCondition === 'first') {
43+
return events[0].exitCode === 0;
44+
} else if (this.successCondition === 'last') {
45+
return events[events.length - 1].exitCode === 0;
46+
} else if (!/^!?command-.+$/.test(this.successCondition)) {
47+
// If not a `command-` syntax, then it's an 'all' condition or it's treated as such.
48+
return events.every(({ exitCode }) => exitCode === 0);
49+
}
4750

48-
default:
49-
return exitCodes.every(exitCode => exitCode === 0);
50-
/* eslint-enable indent */
51+
// Check `command-` syntax condition.
52+
// Note that a command's `name` is not necessarily unique,
53+
// in which case all of them must meet the success condition.
54+
const [, nameOrIndex] = this.successCondition.split('-');
55+
const targetCommandsEvents = events.filter(({ command, index }) => (
56+
command.name === nameOrIndex
57+
|| index === Number(nameOrIndex)
58+
));
59+
if (this.successCondition.startsWith('!')) {
60+
// All commands except the specified ones must exit succesfully
61+
return events.every((event) => (
62+
targetCommandsEvents.includes(event)
63+
|| event.exitCode === 0
64+
));
5165
}
66+
// Only the specified commands must exit succesfully
67+
return targetCommandsEvents.length > 0
68+
&& targetCommandsEvents.every(event => event.exitCode === 0);
5269
}
5370

5471
/**
@@ -62,7 +79,7 @@ export class CompletionListener {
6279
.pipe(
6380
bufferCount(closeStreams.length),
6481
switchMap(exitInfos =>
65-
this.isSuccess(exitInfos.map(({ exitCode }) => exitCode))
82+
this.isSuccess(exitInfos)
6683
? Rx.of(exitInfos, this.scheduler)
6784
: Rx.throwError(exitInfos, this.scheduler),
6885
),

0 commit comments

Comments
 (0)
Please sign in to comment.