Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Add new --matrix option to multiply commands
  • Loading branch information
cdrini committed Jan 16, 2025
commit f656f5d4c06690f7441a2b8a224241218204d5b0
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[][];
Comment thread
cdrini marked this conversation as resolved.
Outdated

constructor(private readonly matrices: readonly string[][]) {
Comment thread
cdrini marked this conversation as resolved.
Outdated
this.matrices = matrices;
Comment thread
cdrini marked this conversation as resolved.
Outdated
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 thread
cdrini marked this conversation as resolved.
Outdated
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) {
Comment thread
cdrini marked this conversation as resolved.
Outdated
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.
Comment thread
cdrini marked this conversation as resolved.
Outdated
*/
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