Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new --matrix option to multiply commands #526

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
8 changes: 8 additions & 0 deletions bin/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,13 @@ const program = yargs(hideBin(process.argv))
type: 'boolean',
default: defaults.timings,
},
matrix: {
describe:
'Run many commands as a matrix using space-separated parameters. ' +
'E.g. concurrently --matrix "a b c" --matrix "1 2 3" "echo {1}{2}"',
type: 'string',
array: true,
},
'passthrough-arguments': {
alias: 'P',
describe:
Expand Down Expand Up @@ -253,6 +260,7 @@ concurrently(
timestampFormat: args.timestampFormat,
timings: args.timings,
teardown: args.teardown,
matrices: args.matrix?.map((matrix) => matrix.split(' ')),
additionalArguments: args.passthroughArguments ? additionalArguments : undefined,
},
).result.then(
Expand Down
116 changes: 116 additions & 0 deletions src/command-parser/expand-matrices.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { CommandInfo } from '../command';
import { combinations, ExpandMatrices } from './expand-matrices';

const createCommandInfo = (command: string): CommandInfo => ({
command,
name: '',
});

describe('ExpandMatrices', () => {
it('should replace placeholders with matrix values', () => {
const matrices = [
['a', 'b'],
['1', '2'],
];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo {1} and {2}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([
{ command: 'echo a and 1', name: '' },
{ command: 'echo a and 2', name: '' },
{ command: 'echo b and 1', name: '' },
{ command: 'echo b and 2', name: '' },
]);
});

it('should handle escaped placeholders', () => {
const matrices = [['a', 'b']];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo \\{1} and {1}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([
{ command: 'echo {1} and a', name: '' },
{ command: 'echo {1} and b', name: '' },
]);
});

it('should replace placeholders with empty string if index is out of bounds', () => {
const matrices = [['a']];
const expandMatrices = new ExpandMatrices(matrices);
const commandInfo = createCommandInfo('echo {2}');

const result = expandMatrices.parse(commandInfo);

expect(result).toEqual([{ command: 'echo ', name: '' }]);
});
});

describe('combinations', () => {
it('should return all possible combinations of the given dimensions', () => {
const dimensions = [
['a', 'b'],
['1', '2'],
];

const result = combinations(dimensions);

expect(result).toEqual([
['a', '1'],
['a', '2'],
['b', '1'],
['b', '2'],
]);
});

it('should handle single dimension', () => {
const dimensions = [['a', 'b']];

const result = combinations(dimensions);

expect(result).toEqual([['a'], ['b']]);
});

it('should handle empty dimensions', () => {
const dimensions: string[][] = [];

const result = combinations(dimensions);

expect(result).toEqual([[]]);
});

it('should handle dimensions with empty arrays', () => {
const dimensions = [['a', 'b'], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with multiple empty arrays', () => {
const dimensions = [[], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with some empty arrays', () => {
const dimensions = [['a', 'b'], [], ['x', 'y']];

const result = combinations(dimensions);

expect(result).toEqual([]);
});

it('should handle dimensions with all empty arrays', () => {
const dimensions = [[], [], []];

const result = combinations(dimensions);

expect(result).toEqual([]);
});
});
62 changes: 62 additions & 0 deletions src/command-parser/expand-matrices.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { quote } from 'shell-quote';

import { CommandInfo } from '../command';
import { CommandParser } from './command-parser';

/**
* Replace placeholders with new commands for each combination of matrices.
*/
export class ExpandMatrices implements CommandParser {
private _bindings: string[][];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I assume the _ is to signal that the field is private - if there isn't another field with the same name (such as a getter), let's not add that prefix.
It can also be made readonly:

Suggested change
private _bindings: string[][];
private readonly bindings: string[][];


constructor(private readonly matrices: readonly string[][]) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't seem like it's read outside of the constructor, so for now let's not make it a field

Suggested change
constructor(private readonly matrices: readonly string[][]) {
constructor(matrices: readonly string[][]) {

this.matrices = matrices;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line isn't necessary, as the private in the constructor param list does the same job.

Suggested change
this.matrices = matrices;

this._bindings = combinations(matrices);
}

parse(commandInfo: CommandInfo) {
return this._bindings.map((binding) => this.replacePlaceholders(commandInfo, binding));
}

private replacePlaceholders(commandInfo: CommandInfo, binding: string[]): CommandInfo {
const command = commandInfo.command.replace(
/\\?\{([0-9]*)?\}/g,
(match, placeholderTarget) => {
// Don't replace the placeholder if it is escaped by a backslash.
if (match.startsWith('\\')) {
return match.slice(1);
}

let index = 0;
if (placeholderTarget && !isNaN(placeholderTarget)) {
Comment on lines +30 to +31
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should an error be thrown if the placeholder isn't a number?
Might convert a "wtf is concurrently doing" to a "oh, my bad, I got it wrong" 😄

index = parseInt(placeholderTarget, 10) - 1;
}

// Replace numeric placeholder if value exists in additional arguments.
if (index < binding.length) {
return quote([binding[index]]);
}

// Replace placeholder with empty string
// if value doesn't exist in additional arguments.
return '';
},
);

return { ...commandInfo, command };
}
}

/**
* Returns all possible combinations of the given dimensions.
*/
export function combinations(dimensions: readonly string[][]): string[][] {
return dimensions.reduce(
(acc, dimension) => {
return acc.flatMap((accItem) =>
dimension.map((dimensionItem) => accItem.concat(dimensionItem)),
);
},
[[]] as string[][],
);
}
10 changes: 10 additions & 0 deletions src/concurrently.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from './command';
import { CommandParser } from './command-parser/command-parser';
import { ExpandArguments } from './command-parser/expand-arguments';
import { ExpandMatrices } from './command-parser/expand-matrices';
import { ExpandShortcut } from './command-parser/expand-shortcut';
import { ExpandWildcard } from './command-parser/expand-wildcard';
import { StripQuotes } from './command-parser/strip-quotes';
Expand Down Expand Up @@ -147,6 +148,11 @@ export type ConcurrentlyOptions = {
*/
killSignal?: string;

/**
* Specify variables which will spawn multiple commands.
*/
matrices?: readonly string[][];

/**
* List of additional arguments passed that will get replaced in each command.
* If not defined, no argument replacing will happen.
Expand Down Expand Up @@ -179,6 +185,10 @@ export function concurrently(
new ExpandWildcard(),
];

if (options.matrices?.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like matrices have preference over passthrough arguments using the same {number} syntax. Was it intended?

It needs to be called out in the help and in the docs.

commandParsers.push(new ExpandMatrices(options.matrices));
}

if (options.additionalArguments) {
commandParsers.push(new ExpandArguments(options.additionalArguments));
}
Expand Down
6 changes: 6 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,11 @@ export type ConcurrentlyOptions = Omit<BaseConcurrentlyOptions, 'abortSignal' |
* If not defined, no argument replacing will happen.
*/
additionalArguments?: string[];

/**
* This command should be run multiple times, for each of the provided matrices.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you please update this comment? It looks like it's assuming some context from elsewhere.

*/
matrices?: readonly string[][];
};

export function concurrently(
Expand Down Expand Up @@ -171,6 +176,7 @@ export function concurrently(
new Teardown({ logger, spawn: options.spawn, commands: options.teardown || [] }),
],
prefixColors: options.prefixColors || [],
matrices: options.matrices,
additionalArguments: options.additionalArguments,
});
}
Expand Down
Loading