diff --git a/README.md b/README.md index f46d099..e435c20 100644 --- a/README.md +++ b/README.md @@ -4,14 +4,16 @@ Elegant replacement for [`each-cli`](https://www.npmjs.com/package/each-cli), [`foreach-cli`](https://www.npmjs.com/package/foreach-cli) and \*nix -[`find -exec`](https://man7.org/linux/man-pages/man1/find.1.html) command. Very useful for NPM +[`find -exec`](https://man7.org/linux/man-pages/man1/find.1.html) command in +very little lines of +[code](https://github.com/zerodevx/findx-cli/blob/main/cli.js). Useful for NPM scripts or CI. - [x] Cross-platform - [x] Concurrency support -- [x] Continues on error, then exits with code 1 -- [x] Logs output of every execution -- [x] Displays progress indicator when TTY +- [x] Continue on error, then exit with code 1 +- [x] Control output of every execution +- [x] Task progress indication in TTY ## Install @@ -25,8 +27,8 @@ $ npm i -g findx-cli $ findx '**/*.jpg' -- convert {{path}} {{dir}}/{{name}}.png ``` -This searches for all files matching the glob pattern, then runs the provided command against each -match. +This searches for all files matching the glob pattern, then runs the provided +command against each match. See below for more usage [examples](#examples). @@ -43,38 +45,40 @@ Arguments: Options: -C, --concurrent concurrent number of executions (default: 10) - -S, --shell run each execution in new shell - -d, --cd change to path directory for each run + --log log level (choices: "stdout", "stderr", "all", + "none", default: "all") + --sh run each execution in new shell + --cd change to path directory for each run -V, --version output the version number -h, --help display help for command ``` ## Command templating -Write your command using [mustache](https://github.com/janl/mustache.js/) syntax. The following tags -are available: +Write your command using [mustache](https://github.com/janl/mustache.js/) +syntax. The following tags are available: -| Tag | Example | Description | -| -------- | ----------------------- | ------------------------ | -| {{path}} | /home/user/dir/file.txt | Full path of file | -| {{root}} | / | Root | -| {{dir}} | /home/user/dir | Directory portion | -| {{base}} | file.txt | File name with extension | -| {{name}} | file | Name portion | -| {{ext}} | .txt | Extension portion | +| Tag | Eg | Desc | +| ---------- | ----------------------- | ------------------------ | +| `{{path}}` | /home/user/dir/file.txt | Full path of file | +| `{{root}}` | / | Root | +| `{{dir}}` | /home/user/dir | Directory portion | +| `{{base}}` | file.txt | File name with extension | +| `{{name}}` | file | Name portion | +| `{{ext}}` | .txt | Extension portion | ## Examples #### Untar each tar file in its own directory ``` -$ findx '**/*.tar' -d -- tar -xvf {{base}} +$ findx '**/*.tar' --cd -- tar -xvf {{base}} ``` #### Ignore some files and run shell-specific commands ``` -$ findx '**/LICENSE !ignored/**' -S -- 'cd {{dir}} && cat LICENSE' +$ findx '**/LICENSE !ignored/**' --sh -- 'cd {{dir}} && cat LICENSE' ``` #### Dry-run glob matches @@ -85,7 +89,8 @@ $ findx '**/*.@(txt,xml)' ## Development -Standard Github [contribution workflow](https://github.com/firstcontributions/first-contributions) +Standard Github +[contribution workflow](https://github.com/firstcontributions/first-contributions) applies. #### Tests diff --git a/cli.js b/cli.js index 04c71f9..093e48f 100644 --- a/cli.js +++ b/cli.js @@ -1,9 +1,10 @@ #!/usr/bin/env node -import { program } from 'commander' +import { program, Option } from 'commander' import { execaCommand } from 'execa' import fastglob from 'fast-glob' import mustache from 'mustache' import tasuku from 'tasuku' +import chalk from 'chalk' import pmap from 'p-map' import fs from 'node:fs/promises' import path from 'node:path' @@ -20,8 +21,13 @@ program .argument('', 'globs to match') .argument('[commands...]', 'commands to execute') .option('-C, --concurrent ', 'concurrent number of executions', 10) - .option('-S, --shell', 'run each execution in new shell') - .option('-d, --cd', 'change to path directory for each run') + .addOption( + new Option('--log ', 'log level') + .choices(['stdout', 'stderr', 'all', 'none']) + .default('all') + ) + .option('--sh', 'run each execution in new shell') + .option('--cd', 'change to path directory for each run') .version(version) .parse() @@ -43,11 +49,9 @@ if (!cmd) { process.exit() } -let errflag mustache.escape = (noop) => noop - -const run = (match) => - execaCommand( +const exe = async (match) => { + const spawn = await execaCommand( mustache.render(cmd, { path: match, ...path.parse(match) @@ -55,18 +59,26 @@ const run = (match) => { all: true, reject: false, - ...(opts.shell && { shell: true }), + ...(opts.sh && { shell: true }), ...(opts.cd && { cwd: path.dirname(match) }) } - ).then(({ exitCode, command, all }) => { - if (exitCode) errflag = true - console.log(`${exitCode ? '✖' : '✔'} ${command}\n${all}`) - }) + ) + const { exitCode: err, escapedCommand: esc, [opts.log]: log } = spawn + const { red: r, green: g, gray: y } = chalk + console.log(`${err ? r('✖') : g('✔')} ${y(esc)}${log ? `\n${log}` : ''}`) + return err +} -const runtty = (match) => tasuku(match, () => run(match)).then(({ clear }) => clear()) +let count = 0 +const exetty = async (match) => { + const task = await tasuku(`[${++count}/${matches.length}] ${match}`, () => + exe(match) + ) + task.clear() + return task.result +} -await pmap(matches, process.stdout.isTTY ? runtty : run, { +const run = await pmap(matches, process.stdout.isTTY ? exetty : exe, { concurrency: opts.concurrent }) - -if (errflag) process.exit(1) +if (run.some((r) => r)) process.exit(1) diff --git a/test/helpers/mock.js b/test/helpers/mock.js index 5a67bd8..00e4635 100644 --- a/test/helpers/mock.js +++ b/test/helpers/mock.js @@ -31,9 +31,14 @@ switch (args[0]) { } break } + case 'logs': { + console.log('test-stdout') + throw new Error('test-stderr') + } default: { - const r = rand(1, 10) - await sleep(r * 1000) - console.log(r) + const t = rand(1, 10) + await sleep(t * 1000) + if (!rand(0, 5)) throw new Error(`Random error: ${t}s`) + console.log(`Mock took: ${t}s`) } } diff --git a/test/spec.js b/test/spec.js index 81fb5b9..2c32896 100644 --- a/test/spec.js +++ b/test/spec.js @@ -25,7 +25,36 @@ test('concurrency', (t) => { test('error', (t) => { const err = t.throws(() => - run('-C', '2', 'test/**/*.txt', 'node', 'test/helpers/mock.js', 'error', '{{dir}}') + run( + '-C', + '2', + 'test/**/*.txt', + 'node', + 'test/helpers/mock.js', + 'error', + '{{dir}}' + ) ) t.is(err.stdout.split('done').length, 5) }) + +test('logs', (t) => { + const run2 = (...args) => + t.throws(() => + run( + ...args, + 'test/fixtures/dummy.txt', + 'node', + 'test/helpers/mock.js', + 'logs' + ) + ) + const { stdout: all } = run2() + t.true(all.includes('test-stdout') && all.includes('test-stderr')) + const { stdout } = run2('--log', 'stdout') + t.true(stdout.includes('test-stdout') && !stdout.includes('test-stderr')) + const { stdout: stderr } = run2('--log', 'stderr') + t.true(!stderr.includes('test-stdout') && stderr.includes('test-stderr')) + const { stdout: none } = run2('--log', 'none') + t.true(!none.includes('test-stdout') && !none.includes('test-stderr')) +})