diff --git a/.editorconfig b/.editorconfig index 2bab9d70..45e9c51a 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 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 diff --git a/README.md b/README.md index 15e42a64..bc7249a4 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,6 +264,23 @@ 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 -P "echo {1}" -- foo + + - Passthrough all additional arguments via '{@}' placeholder + + $ concurrently -P "npm:dev-* -- {@}" -- --watch --noEmit + + - Passthrough all additional arguments combined via '{*}' placeholder + + $ concurrently -P "npm:dev-* -- {*}" -- --watch --noEmit + For more details, visit https://github.com/open-cli-tools/concurrently ``` @@ -299,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` + - `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.spec.ts b/bin/concurrently.spec.ts index b8ac8537..c0e4713e 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 f7dd870c..29c86b3b 100755 --- a/bin/concurrently.ts +++ b/bin/concurrently.ts @@ -1,10 +1,20 @@ #!/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 === '--'); +// 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') @@ -70,19 +80,27 @@ const args = yargs 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', }, @@ -154,38 +172,45 @@ const args = yargs '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') .group(['restart-tries', 'restart-after'], 'Restarting') .epilogue(epilogue) - .argv; + .parseSync(); -const names = (args.names || '').split(args['name-separator']); +// Get names of commands by the specified 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]; -concurrently(args._.map((command, index) => ({ - command: String(command), - name: names[index], -})), { - 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], + })), + { + handleInput: args.handleInput, + defaultInputTarget: args.defaultInputTarget, + killOthers: args.killOthers + ? ['success', 'failure'] + : (args.killOthersOnFail ? ['failure'] : []), + maxProcesses: args.maxProcesses, + raw: args.raw, + hide: args.hide.split(','), + group: args.group, + prefix: args.prefix, + prefixColors: args.prefixColors.split(','), + prefixLength: args.prefixLength, + restartDelay: args.restartAfter, + restartTries: args.restartTries, + successCondition: args.success, + timestampFormat: args.timestampFormat, + timings: args.timings, + additionalArguments: args.passthroughArguments ? argsAfterSep : undefined, + }, +).result.then( () => process.exit(0), () => process.exit(1), ); diff --git a/bin/epilogue.ts b/bin/epilogue.ts index 1fce3805..dc3c4d54 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 -P "echo {1}" -- foo', + }, + { + description: 'Passthrough all additional arguments via \'{@}\' placeholder', + example: '$ $0 -P "npm:dev-* -- {@}" -- --watch --noEmit', + }, + { + description: 'Passthrough all additional arguments combined via \'{*}\' placeholder', + example: '$ $0 -P "npm:dev-* -- {*}" -- --watch --noEmit', + }, ]; export const epilogue = ` diff --git a/package-lock.json b/package-lock.json index 9e0fc350..e6315646 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,11 @@ "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", - "yargs": "^16.2.0" + "yargs": "^17.3.1" }, "bin": { "concurrently": "dist/bin/concurrently.js" @@ -25,7 +26,9 @@ "@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", "@typescript-eslint/parser": "^5.8.1", "coveralls": "^3.1.0", @@ -797,6 +800,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", @@ -1073,6 +1085,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", @@ -1086,9 +1104,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": "*" @@ -4641,6 +4659,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", @@ -6499,6 +6526,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", @@ -6921,24 +6953,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" @@ -7777,30 +7809,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", @@ -8451,6 +8492,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": { @@ -8701,6 +8753,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", @@ -8714,9 +8772,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": "*" @@ -11438,6 +11496,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", @@ -12871,6 +12938,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", @@ -13220,21 +13292,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": { @@ -13867,23 +13939,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..23660ca8 100644 --- a/package.json +++ b/package.json @@ -44,16 +44,19 @@ "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", - "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/shell-quote": "^1.7.1", "@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..45a8d1d4 --- /dev/null +++ b/src/command-parser/expand-arguments.spec.ts @@ -0,0 +1,68 @@ +import { CommandInfo } from '../command'; +import { ExpandArguments } from './expand-arguments'; + +const createCommandInfo = (command: string): CommandInfo => ({ + command, + name: '', +}); + +it('returns command as is when no placeholders', () => { + 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 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 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 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 not enough passthrough arguments', () => { + 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 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 parser = new ExpandArguments([]); + const commandInfo = createCommandInfo('echo {*}'); + expect(parser.parse(commandInfo)).toEqual({ ...commandInfo, command: 'echo ' }); +}); + +it('all arguments placeholder is replaced', () => { + 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 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} \\{@} \\{*}'); + 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..fec5462c --- /dev/null +++ b/src/command-parser/expand-arguments.ts @@ -0,0 +1,44 @@ +import { CommandInfo } from '../command'; +import { CommandParser } from './command-parser'; +import { quote } from 'shell-quote'; + +/** + * Replace placeholders with additional arguments. + */ +export class ExpandArguments implements CommandParser { + constructor(private readonly additionalArguments: string[]) {} + + parse(commandInfo: CommandInfo) { + 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, + }); + } +} 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 }); diff --git a/src/command.ts b/src/command.ts index 2ee253db..48ed20e7 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, } diff --git a/src/concurrently.spec.ts b/src/concurrently.spec.ts index ec13d990..faa327aa 100644 --- a/src/concurrently.spec.ts +++ b/src/concurrently.spec.ts @@ -187,6 +187,43 @@ it('uses overridden cwd option for each command if specified', () => { })); }); +it('argument placeholders are properly replaced when additional arguments are passed', () => { + create( + [ + { command: 'echo {1}' }, + { command: 'echo {@}' }, + { command: 'echo {*}' }, + { command: 'echo \\{@}' }, + ], + { + additionalArguments: ['foo', 'bar'], + }, + ); + + 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('argument placeholders are not replaced when additional arguments are not defined', () => { + create( + [ + { command: 'echo {1}' }, + { command: 'echo {@}' }, + { command: 'echo {*}' }, + { command: 'echo \\{@}' }, + ], + ); + + 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); @@ -199,7 +236,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 29c97412..06210925 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'; @@ -51,7 +52,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[], /** @@ -96,6 +105,14 @@ export type ConcurrentlyOptions = { * Defaults to the `tree-kill` module. */ kill: KillProcess, + + /** + * List of additional arguments passed that will get replaced in each command. + * If not defined, no argument replacing will happen. + * + * @see ExpandArguments + */ + additionalArguments?: string[], }; /** @@ -119,6 +136,10 @@ export function concurrently( new ExpandNpmWildcard(), ]; + if (options.additionalArguments) { + commandParsers.push(new ExpandArguments(options.additionalArguments)); + } + let lastColor = ''; let commands = _(baseCommands) .map(mapToCommandInfo) @@ -149,7 +170,6 @@ export function concurrently( ); commands = handleResult.commands; - if (options.logger) { const outputWriter = new OutputWriter({ outputStream: options.outputStream, 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..371b8e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -77,6 +77,12 @@ export type ConcurrentlyOptions = BaseConcurrentlyOptions & { * @see LogTimings */ timings?: boolean, + + /** + * List of additional arguments passed that will get replaced in each command. + * If not defined, no argument replacing will happen. + */ + additionalArguments?: string[], }; export default (commands: ConcurrentlyCommandInput[], options: Partial = {}) => { @@ -122,6 +128,7 @@ export default (commands: ConcurrentlyCommandInput[], options: Partial