From 304862978b111a8e752d207dc92d2798cef41cf6 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Tue, 15 Feb 2022 11:44:36 +0100 Subject: [PATCH 01/15] Support passthrough of arguments via placeholders --- README.md | 15 ++- bin/concurrently.ts | 12 +- bin/epilogue.ts | 12 ++ package-lock.json | 126 ++++++++++++++------ package.json | 3 +- src/command-parser/expand-arguments.spec.ts | 56 +++++++++ src/command-parser/expand-arguments.ts | 29 +++++ src/command.ts | 11 +- src/concurrently.spec.ts | 16 +++ src/concurrently.ts | 4 + 10 files changed, 244 insertions(+), 40 deletions(-) create mode 100644 src/command-parser/expand-arguments.spec.ts create mode 100644 src/command-parser/expand-arguments.ts diff --git a/README.md b/README.md index 15e42a64..0cdde051 100644 --- a/README.md +++ b/README.md @@ -258,6 +258,18 @@ Examples: $ concurrently "npm:watch-*" + - Passthrough some additional arguments via '{}' placeholder + + $ concurrently "echo {1}" -- foo + + - Passthrough all additional arguments via '{@}' placeholder + + $ concurrently "npm:dev-* -- {@}" -- --watch --noEmit + + - Passthrough all additional arguments combined via '{*}' placeholder + + $ concurrently "npm:dev-* -- {*}" -- --watch --noEmit + For more details, visit https://github.com/open-cli-tools/concurrently ``` @@ -267,7 +279,7 @@ concurrently can be used programmatically by using the API documented below: ### `concurrently(commands[, options])` - `commands`: an array of either strings (containing the commands to run) or objects - with the shape `{ command, name, prefixColor, env, cwd }`. + with the shape `{ command, name, prefixColor, env, cwd, passthroughArgs }`. - `options` (optional): an object containing any of the below: - `cwd`: the working directory to be used by all commands. Can be overriden per command. @@ -332,6 +344,7 @@ It has the following properties: - `name`: the name of the command; defaults to an empty string. - `cwd`: the current working directory of the command. - `env`: an object with all the environment variables that the command will be spawned with. +- `passthroughArgs`: list of additional arguments which can be used in command via placeholders. - `killed`: whether the command has been killed. - `exited`: whether the command exited yet. - `pid`: the command's process ID. diff --git a/bin/concurrently.ts b/bin/concurrently.ts index f7dd870c..5715e360 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -1,10 +1,16 @@ #!/usr/bin/env node import yargs from 'yargs'; +import { hideBin } from 'yargs/helpers'; import * as defaults from '../src/defaults'; import concurrently from '../src/index'; import { epilogue } from './epilogue'; -const args = yargs +// Clean-up arguments (yargs expects only the arguments after the program name) +const cleanArgs = hideBin(process.argv); +// Find argument separator (double dash) +const argsSepIdx = cleanArgs.findIndex((arg) => arg === '--'); +// If separator has been found, only pass arguments before it to yargs, otherwise pass all arguments +const args = yargs(argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs) .usage('$0 [options] ') .help('h') .alias('h', 'help') @@ -160,13 +166,15 @@ const args = yargs .group(['k', 'kill-others-on-fail'], 'Killing other processes') .group(['restart-tries', 'restart-after'], 'Restarting') .epilogue(epilogue) - .argv; + .parseSync(); const names = (args.names || '').split(args['name-separator']); concurrently(args._.map((command, index) => ({ command: String(command), name: names[index], + // Pass arguments after separator, if there are any + passthroughArgs: argsSepIdx >= 0 ? cleanArgs.slice(argsSepIdx + 1) : [], })), { handleInput: args['handle-input'], defaultInputTarget: args['default-input-target'], diff --git a/bin/epilogue.ts b/bin/epilogue.ts index 1fce3805..b508b8a7 100644 --- a/bin/epilogue.ts +++ b/bin/epilogue.ts @@ -54,6 +54,18 @@ const examples = [ description: 'Exclude patterns so that between "lint:js" and "lint:fix:js", only "lint:js" is ran', example: '$ $0 "npm:*(!fix)"', }, + { + description: 'Passthrough some additional arguments via \'{}\' placeholder', + example: '$ $0 "echo {1}" -- foo', + }, + { + description: 'Passthrough all additional arguments via \'{@}\' placeholder', + example: '$ $0 "npm:dev-* -- {@}" -- --watch --noEmit', + }, + { + description: 'Passthrough all additional arguments combined via \'{*}\' placeholder', + example: '$ $0 "npm:dev-* -- {*}" -- --watch --noEmit', + }, ]; export const epilogue = ` diff --git a/package-lock.json b/package-lock.json index 59311a5d..fcd17fff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", - "yargs": "^16.2.0" + "yargs": "^17.3.1" }, "bin": { "concurrently": "dist/bin/concurrently.js" @@ -26,6 +26,7 @@ "@types/lodash": "^4.14.178", "@types/node": "^17.0.0", "@types/supports-color": "^8.1.1", + "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "^5.8.1", "@typescript-eslint/parser": "^5.8.1", "coveralls": "^3.1.0", @@ -754,6 +755,15 @@ "node": ">= 10.14.2" } }, + "node_modules/@jest/types/node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1043,9 +1053,9 @@ "dev": true }, "node_modules/@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.8.tgz", + "integrity": "sha512-wDeUwiUmem9FzsyysEwRukaEdDNcwbROvQ9QGRKaLI6t+IltNzbn4/i4asmB10auvZGQCzSQ6t0GSczEThlUXw==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -4523,6 +4533,15 @@ "node": ">= 10.14.2" } }, + "node_modules/jest-runtime/node_modules/@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, "node_modules/jest-runtime/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -6745,24 +6764,24 @@ } }, "node_modules/string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" }, "engines": { "node": ">=8" } }, "node_modules/strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dependencies": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" @@ -7587,30 +7606,39 @@ "dev": true }, "node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" }, "engines": { - "node": ">=10" + "node": ">=12" } }, "node_modules/yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, "engines": { "node": ">=10" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==", + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -8261,6 +8289,17 @@ "@types/node": "*", "@types/yargs": "^15.0.0", "chalk": "^4.0.0" + }, + "dependencies": { + "@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + } } }, "@nodelib/fs.scandir": { @@ -8524,9 +8563,9 @@ "dev": true }, "@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "version": "17.0.8", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.8.tgz", + "integrity": "sha512-wDeUwiUmem9FzsyysEwRukaEdDNcwbROvQ9QGRKaLI6t+IltNzbn4/i4asmB10auvZGQCzSQ6t0GSczEThlUXw==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -11246,6 +11285,15 @@ "yargs": "^15.4.1" }, "dependencies": { + "@types/yargs": { + "version": "15.0.14", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", + "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -13028,21 +13076,21 @@ } }, "string-width": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.0.tgz", - "integrity": "sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "requires": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" + "strip-ansi": "^6.0.1" } }, "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "requires": { - "ansi-regex": "^5.0.0" + "ansi-regex": "^5.0.1" } }, "strip-bom": { @@ -13675,23 +13723,31 @@ "dev": true }, "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.3.1.tgz", + "integrity": "sha512-WUANQeVgjLbNsEmGk20f+nlHgOqzRFpiGWVaBrYGYIGANIIu3lWjoyi0fNlFmJkvfhCZ6BXINe7/W2O2bV4iaA==", "requires": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "string-width": "^4.2.0", + "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" + "yargs-parser": "^21.0.0" + }, + "dependencies": { + "yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-z9kApYUOCwoeZ78rfRYYWdiU/iNL6mwwYlkkZfJoyMR1xps+NEBX5X7XmRpxkZHhXJ6+Ey00IwKxBBSW9FIjyA==" + } } }, "yargs-parser": { "version": "20.2.9", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==" + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index 6a752b1d..39cf2e55 100644 --- a/package.json +++ b/package.json @@ -47,13 +47,14 @@ "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", - "yargs": "^16.2.0" + "yargs": "^17.3.1" }, "devDependencies": { "@types/jest": "^27.0.3", "@types/lodash": "^4.14.178", "@types/node": "^17.0.0", "@types/supports-color": "^8.1.1", + "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "^5.8.1", "@typescript-eslint/parser": "^5.8.1", "coveralls": "^3.1.0", diff --git a/src/command-parser/expand-arguments.spec.ts b/src/command-parser/expand-arguments.spec.ts new file mode 100644 index 00000000..efe67687 --- /dev/null +++ b/src/command-parser/expand-arguments.spec.ts @@ -0,0 +1,56 @@ +import { CommandInfo } from '../command'; +import { ExpandArguments } from './expand-arguments'; + +const parser = new ExpandArguments(); + +const createCommandInfo = (command: string, passthroughArgs: string[]): CommandInfo => ({ + command, + name: '', + passthroughArgs, +}); + +it('returns command as is when no placeholders', () => { + const commandInfo = createCommandInfo('echo foo', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' }); +}); + +it('single argument placeholder is replaced', () => { + const commandInfo = createCommandInfo('echo {1}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo\'' }); +}); + +it('multiple single argument placeholders are replaced', () => { + const commandInfo = createCommandInfo('echo {2} {1}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'bar\' \'foo\'' }); +}); + +it('empty replacement with single placeholder and no passthrough arguments', () => { + const commandInfo = createCommandInfo('echo {3}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); +}); + +it('empty replacement with all placeholder and no passthrough arguments', () => { + const commandInfo = createCommandInfo('echo {@}', []); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); +}); + +it('empty replacement with combined placeholder and no passthrough arguments', () => { + const commandInfo = createCommandInfo('echo {*}', []); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); +}); + +it('all arguments placeholder is replaced', () => { + const commandInfo = createCommandInfo('echo {@}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo\' \'bar\'' }); +}); + +it('combined arguments placeholder is replaced', () => { + const commandInfo = createCommandInfo('echo {*}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo bar\'' }); +}); + +it('escaped argument placeholders are not replaced', () => { + // Equals to single backslash on command line + const commandInfo = createCommandInfo('echo \\{1} \\{@} \\{*}', ['foo', 'bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo {1} {@} {*}' }); +}); diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts new file mode 100644 index 00000000..969bd3d3 --- /dev/null +++ b/src/command-parser/expand-arguments.ts @@ -0,0 +1,29 @@ +import { CommandInfo } from '../command'; +import { CommandParser } from './command-parser'; + +/** + * Replace placeholders with passthrough arguments. + */ +export class ExpandArguments implements CommandParser { + parse(commandInfo: CommandInfo) { + const command = commandInfo.command.replace(/\\?{([@\*]|\d+)\}/g, (match, p1) => { + if (match.startsWith('\\')) { + return match.substring(1); + } + if (!isNaN(p1) && p1 > 0 && commandInfo.passthroughArgs[p1-1]) { + return `'${commandInfo.passthroughArgs[p1-1]}'`; + } + if (p1 === '@') { + return commandInfo.passthroughArgs.map((arg) => `'${arg}'`).join(' '); + } + if (p1 === '*' && commandInfo.passthroughArgs.length > 0) { + return `'${commandInfo.passthroughArgs.join(' ')}'`; + } + return ''; + }); + + return Object.assign({}, commandInfo, { + command, + }); + } +}; diff --git a/src/command.ts b/src/command.ts index 2ee253db..a8f3969f 100644 --- a/src/command.ts +++ b/src/command.ts @@ -28,6 +28,11 @@ export interface CommandInfo { */ cwd?: string, prefixColor?: string, + + /** + * Additional arguments which can be used in command via placeholders. + */ + passthroughArgs?: string[] } export interface CloseEvent { @@ -95,6 +100,9 @@ export class Command implements CommandInfo { /** @inheritdoc */ readonly cwd?: string; + /** @inheritdoc */ + readonly passthroughArgs: string[]; + readonly close = new Rx.Subject(); readonly error = new Rx.Subject(); readonly stdout = new Rx.Subject(); @@ -112,7 +120,7 @@ export class Command implements CommandInfo { } constructor( - { index, name, command, prefixColor, env, cwd }: CommandInfo & { index: number }, + { index, name, command, prefixColor, env, cwd, passthroughArgs }: CommandInfo & { index: number }, spawnOpts: SpawnOptions, spawn: SpawnCommand, killProcess: KillProcess, @@ -123,6 +131,7 @@ export class Command implements CommandInfo { this.prefixColor = prefixColor; this.env = env; this.cwd = cwd; + this.passthroughArgs = passthroughArgs; this.killProcess = killProcess; this.spawn = spawn; this.spawnOpts = spawnOpts; diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index ec13d990..6ba350c5 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -187,6 +187,22 @@ it('uses overridden cwd option for each command if specified', () => { })); }); +it('argument placeholders are properly replaced', () => { + const passthroughArgs = ['foo', 'bar']; + create([ + { command: 'echo {1}', passthroughArgs }, + { command: 'echo {@}', passthroughArgs }, + { command: 'echo {*}', passthroughArgs }, + { command: 'echo \\{@}', passthroughArgs }, + ]); + + expect(spawn).toHaveBeenCalledTimes(4); + expect(spawn).toHaveBeenCalledWith('echo \'foo\'', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo \'foo\' \'bar\'', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo \'foo bar\'', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); +}); + it('runs onFinish hook after all commands run', async () => { const promise = create(['foo', 'bar'], { maxProcesses: 1 }); expect(spawn).toHaveBeenCalledTimes(1); diff --git a/src/concurrently.ts b/src/concurrently.ts index 29c97412..f36c4f38 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -5,6 +5,7 @@ import { Writable } from 'stream'; import treeKill from 'tree-kill'; import { CloseEvent, Command, CommandInfo, KillProcess, SpawnCommand } from './command'; import { CommandParser } from './command-parser/command-parser'; +import { ExpandArguments } from './command-parser/expand-arguments'; import { ExpandNpmShortcut } from './command-parser/expand-npm-shortcut'; import { ExpandNpmWildcard } from './command-parser/expand-npm-wildcard'; import { StripQuotes } from './command-parser/strip-quotes'; @@ -117,6 +118,7 @@ export function concurrently( new StripQuotes(), new ExpandNpmShortcut(), new ExpandNpmWildcard(), + new ExpandArguments(), ]; let lastColor = ''; @@ -184,6 +186,7 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: '', env: {}, cwd: '', + passthroughArgs: [], }; } @@ -192,6 +195,7 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: command.name || '', env: command.env || {}, cwd: command.cwd || '', + passthroughArgs: command.passthroughArgs || [], }, command.prefixColor ? { prefixColor: command.prefixColor, } : {}); From 73420d3ed7a108173781da8db81b022c2b09f87a Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Tue, 15 Feb 2022 11:50:13 +0100 Subject: [PATCH 02/15] Add comment for 'prefixColor' --- src/command.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/command.ts b/src/command.ts index a8f3969f..11c3394b 100644 --- a/src/command.ts +++ b/src/command.ts @@ -27,6 +27,10 @@ export interface CommandInfo { * The current working directory of the process when spawned. */ cwd?: string, + + /** + * Color to use on prefix of command. + */ prefixColor?: string, /** From 71286470f3ffd2e25a5d7fd9e88878392596dfec Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Tue, 15 Feb 2022 11:50:37 +0100 Subject: [PATCH 03/15] Replace deprecated 'substr' method by 'substring' --- src/command-parser/strip-quotes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-parser/strip-quotes.ts b/src/command-parser/strip-quotes.ts index b464a24d..ec709ef8 100644 --- a/src/command-parser/strip-quotes.ts +++ b/src/command-parser/strip-quotes.ts @@ -10,7 +10,7 @@ export class StripQuotes implements CommandParser { // Removes the quotes surrounding a command. if (/^"(.+?)"$/.test(command) || /^'(.+?)'$/.test(command)) { - command = command.substr(1, command.length - 2); + command = command.substring(1, command.length - 1); } return Object.assign({}, commandInfo, { command }); From 425fb1c3e1c9011ceac8aa82c008ab1399d16acc Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Tue, 8 Mar 2022 15:01:18 +0100 Subject: [PATCH 04/15] Introduce option 'passthrough-arguments' --- README.md | 74 +++++++++++-------- bin/concurrently.spec.ts | 23 +++++- bin/concurrently.ts | 78 +++++++++++++-------- bin/epilogue.ts | 6 +- src/command-parser/expand-arguments.spec.ts | 4 +- src/command-parser/expand-arguments.ts | 12 ++-- src/command.ts | 8 +-- src/concurrently.spec.ts | 42 ++++++++--- src/concurrently.ts | 14 +++- src/defaults.ts | 20 ++++-- src/index.ts | 6 ++ 11 files changed, 193 insertions(+), 94 deletions(-) diff --git a/README.md b/README.md index 0cdde051..38546bca 100644 --- a/README.md +++ b/README.md @@ -134,33 +134,37 @@ Good frontend one-liner example [here](https://github.com/kimmobrunfeldt/dont-co Help: ``` - concurrently [options] General - -m, --max-processes How many processes should run at once. - New processes only spawn after all restart tries of a - process. [number] - -n, --names List of custom names to be used in prefix template. - Example names: "main,browser,server" [string] - --name-separator The character to split on. Example usage: - concurrently -n "styles|scripts|server" --name-separator - "|" [default: ","] - -r, --raw Output only raw output of processes, disables - prettifying and concurrently coloring. [boolean] - -s, --success Return exit code of zero or one based on the success or - failure of the "first" child to terminate, the "last - child", or succeed only if "all" child processes - succeed. + -m, --max-processes How many processes should run at once. + New processes only spawn after all restart tries + of a process. [number] + -n, --names List of custom names to be used in prefix + template. + Example names: "main,browser,server" [string] + --name-separator The character to split on. Example usage: + concurrently -n "styles|scripts|server" + --name-separator "|" [default: ","] + -s, --success Return exit code of zero or one based on the + success or failure of the "first" child to + terminate, the "last child", or succeed only if + "all" child processes succeed. [choices: "first", "last", "all"] [default: "all"] - --no-color Disables colors from logging [boolean] - --hide Comma-separated list of processes to hide the output. - The processes can be identified by their name or index. - [string] [default: ""] - -g, --group Order the output as if the commands were run - sequentially. [boolean] - --timings Show timing information for all processes + -r, --raw Output only raw output of processes, disables + prettifying and concurrently coloring. [boolean] + --no-color Disables colors from logging. [boolean] + --hide Comma-separated list of processes to hide the + output. + The processes can be identified by their name or + index. [string] [default: ""] + -g, --group Order the output as if the commands were run + sequentially. [boolean] + --timings Show timing information for all processes. [boolean] [default: false] + -P, --passthrough-arguments Passthrough additional arguments to commands + (accessible via placeholders) instead of treating + them as commands. [boolean] [default: false] Prefix styling -p, --prefix Prefix used in logging for each process. @@ -197,9 +201,9 @@ Input handling process. [default: 0] Killing other processes - -k, --kill-others kill other processes if one exits or dies [boolean] - --kill-others-on-fail kill other processes if one exits with non zero - status code [boolean] + -k, --kill-others Kill other processes if one exits or dies.[boolean] + --kill-others-on-fail Kill other processes if one exits with non zero + status code. [boolean] Restarting --restart-tries How many times a process that died should restart. @@ -212,6 +216,7 @@ Options: -h, --help Show help [boolean] -v, -V, --version Show version number [boolean] + Examples: - Output nothing more than stdout+stderr of child processes @@ -233,7 +238,8 @@ Examples: - Configuring via environment variables with CONCURRENTLY_ prefix - $ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true concurrently "echo hello" "echo world" + $ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true concurrently "echo + hello" "echo world" - Send input to default @@ -258,17 +264,22 @@ Examples: $ concurrently "npm:watch-*" + - Exclude patterns so that between "lint:js" and "lint:fix:js", only "lint:js" + is ran + + $ concurrently "npm:*(!fix)" + - Passthrough some additional arguments via '{}' placeholder - $ concurrently "echo {1}" -- foo + $ concurrently -P "echo {1}" -- foo - Passthrough all additional arguments via '{@}' placeholder - $ concurrently "npm:dev-* -- {@}" -- --watch --noEmit + $ concurrently -P "npm:dev-* -- {@}" -- --watch --noEmit - Passthrough all additional arguments combined via '{*}' placeholder - $ concurrently "npm:dev-* -- {*}" -- --watch --noEmit + $ concurrently -P "npm:dev-* -- {*}" -- --watch --noEmit For more details, visit https://github.com/open-cli-tools/concurrently ``` @@ -279,7 +290,7 @@ concurrently can be used programmatically by using the API documented below: ### `concurrently(commands[, options])` - `commands`: an array of either strings (containing the commands to run) or objects - with the shape `{ command, name, prefixColor, env, cwd, passthroughArgs }`. + with the shape `{ command, name, prefixColor, env, cwd, additionalArguments }`. - `options` (optional): an object containing any of the below: - `cwd`: the working directory to be used by all commands. Can be overriden per command. @@ -311,6 +322,7 @@ concurrently can be used programmatically by using the API documented below: - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`. - `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format) to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ` + - `passthroughArguments`: Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. Default: `false` > **Returns:** an object in the shape `{ result, commands }`. > - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option), @@ -344,7 +356,7 @@ It has the following properties: - `name`: the name of the command; defaults to an empty string. - `cwd`: the current working directory of the command. - `env`: an object with all the environment variables that the command will be spawned with. -- `passthroughArgs`: list of additional arguments which can be used in command via placeholders. +- `additionalArguments`: list of additional arguments which can be used in command via placeholders. - `killed`: whether the command has been killed. - `exited`: whether the command exited yet. - `pid`: the command's process ID. diff --git a/bin/concurrently.spec.ts b/bin/concurrently.spec.ts index b8ac8537..5517b7b4 100644 --- a/bin/concurrently.spec.ts +++ b/bin/concurrently.spec.ts @@ -58,11 +58,11 @@ it('has help command', done => { }); it('has version command', done => { - Rx.combineLatest( + Rx.combineLatest([ run('--version').close, run('-V').close, run('-v').close, - ).subscribe(events => { + ]).subscribe(events => { expect(events[0][0]).toBe(0); expect(events[1][0]).toBe(0); expect(events[2][0]).toBe(0); @@ -446,3 +446,22 @@ describe('--timings', () => { }, done); }); }); + +describe('--passthrough-arguments', () => { + it('argument placeholders are properly replaced when passthrough-arguments is enabled', done => { + const child = run('--passthrough-arguments "echo {1}" -- echo'); + child.log.pipe(buffer(child.close)).subscribe(lines => { + expect(lines).toContainEqual(expect.stringContaining('[0] echo \'echo\' exited with code 0')); + done(); + }, done); + }); + + it('argument placeholders are not replaced when passthrough-arguments is disabled', done => { + const child = run('"echo {1}" -- echo'); + child.log.pipe(buffer(child.close)).subscribe(lines => { + expect(lines).toContainEqual(expect.stringContaining('[0] echo {1} exited with code 0')); + expect(lines).toContainEqual(expect.stringContaining('[1] echo exited with code 0')); + done(); + }, done); + }); +}); diff --git a/bin/concurrently.ts b/bin/concurrently.ts index 5715e360..ce4ae9f1 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -9,8 +9,12 @@ import { epilogue } from './epilogue'; const cleanArgs = hideBin(process.argv); // Find argument separator (double dash) const argsSepIdx = cleanArgs.findIndex((arg) => arg === '--'); -// If separator has been found, only pass arguments before it to yargs, otherwise pass all arguments -const args = yargs(argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs) +// Arguments before separator +const argsBeforeSep = argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs; +// Arguments after separator +const argsAfterSep = argsSepIdx >= 0 ? cleanArgs.slice(argsSepIdx + 1) : []; + +const args = yargs(argsBeforeSep) .usage('$0 [options] ') .help('h') .alias('h', 'help') @@ -76,19 +80,27 @@ const args = yargs(argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs) type: 'boolean', }, 'timings': { - describe: 'Show timing information for all processes', + describe: 'Show timing information for all processes.', type: 'boolean', default: defaults.timings, }, + 'passthrough-arguments': { + alias: 'P', + describe: + 'Passthrough additional arguments to commands (accessible via placeholders) ' + + 'instead of treating them as commands.', + type: 'boolean', + default: defaults.passthroughArguments, + }, // Kill others 'kill-others': { alias: 'k', - describe: 'kill other processes if one exits or dies', + describe: 'Kill other processes if one exits or dies.', type: 'boolean', }, 'kill-others-on-fail': { - describe: 'kill other processes if one exits with non zero status code', + describe: 'Kill other processes if one exits with non zero status code.', type: 'boolean', }, @@ -160,7 +172,7 @@ const args = yargs(argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs) 'Can be either the index or the name of the process.', }, }) - .group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide', 'group', 'timings'], 'General') + .group(['m', 'n', 'name-separator', 's', 'r', 'no-color', 'hide', 'g', 'timings', 'P'], 'General') .group(['p', 'c', 'l', 't'], 'Prefix styling') .group(['i', 'default-input-target'], 'Input handling') .group(['k', 'kill-others-on-fail'], 'Killing other processes') @@ -169,31 +181,37 @@ const args = yargs(argsSepIdx >= 0 ? cleanArgs.slice(0, argsSepIdx) : cleanArgs) .parseSync(); const names = (args.names || '').split(args['name-separator']); +// If "passthrough-arguments" is disabled, treat additional arguments as commands +const commands = args.passthroughArguments ? args._ : [...args._, ...argsAfterSep]; -concurrently(args._.map((command, index) => ({ - command: String(command), - name: names[index], - // Pass arguments after separator, if there are any - passthroughArgs: argsSepIdx >= 0 ? cleanArgs.slice(argsSepIdx + 1) : [], -})), { - handleInput: args['handle-input'], - defaultInputTarget: args['default-input-target'], - killOthers: args.killOthers - ? ['success', 'failure'] - : (args.killOthersOnFail ? ['failure'] : []), - maxProcesses: args['max-processes'], - raw: args.raw, - hide: args.hide.split(','), - group: args.group, - prefix: args.prefix, - prefixColors: args['prefix-colors'].split(','), - prefixLength: args['prefix-length'], - restartDelay: args['restart-after'], - restartTries: args['restart-tries'], - successCondition: args.success, - timestampFormat: args['timestamp-format'], - timings: args.timings, -}).result.then( +concurrently( + commands.map((command, index) => ({ + command: String(command), + name: names[index], + // If enabled, make additional arguments accessible in command + additionalArguments: args.passthroughArguments ? argsAfterSep : [], + })), + { + handleInput: args['handle-input'], + defaultInputTarget: args['default-input-target'], + killOthers: args.killOthers + ? ['success', 'failure'] + : (args.killOthersOnFail ? ['failure'] : []), + maxProcesses: args['max-processes'], + raw: args.raw, + hide: args.hide.split(','), + group: args.group, + prefix: args.prefix, + prefixColors: args['prefix-colors'].split(','), + prefixLength: args['prefix-length'], + restartDelay: args['restart-after'], + restartTries: args['restart-tries'], + successCondition: args.success, + timestampFormat: args['timestamp-format'], + timings: args.timings, + passthroughArguments: args.passthroughArguments, + }, +).result.then( () => process.exit(0), () => process.exit(1), ); diff --git a/bin/epilogue.ts b/bin/epilogue.ts index b508b8a7..dc3c4d54 100644 --- a/bin/epilogue.ts +++ b/bin/epilogue.ts @@ -56,15 +56,15 @@ const examples = [ }, { description: 'Passthrough some additional arguments via \'{}\' placeholder', - example: '$ $0 "echo {1}" -- foo', + example: '$ $0 -P "echo {1}" -- foo', }, { description: 'Passthrough all additional arguments via \'{@}\' placeholder', - example: '$ $0 "npm:dev-* -- {@}" -- --watch --noEmit', + example: '$ $0 -P "npm:dev-* -- {@}" -- --watch --noEmit', }, { description: 'Passthrough all additional arguments combined via \'{*}\' placeholder', - example: '$ $0 "npm:dev-* -- {*}" -- --watch --noEmit', + example: '$ $0 -P "npm:dev-* -- {*}" -- --watch --noEmit', }, ]; diff --git a/src/command-parser/expand-arguments.spec.ts b/src/command-parser/expand-arguments.spec.ts index efe67687..aa2ce30b 100644 --- a/src/command-parser/expand-arguments.spec.ts +++ b/src/command-parser/expand-arguments.spec.ts @@ -3,10 +3,10 @@ import { ExpandArguments } from './expand-arguments'; const parser = new ExpandArguments(); -const createCommandInfo = (command: string, passthroughArgs: string[]): CommandInfo => ({ +const createCommandInfo = (command: string, additionalArguments: string[]): CommandInfo => ({ command, name: '', - passthroughArgs, + additionalArguments, }); it('returns command as is when no placeholders', () => { diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index 969bd3d3..22df7b9c 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -2,7 +2,7 @@ import { CommandInfo } from '../command'; import { CommandParser } from './command-parser'; /** - * Replace placeholders with passthrough arguments. + * Replace placeholders with additional arguments. */ export class ExpandArguments implements CommandParser { parse(commandInfo: CommandInfo) { @@ -10,14 +10,14 @@ export class ExpandArguments implements CommandParser { if (match.startsWith('\\')) { return match.substring(1); } - if (!isNaN(p1) && p1 > 0 && commandInfo.passthroughArgs[p1-1]) { - return `'${commandInfo.passthroughArgs[p1-1]}'`; + if (!isNaN(p1) && p1 > 0 && commandInfo.additionalArguments[p1-1]) { + return `'${commandInfo.additionalArguments[p1-1]}'`; } if (p1 === '@') { - return commandInfo.passthroughArgs.map((arg) => `'${arg}'`).join(' '); + return commandInfo.additionalArguments.map((arg) => `'${arg}'`).join(' '); } - if (p1 === '*' && commandInfo.passthroughArgs.length > 0) { - return `'${commandInfo.passthroughArgs.join(' ')}'`; + if (p1 === '*' && commandInfo.additionalArguments.length > 0) { + return `'${commandInfo.additionalArguments.join(' ')}'`; } return ''; }); diff --git a/src/command.ts b/src/command.ts index 11c3394b..5feada49 100644 --- a/src/command.ts +++ b/src/command.ts @@ -36,7 +36,7 @@ export interface CommandInfo { /** * Additional arguments which can be used in command via placeholders. */ - passthroughArgs?: string[] + additionalArguments?: string[] } export interface CloseEvent { @@ -105,7 +105,7 @@ export class Command implements CommandInfo { readonly cwd?: string; /** @inheritdoc */ - readonly passthroughArgs: string[]; + readonly additionalArguments: string[]; readonly close = new Rx.Subject(); readonly error = new Rx.Subject(); @@ -124,7 +124,7 @@ export class Command implements CommandInfo { } constructor( - { index, name, command, prefixColor, env, cwd, passthroughArgs }: CommandInfo & { index: number }, + { index, name, command, prefixColor, env, cwd, additionalArguments }: CommandInfo & { index: number }, spawnOpts: SpawnOptions, spawn: SpawnCommand, killProcess: KillProcess, @@ -135,7 +135,7 @@ export class Command implements CommandInfo { this.prefixColor = prefixColor; this.env = env; this.cwd = cwd; - this.passthroughArgs = passthroughArgs; + this.additionalArguments = additionalArguments; this.killProcess = killProcess; this.spawn = spawn; this.spawnOpts = spawnOpts; diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index 6ba350c5..aaa30a68 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -187,14 +187,19 @@ it('uses overridden cwd option for each command if specified', () => { })); }); -it('argument placeholders are properly replaced', () => { - const passthroughArgs = ['foo', 'bar']; - create([ - { command: 'echo {1}', passthroughArgs }, - { command: 'echo {@}', passthroughArgs }, - { command: 'echo {*}', passthroughArgs }, - { command: 'echo \\{@}', passthroughArgs }, - ]); +it('argument placeholders are properly replaced when passthrough-arguments is enabled', () => { + const additionalArguments = ['foo', 'bar']; + create( + [ + { command: 'echo {1}', additionalArguments }, + { command: 'echo {@}', additionalArguments }, + { command: 'echo {*}', additionalArguments }, + { command: 'echo \\{@}', additionalArguments }, + ], + { + passthroughArguments: true, + }, + ); expect(spawn).toHaveBeenCalledTimes(4); expect(spawn).toHaveBeenCalledWith('echo \'foo\'', expect.objectContaining({})); @@ -203,6 +208,27 @@ it('argument placeholders are properly replaced', () => { expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); }); +it('argument placeholders are not replaced when passthrough-arguments is disabled', () => { + const additionalArguments = ['foo', 'bar']; + create( + [ + { command: 'echo {1}', additionalArguments }, + { command: 'echo {@}', additionalArguments }, + { command: 'echo {*}', additionalArguments }, + { command: 'echo \\{@}', additionalArguments }, + ], + { + passthroughArguments: false, + }, + ); + + expect(spawn).toHaveBeenCalledTimes(4); + expect(spawn).toHaveBeenCalledWith('echo {1}', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo {*}', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); +}); + it('runs onFinish hook after all commands run', async () => { const promise = create(['foo', 'bar'], { maxProcesses: 1 }); expect(spawn).toHaveBeenCalledTimes(1); diff --git a/src/concurrently.ts b/src/concurrently.ts index f36c4f38..10ba8b0b 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -97,6 +97,11 @@ export type ConcurrentlyOptions = { * Defaults to the `tree-kill` module. */ kill: KillProcess, + + /** + * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. + */ + passthroughArguments?: boolean, }; /** @@ -118,9 +123,12 @@ export function concurrently( new StripQuotes(), new ExpandNpmShortcut(), new ExpandNpmWildcard(), - new ExpandArguments(), ]; + if (options.passthroughArguments) { + commandParsers.push(new ExpandArguments()); + } + let lastColor = ''; let commands = _(baseCommands) .map(mapToCommandInfo) @@ -186,7 +194,7 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: '', env: {}, cwd: '', - passthroughArgs: [], + additionalArguments: [], }; } @@ -195,7 +203,7 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: command.name || '', env: command.env || {}, cwd: command.cwd || '', - passthroughArgs: command.passthroughArgs || [], + additionalArguments: command.additionalArguments || [], }, command.prefixColor ? { prefixColor: command.prefixColor, } : {}); diff --git a/src/defaults.ts b/src/defaults.ts index a9e6a2d7..9f74abed 100644 --- a/src/defaults.ts +++ b/src/defaults.ts @@ -7,7 +7,7 @@ import { SuccessCondition } from './completion-listener'; export const defaultInputTarget = 0; /** - * Whether process.stdin should be forwarded to child processes + * Whether process.stdin should be forwarded to child processes. */ export const handleInput = false; @@ -16,9 +16,14 @@ export const handleInput = false; */ export const maxProcesses = 0; -// Indices and names of commands whose output are not to be logged +/** + * Indices and names of commands whose output are not to be logged. + */ export const hide = ''; +/** + * The character to split on. + */ export const nameSeparator = ','; /** @@ -33,14 +38,14 @@ export const prefix = ''; export const prefixColors = 'reset'; /** - * How many bytes we'll show on the command prefix + * How many bytes we'll show on the command prefix. */ export const prefixLength = 10; export const raw = false; /** - * Number of attempts of restarting a process, if it exits with non-0 code + * Number of attempts of restarting a process, if it exits with non-0 code. */ export const restartTries = 0; @@ -67,6 +72,11 @@ export const timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS'; export const cwd: string | undefined = undefined; /** - * Whether to show timing information for processes in console output + * Whether to show timing information for processes in console output. */ export const timings = false; + +/** + * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. + */ +export const passthroughArguments = false; diff --git a/src/index.ts b/src/index.ts index b3704245..dc7d3304 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,6 +77,11 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & { * @see LogTimings */ timings?: boolean, + + /** + * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. + */ + passthroughArguments?: boolean, }; export default (commands: ConcurrentlyCommandInput[], options: Partial = {}) => { @@ -122,6 +127,7 @@ export default (commands: ConcurrentlyCommandInput[], options: Partial Date: Thu, 21 Apr 2022 10:47:30 +0200 Subject: [PATCH 05/15] Make expand-arguments class clearer --- src/command-parser/expand-arguments.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index 22df7b9c..cc41d333 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -6,17 +6,18 @@ import { CommandParser } from './command-parser'; */ export class ExpandArguments implements CommandParser { parse(commandInfo: CommandInfo) { - const command = commandInfo.command.replace(/\\?{([@\*]|\d+)\}/g, (match, p1) => { + const command = commandInfo.command.replace(/\\?{([@\*]|\d+)\}/g, (match, placeholderTarget) => { + // Don't replace the placeholder if it is escaped by a backslash. if (match.startsWith('\\')) { return match.substring(1); } - if (!isNaN(p1) && p1 > 0 && commandInfo.additionalArguments[p1-1]) { - return `'${commandInfo.additionalArguments[p1-1]}'`; + if (!isNaN(placeholderTarget) && placeholderTarget > 0 && commandInfo.additionalArguments[placeholderTarget-1]) { + return `'${commandInfo.additionalArguments[placeholderTarget-1]}'`; } - if (p1 === '@') { + if (placeholderTarget === '@') { return commandInfo.additionalArguments.map((arg) => `'${arg}'`).join(' '); } - if (p1 === '*' && commandInfo.additionalArguments.length > 0) { + if (placeholderTarget === '*' && commandInfo.additionalArguments.length > 0) { return `'${commandInfo.additionalArguments.join(' ')}'`; } return ''; From 76c9595294e4679b3109178240cf787f0e157990 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Thu, 21 Apr 2022 12:24:42 +0200 Subject: [PATCH 06/15] Use shell-quote to quote passthrough arguments --- bin/concurrently.spec.ts | 2 +- package-lock.json | 24 +++++++++++++++++++++ package.json | 2 ++ src/command-parser/expand-arguments.spec.ts | 11 +++++++--- src/command-parser/expand-arguments.ts | 7 +++--- src/concurrently.spec.ts | 4 ++-- 6 files changed, 41 insertions(+), 9 deletions(-) diff --git a/bin/concurrently.spec.ts b/bin/concurrently.spec.ts index 5517b7b4..c0e4713e 100644 --- a/bin/concurrently.spec.ts +++ b/bin/concurrently.spec.ts @@ -451,7 +451,7 @@ describe('--passthrough-arguments', () => { it('argument placeholders are properly replaced when passthrough-arguments is enabled', done => { const child = run('--passthrough-arguments "echo {1}" -- echo'); child.log.pipe(buffer(child.close)).subscribe(lines => { - expect(lines).toContainEqual(expect.stringContaining('[0] echo \'echo\' exited with code 0')); + expect(lines).toContainEqual(expect.stringContaining('[0] echo echo exited with code 0')); done(); }, done); }); diff --git a/package-lock.json b/package-lock.json index fcd17fff..ae6540d0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "date-fns": "^2.16.1", "lodash": "^4.17.21", "rxjs": "^6.6.3", + "shell-quote": "^1.7.3", "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", @@ -25,6 +26,7 @@ "@types/jest": "^27.0.3", "@types/lodash": "^4.14.178", "@types/node": "^17.0.0", + "@types/shell-quote": "^1.7.1", "@types/supports-color": "^8.1.1", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "^5.8.1", @@ -1040,6 +1042,12 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "node_modules/@types/shell-quote": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.1.tgz", + "integrity": "sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -6355,6 +6363,11 @@ "node": ">=0.10.0" } }, + "node_modules/shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" + }, "node_modules/shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", @@ -8550,6 +8563,12 @@ "integrity": "sha512-6gOkRe7OIioWAXfnO/2lFiv+SJichKVSys1mSsgyrYHSEjk8Ctv4tSR/Odvnu+HWlH2C8j53dahU03XmQdd5fA==", "dev": true }, + "@types/shell-quote": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/@types/shell-quote/-/shell-quote-1.7.1.tgz", + "integrity": "sha512-SWZ2Nom1pkyXCDohRSrkSKvDh8QOG9RfAsrt5/NsPQC4UQJ55eG0qClA40I+Gkez4KTQ0uDUT8ELRXThf3J5jw==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.0.tgz", @@ -12727,6 +12746,11 @@ "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", "dev": true }, + "shell-quote": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.3.tgz", + "integrity": "sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==" + }, "shellwords": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", diff --git a/package.json b/package.json index 39cf2e55..23660ca8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "date-fns": "^2.16.1", "lodash": "^4.17.21", "rxjs": "^6.6.3", + "shell-quote": "^1.7.3", "spawn-command": "^0.0.2-1", "supports-color": "^8.1.0", "tree-kill": "^1.2.2", @@ -53,6 +54,7 @@ "@types/jest": "^27.0.3", "@types/lodash": "^4.14.178", "@types/node": "^17.0.0", + "@types/shell-quote": "^1.7.1", "@types/supports-color": "^8.1.1", "@types/yargs": "^17.0.8", "@typescript-eslint/eslint-plugin": "^5.8.1", diff --git a/src/command-parser/expand-arguments.spec.ts b/src/command-parser/expand-arguments.spec.ts index aa2ce30b..282b1775 100644 --- a/src/command-parser/expand-arguments.spec.ts +++ b/src/command-parser/expand-arguments.spec.ts @@ -16,12 +16,17 @@ it('returns command as is when no placeholders', () => { it('single argument placeholder is replaced', () => { const commandInfo = createCommandInfo('echo {1}', ['foo', 'bar']); - expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo\'' }); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' }); +}); + +it('argument placeholder is replaced and quoted properly', () => { + const commandInfo = createCommandInfo('echo {1}', ['foo bar']); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo bar\'' }); }); it('multiple single argument placeholders are replaced', () => { const commandInfo = createCommandInfo('echo {2} {1}', ['foo', 'bar']); - expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'bar\' \'foo\'' }); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo bar foo' }); }); it('empty replacement with single placeholder and no passthrough arguments', () => { @@ -41,7 +46,7 @@ it('empty replacement with combined placeholder and no passthrough arguments', ( it('all arguments placeholder is replaced', () => { const commandInfo = createCommandInfo('echo {@}', ['foo', 'bar']); - expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo\' \'bar\'' }); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo bar' }); }); it('combined arguments placeholder is replaced', () => { diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index cc41d333..3eed6242 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -1,5 +1,6 @@ import { CommandInfo } from '../command'; import { CommandParser } from './command-parser'; +import { quote } from 'shell-quote'; /** * Replace placeholders with additional arguments. @@ -12,13 +13,13 @@ export class ExpandArguments implements CommandParser { return match.substring(1); } if (!isNaN(placeholderTarget) && placeholderTarget > 0 && commandInfo.additionalArguments[placeholderTarget-1]) { - return `'${commandInfo.additionalArguments[placeholderTarget-1]}'`; + return quote([commandInfo.additionalArguments[placeholderTarget-1]]); } if (placeholderTarget === '@') { - return commandInfo.additionalArguments.map((arg) => `'${arg}'`).join(' '); + return quote(commandInfo.additionalArguments); } if (placeholderTarget === '*' && commandInfo.additionalArguments.length > 0) { - return `'${commandInfo.additionalArguments.join(' ')}'`; + return quote([commandInfo.additionalArguments.join(' ')]); } return ''; }); diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index aaa30a68..9afed625 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -202,8 +202,8 @@ it('argument placeholders are properly replaced when passthrough-arguments is en ); expect(spawn).toHaveBeenCalledTimes(4); - expect(spawn).toHaveBeenCalledWith('echo \'foo\'', expect.objectContaining({})); - expect(spawn).toHaveBeenCalledWith('echo \'foo\' \'bar\'', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo foo', expect.objectContaining({})); + expect(spawn).toHaveBeenCalledWith('echo foo bar', expect.objectContaining({})); expect(spawn).toHaveBeenCalledWith('echo \'foo bar\'', expect.objectContaining({})); expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); }); From 0da7370b0ad9c39684dc431a9aa0197a8e75271a Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 25 Apr 2022 16:49:22 +0200 Subject: [PATCH 07/15] Pass additionalArguments directly to ExpandArguments --- README.md | 6 ++-- bin/concurrently.ts | 4 +-- src/command-parser/expand-arguments.spec.ts | 35 ++++++++++++--------- src/command-parser/expand-arguments.ts | 16 +++++++--- src/command.ts | 11 +------ src/concurrently.spec.ts | 22 ++++++------- src/concurrently.ts | 20 +++++++++--- src/index.ts | 7 +++++ 8 files changed, 72 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 38546bca..28046642 100644 --- a/README.md +++ b/README.md @@ -290,7 +290,7 @@ concurrently can be used programmatically by using the API documented below: ### `concurrently(commands[, options])` - `commands`: an array of either strings (containing the commands to run) or objects - with the shape `{ command, name, prefixColor, env, cwd, additionalArguments }`. + with the shape `{ command, name, prefixColor, env, cwd }`. - `options` (optional): an object containing any of the below: - `cwd`: the working directory to be used by all commands. Can be overriden per command. @@ -322,7 +322,8 @@ concurrently can be used programmatically by using the API documented below: - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`. - `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format) to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ` - - `passthroughArguments`: Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. Default: `false` + - `passthroughArguments`: passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. Default: `false` + - `additionalArguments`: list of additional arguments passed to concurrently. > **Returns:** an object in the shape `{ result, commands }`. > - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option), @@ -356,7 +357,6 @@ It has the following properties: - `name`: the name of the command; defaults to an empty string. - `cwd`: the current working directory of the command. - `env`: an object with all the environment variables that the command will be spawned with. -- `additionalArguments`: list of additional arguments which can be used in command via placeholders. - `killed`: whether the command has been killed. - `exited`: whether the command exited yet. - `pid`: the command's process ID. diff --git a/bin/concurrently.ts b/bin/concurrently.ts index ce4ae9f1..8f243271 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -180,6 +180,7 @@ const args = yargs(argsBeforeSep) .epilogue(epilogue) .parseSync(); +// Get names of commands by the specified separator const names = (args.names || '').split(args['name-separator']); // If "passthrough-arguments" is disabled, treat additional arguments as commands const commands = args.passthroughArguments ? args._ : [...args._, ...argsAfterSep]; @@ -188,8 +189,6 @@ concurrently( commands.map((command, index) => ({ command: String(command), name: names[index], - // If enabled, make additional arguments accessible in command - additionalArguments: args.passthroughArguments ? argsAfterSep : [], })), { handleInput: args['handle-input'], @@ -210,6 +209,7 @@ concurrently( timestampFormat: args['timestamp-format'], timings: args.timings, passthroughArguments: args.passthroughArguments, + additionalArguments: argsAfterSep, }, ).result.then( () => process.exit(0), diff --git a/src/command-parser/expand-arguments.spec.ts b/src/command-parser/expand-arguments.spec.ts index 282b1775..74a7da92 100644 --- a/src/command-parser/expand-arguments.spec.ts +++ b/src/command-parser/expand-arguments.spec.ts @@ -1,61 +1,68 @@ import { CommandInfo } from '../command'; import { ExpandArguments } from './expand-arguments'; -const parser = new ExpandArguments(); - -const createCommandInfo = (command: string, additionalArguments: string[]): CommandInfo => ({ +const createCommandInfo = (command: string): CommandInfo => ({ command, name: '', - additionalArguments, }); it('returns command as is when no placeholders', () => { - const commandInfo = createCommandInfo('echo foo', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo foo'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' }); }); it('single argument placeholder is replaced', () => { - const commandInfo = createCommandInfo('echo {1}', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo {1}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo' }); }); it('argument placeholder is replaced and quoted properly', () => { - const commandInfo = createCommandInfo('echo {1}', ['foo bar']); + const parser = new ExpandArguments(['foo bar']); + const commandInfo = createCommandInfo('echo {1}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo bar\'' }); }); it('multiple single argument placeholders are replaced', () => { - const commandInfo = createCommandInfo('echo {2} {1}', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo {2} {1}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo bar foo' }); }); it('empty replacement with single placeholder and no passthrough arguments', () => { - const commandInfo = createCommandInfo('echo {3}', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo {3}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); }); it('empty replacement with all placeholder and no passthrough arguments', () => { - const commandInfo = createCommandInfo('echo {@}', []); + const parser = new ExpandArguments([]); + const commandInfo = createCommandInfo('echo {@}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); }); it('empty replacement with combined placeholder and no passthrough arguments', () => { - const commandInfo = createCommandInfo('echo {*}', []); + const parser = new ExpandArguments([]); + const commandInfo = createCommandInfo('echo {*}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); }); it('all arguments placeholder is replaced', () => { - const commandInfo = createCommandInfo('echo {@}', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo {@}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo foo bar' }); }); it('combined arguments placeholder is replaced', () => { - const commandInfo = createCommandInfo('echo {*}', ['foo', 'bar']); + const parser = new ExpandArguments(['foo', 'bar']); + const commandInfo = createCommandInfo('echo {*}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo \'foo bar\'' }); }); it('escaped argument placeholders are not replaced', () => { + const parser = new ExpandArguments(['foo', 'bar']); // Equals to single backslash on command line - const commandInfo = createCommandInfo('echo \\{1} \\{@} \\{*}', ['foo', 'bar']); + const commandInfo = createCommandInfo('echo \\{1} \\{@} \\{*}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo {1} {@} {*}' }); }); diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index 3eed6242..e6c2261a 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -6,20 +6,26 @@ import { quote } from 'shell-quote'; * Replace placeholders with additional arguments. */ export class ExpandArguments implements CommandParser { + additionalArguments: string[]; + + constructor(additionalArguments: string[]) { + this.additionalArguments = additionalArguments; + } + parse(commandInfo: CommandInfo) { const command = commandInfo.command.replace(/\\?{([@\*]|\d+)\}/g, (match, placeholderTarget) => { // Don't replace the placeholder if it is escaped by a backslash. if (match.startsWith('\\')) { return match.substring(1); } - if (!isNaN(placeholderTarget) && placeholderTarget > 0 && commandInfo.additionalArguments[placeholderTarget-1]) { - return quote([commandInfo.additionalArguments[placeholderTarget-1]]); + if (!isNaN(placeholderTarget) && placeholderTarget > 0 && this.additionalArguments[placeholderTarget-1]) { + return quote([this.additionalArguments[placeholderTarget-1]]); } if (placeholderTarget === '@') { - return quote(commandInfo.additionalArguments); + return quote(this.additionalArguments); } - if (placeholderTarget === '*' && commandInfo.additionalArguments.length > 0) { - return quote([commandInfo.additionalArguments.join(' ')]); + if (placeholderTarget === '*') { + return quote([this.additionalArguments.join(' ')]); } return ''; }); diff --git a/src/command.ts b/src/command.ts index 5feada49..48ed20e7 100644 --- a/src/command.ts +++ b/src/command.ts @@ -32,11 +32,6 @@ export interface CommandInfo { * Color to use on prefix of command. */ prefixColor?: string, - - /** - * Additional arguments which can be used in command via placeholders. - */ - additionalArguments?: string[] } export interface CloseEvent { @@ -104,9 +99,6 @@ export class Command implements CommandInfo { /** @inheritdoc */ readonly cwd?: string; - /** @inheritdoc */ - readonly additionalArguments: string[]; - readonly close = new Rx.Subject(); readonly error = new Rx.Subject(); readonly stdout = new Rx.Subject(); @@ -124,7 +116,7 @@ export class Command implements CommandInfo { } constructor( - { index, name, command, prefixColor, env, cwd, additionalArguments }: CommandInfo & { index: number }, + { index, name, command, prefixColor, env, cwd }: CommandInfo & { index: number }, spawnOpts: SpawnOptions, spawn: SpawnCommand, killProcess: KillProcess, @@ -135,7 +127,6 @@ export class Command implements CommandInfo { this.prefixColor = prefixColor; this.env = env; this.cwd = cwd; - this.additionalArguments = additionalArguments; this.killProcess = killProcess; this.spawn = spawn; this.spawnOpts = spawnOpts; diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index 9afed625..b4b0c2b2 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -188,16 +188,16 @@ it('uses overridden cwd option for each command if specified', () => { }); it('argument placeholders are properly replaced when passthrough-arguments is enabled', () => { - const additionalArguments = ['foo', 'bar']; create( [ - { command: 'echo {1}', additionalArguments }, - { command: 'echo {@}', additionalArguments }, - { command: 'echo {*}', additionalArguments }, - { command: 'echo \\{@}', additionalArguments }, + { command: 'echo {1}' }, + { command: 'echo {@}' }, + { command: 'echo {*}' }, + { command: 'echo \\{@}' }, ], { passthroughArguments: true, + additionalArguments: ['foo', 'bar'], }, ); @@ -209,16 +209,16 @@ it('argument placeholders are properly replaced when passthrough-arguments is en }); it('argument placeholders are not replaced when passthrough-arguments is disabled', () => { - const additionalArguments = ['foo', 'bar']; create( [ - { command: 'echo {1}', additionalArguments }, - { command: 'echo {@}', additionalArguments }, - { command: 'echo {*}', additionalArguments }, - { command: 'echo \\{@}', additionalArguments }, + { command: 'echo {1}' }, + { command: 'echo {@}' }, + { command: 'echo {*}' }, + { command: 'echo \\{@}' }, ], { passthroughArguments: false, + additionalArguments: ['foo', 'bar'], }, ); @@ -241,7 +241,7 @@ it('runs onFinish hook after all commands run', async () => { expect(onFinishHooks[1]).not.toHaveBeenCalled(); processes[1].emit('close', 0, null); - await promise; + await promise.result; expect(onFinishHooks[0]).toHaveBeenCalled(); expect(onFinishHooks[1]).toHaveBeenCalled(); diff --git a/src/concurrently.ts b/src/concurrently.ts index 10ba8b0b..301cc18a 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -21,6 +21,7 @@ const defaults: ConcurrentlyOptions = { raw: false, controllers: [], cwd: undefined, + additionalArguments: [], }; /** @@ -52,7 +53,15 @@ export type ConcurrentlyOptions = { * Which stream should the commands output be written to. */ outputStream?: Writable, + + /** + * Whether the output should be ordered as if the commands were run sequentially. + */ group?: boolean, + + /** + * Comma-separated list of chalk colors to use on prefixes. + */ prefixColors?: string[], /** @@ -102,6 +111,12 @@ export type ConcurrentlyOptions = { * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. */ passthroughArguments?: boolean, + + /** + * List of additional arguments passed to concurrently. + * Defaults to an empty array. + */ + additionalArguments?: string[], }; /** @@ -126,7 +141,7 @@ export function concurrently( ]; if (options.passthroughArguments) { - commandParsers.push(new ExpandArguments()); + commandParsers.push(new ExpandArguments(options.additionalArguments)); } let lastColor = ''; @@ -159,7 +174,6 @@ export function concurrently( ); commands = handleResult.commands; - if (options.logger) { const outputWriter = new OutputWriter({ outputStream: options.outputStream, @@ -194,7 +208,6 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: '', env: {}, cwd: '', - additionalArguments: [], }; } @@ -203,7 +216,6 @@ function mapToCommandInfo(command: ConcurrentlyCommandInput): CommandInfo { name: command.name || '', env: command.env || {}, cwd: command.cwd || '', - additionalArguments: command.additionalArguments || [], }, command.prefixColor ? { prefixColor: command.prefixColor, } : {}); diff --git a/src/index.ts b/src/index.ts index dc7d3304..86b0f917 100644 --- a/src/index.ts +++ b/src/index.ts @@ -82,6 +82,12 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & { * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. */ passthroughArguments?: boolean, + + + /** + * List of additional arguments passed to concurrently. + */ + additionalArguments?: string[], }; export default (commands: ConcurrentlyCommandInput[], options: Partial = {}) => { @@ -128,6 +134,7 @@ export default (commands: ConcurrentlyCommandInput[], options: Partial Date: Mon, 2 May 2022 09:17:11 +0200 Subject: [PATCH 08/15] Enhance constructor in ExpandArguments Co-authored-by: Gustavo Henke --- src/command-parser/expand-arguments.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index e6c2261a..d0e84aae 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -6,10 +6,7 @@ import { quote } from 'shell-quote'; * Replace placeholders with additional arguments. */ export class ExpandArguments implements CommandParser { - additionalArguments: string[]; - - constructor(additionalArguments: string[]) { - this.additionalArguments = additionalArguments; + constructor(private readonly additionalArguments: string[]) { } parse(commandInfo: CommandInfo) { From 3117a147988722b49bdf5dda23b622c590b783be Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 2 May 2022 10:16:46 +0200 Subject: [PATCH 09/15] Fix check for number argument replacement Co-authored-by: Gustavo Henke --- src/command-parser/expand-arguments.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index d0e84aae..f6d0b7df 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -15,7 +15,7 @@ export class ExpandArguments implements CommandParser { if (match.startsWith('\\')) { return match.substring(1); } - if (!isNaN(placeholderTarget) && placeholderTarget > 0 && this.additionalArguments[placeholderTarget-1]) { + if (!isNaN(placeholderTarget) && placeholderTarget > 0 && placeholderTarget <= this.additionalArguments.length) { return quote([this.additionalArguments[placeholderTarget-1]]); } if (placeholderTarget === '@') { From b9b27e8062ecb3bdc0544b1c67dfd6f54eac2c92 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 2 May 2022 10:46:07 +0200 Subject: [PATCH 10/15] Fix & enhance EditorConfig --- .editorconfig | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.editorconfig b/.editorconfig index 2bab9d70..dba32748 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,16 +3,17 @@ # top-most EditorConfig file root = true -# Unix-style newlines with a newline ending every file +# Unix-style newlines with a newline ending and space indentation for all files [*] end_of_line = lf insert_final_newline = true indent_style = space -# Tab indentation (no size specified) -[*.js] +# 4 space indentation and max line length of 100 in *.ts files +[*.ts] indent_size = 4 +max_line_length = 100 -# Matches the exact files package.json +# 2 space indentation in package.json [package.json] indent_size = 2 From 658e6be340748f4c703d0b235d00b8c67354446e Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 2 May 2022 10:46:24 +0200 Subject: [PATCH 11/15] More specific placeholder regex & reformat file --- src/command-parser/expand-arguments.ts | 48 ++++++++++++++++---------- 1 file changed, 29 insertions(+), 19 deletions(-) diff --git a/src/command-parser/expand-arguments.ts b/src/command-parser/expand-arguments.ts index f6d0b7df..fec5462c 100644 --- a/src/command-parser/expand-arguments.ts +++ b/src/command-parser/expand-arguments.ts @@ -6,29 +6,39 @@ import { quote } from 'shell-quote'; * Replace placeholders with additional arguments. */ export class ExpandArguments implements CommandParser { - constructor(private readonly additionalArguments: string[]) { - } + constructor(private readonly additionalArguments: string[]) {} parse(commandInfo: CommandInfo) { - const command = commandInfo.command.replace(/\\?{([@\*]|\d+)\}/g, (match, placeholderTarget) => { - // Don't replace the placeholder if it is escaped by a backslash. - if (match.startsWith('\\')) { - return match.substring(1); - } - if (!isNaN(placeholderTarget) && placeholderTarget > 0 && placeholderTarget <= this.additionalArguments.length) { - return quote([this.additionalArguments[placeholderTarget-1]]); - } - if (placeholderTarget === '@') { - return quote(this.additionalArguments); - } - if (placeholderTarget === '*') { - return quote([this.additionalArguments.join(' ')]); - } - return ''; - }); + const command = commandInfo.command.replace( + /\\?\{([@\*]|[1-9][0-9]*)\}/g, + (match, placeholderTarget) => { + // Don't replace the placeholder if it is escaped by a backslash. + if (match.startsWith('\\')) { + return match.substring(1); + } + // Replace numeric placeholder if value exists in additional arguments. + if ( + !isNaN(placeholderTarget) && + placeholderTarget <= this.additionalArguments.length + ) { + return quote([this.additionalArguments[placeholderTarget - 1]]); + } + // Replace all arguments placeholder. + if (placeholderTarget === '@') { + return quote(this.additionalArguments); + } + // Replace combined arguments placeholder. + if (placeholderTarget === '*') { + return quote([this.additionalArguments.join(' ')]); + } + // Replace placeholder with empty string + // if value doesn't exist in additional arguments. + return ''; + }, + ); return Object.assign({}, commandInfo, { command, }); } -}; +} From 809c7da24a3d95c50d26456bf0cbc0e744771527 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 2 May 2022 10:58:01 +0200 Subject: [PATCH 12/15] Clarify description for expand-arguments test Co-authored-by: Gustavo Henke --- src/command-parser/expand-arguments.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/command-parser/expand-arguments.spec.ts b/src/command-parser/expand-arguments.spec.ts index 74a7da92..45a8d1d4 100644 --- a/src/command-parser/expand-arguments.spec.ts +++ b/src/command-parser/expand-arguments.spec.ts @@ -30,7 +30,7 @@ it('multiple single argument placeholders are replaced', () => { expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo bar foo' }); }); -it('empty replacement with single placeholder and no passthrough arguments', () => { +it('empty replacement with single placeholder and not enough passthrough arguments', () => { const parser = new ExpandArguments(['foo', 'bar']); const commandInfo = createCommandInfo('echo {3}'); expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); From f8c82537b2dc81b076479b0d76c76a6b33207551 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 2 May 2022 12:04:10 +0200 Subject: [PATCH 13/15] Reduce options for replacing of arguments --- README.md | 3 +-- bin/concurrently.ts | 3 +-- src/concurrently.spec.ts | 9 ++------- src/concurrently.ts | 14 +++++--------- src/index.ts | 10 ++-------- 5 files changed, 11 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 28046642..bc7249a4 100644 --- a/README.md +++ b/README.md @@ -322,8 +322,7 @@ concurrently can be used programmatically by using the API documented below: - `restartDelay`: how many milliseconds to wait between process restarts. Default: `0`. - `timestampFormat`: a [date-fns format](https://date-fns.org/v2.0.1/docs/format) to use when prefixing with `time`. Default: `yyyy-MM-dd HH:mm:ss.ZZZ` - - `passthroughArguments`: passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. Default: `false` - - `additionalArguments`: list of additional arguments passed to concurrently. + - `additionalArguments`: list of additional arguments passed that will get replaced in each command. If not defined, no argument replacing will happen. > **Returns:** an object in the shape `{ result, commands }`. > - `result`: a `Promise` that resolves if the run was successful (according to `successCondition` option), diff --git a/bin/concurrently.ts b/bin/concurrently.ts index 8f243271..4dd8f895 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -208,8 +208,7 @@ concurrently( successCondition: args.success, timestampFormat: args['timestamp-format'], timings: args.timings, - passthroughArguments: args.passthroughArguments, - additionalArguments: argsAfterSep, + additionalArguments: args.passthroughArguments ? argsAfterSep : undefined, }, ).result.then( () => process.exit(0), diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index b4b0c2b2..faa327aa 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -187,7 +187,7 @@ it('uses overridden cwd option for each command if specified', () => { })); }); -it('argument placeholders are properly replaced when passthrough-arguments is enabled', () => { +it('argument placeholders are properly replaced when additional arguments are passed', () => { create( [ { command: 'echo {1}' }, @@ -196,7 +196,6 @@ it('argument placeholders are properly replaced when passthrough-arguments is en { command: 'echo \\{@}' }, ], { - passthroughArguments: true, additionalArguments: ['foo', 'bar'], }, ); @@ -208,7 +207,7 @@ it('argument placeholders are properly replaced when passthrough-arguments is en expect(spawn).toHaveBeenCalledWith('echo {@}', expect.objectContaining({})); }); -it('argument placeholders are not replaced when passthrough-arguments is disabled', () => { +it('argument placeholders are not replaced when additional arguments are not defined', () => { create( [ { command: 'echo {1}' }, @@ -216,10 +215,6 @@ it('argument placeholders are not replaced when passthrough-arguments is disable { command: 'echo {*}' }, { command: 'echo \\{@}' }, ], - { - passthroughArguments: false, - additionalArguments: ['foo', 'bar'], - }, ); expect(spawn).toHaveBeenCalledTimes(4); diff --git a/src/concurrently.ts b/src/concurrently.ts index 301cc18a..06210925 100644 --- a/src/concurrently.ts +++ b/src/concurrently.ts @@ -21,7 +21,6 @@ const defaults: ConcurrentlyOptions = { raw: false, controllers: [], cwd: undefined, - additionalArguments: [], }; /** @@ -108,13 +107,10 @@ export type ConcurrentlyOptions = { kill: KillProcess, /** - * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. - */ - passthroughArguments?: boolean, - - /** - * List of additional arguments passed to concurrently. - * Defaults to an empty array. + * List of additional arguments passed that will get replaced in each command. + * If not defined, no argument replacing will happen. + * + * @see ExpandArguments */ additionalArguments?: string[], }; @@ -140,7 +136,7 @@ export function concurrently( new ExpandNpmWildcard(), ]; - if (options.passthroughArguments) { + if (options.additionalArguments) { commandParsers.push(new ExpandArguments(options.additionalArguments)); } diff --git a/src/index.ts b/src/index.ts index 86b0f917..371b8e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -79,13 +79,8 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & { timings?: boolean, /** - * Passthrough additional arguments to commands (accessible via placeholders) instead of treating them as commands. - */ - passthroughArguments?: boolean, - - - /** - * List of additional arguments passed to concurrently. + * List of additional arguments passed that will get replaced in each command. + * If not defined, no argument replacing will happen. */ additionalArguments?: string[], }; @@ -133,7 +128,6 @@ export default (commands: ConcurrentlyCommandInput[], options: Partial Date: Mon, 9 May 2022 11:20:20 +0200 Subject: [PATCH 14/15] Small fix for comment in EditorConfig --- .editorconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index dba32748..45e9c51a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -3,7 +3,7 @@ # top-most EditorConfig file root = true -# Unix-style newlines with a newline ending and space indentation for all files +# Unix-style newlines with newline ending and space indentation for all files [*] end_of_line = lf insert_final_newline = true From a370af3d6bac73d28a1c680459f67fdfa0cd6fa9 Mon Sep 17 00:00:00 2001 From: Pascal Jufer Date: Mon, 9 May 2022 11:21:16 +0200 Subject: [PATCH 15/15] Consistent (camel case) usage of args properties --- bin/concurrently.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/concurrently.ts b/bin/concurrently.ts index 4dd8f895..29c86b3b 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -181,7 +181,7 @@ const args = yargs(argsBeforeSep) .parseSync(); // Get names of commands by the specified separator -const names = (args.names || '').split(args['name-separator']); +const names = (args.names || '').split(args.nameSeparator); // If "passthrough-arguments" is disabled, treat additional arguments as commands const commands = args.passthroughArguments ? args._ : [...args._, ...argsAfterSep]; @@ -191,22 +191,22 @@ concurrently( name: names[index], })), { - handleInput: args['handle-input'], - defaultInputTarget: args['default-input-target'], + handleInput: args.handleInput, + defaultInputTarget: args.defaultInputTarget, killOthers: args.killOthers ? ['success', 'failure'] : (args.killOthersOnFail ? ['failure'] : []), - maxProcesses: args['max-processes'], + maxProcesses: args.maxProcesses, raw: args.raw, hide: args.hide.split(','), group: args.group, prefix: args.prefix, - prefixColors: args['prefix-colors'].split(','), - prefixLength: args['prefix-length'], - restartDelay: args['restart-after'], - restartTries: args['restart-tries'], + prefixColors: args.prefixColors.split(','), + prefixLength: args.prefixLength, + restartDelay: args.restartAfter, + restartTries: args.restartTries, successCondition: args.success, - timestampFormat: args['timestamp-format'], + timestampFormat: args.timestampFormat, timings: args.timings, additionalArguments: args.passthroughArguments ? argsAfterSep : undefined, },