diff --git a/docs/lines.md b/docs/lines.md index efd7db52d2..7980e92442 100644 --- a/docs/lines.md +++ b/docs/lines.md @@ -36,7 +36,7 @@ Alternatively, [`subprocess.iterable()`](api.md#subprocessiterablereadableoption The iteration waits for the subprocess to end (even when using [`break`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/break) or [`return`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/return)). It throws if the subprocess [fails](api.md#result). This means you do not need to `await` the subprocess' [promise](execution.md#result). ```js -for await (const line of execa`npm run build`.iterable())) { +for await (const line of execa`npm run build`.iterable()) { /* ... */ } ``` @@ -51,6 +51,121 @@ for await (const stderrLine of execa`npm run build`.iterable({from: 'stderr'})) } ``` +## Custom delimiters + +By default, lines are split using newline characters (`\n` or `\r\n`). To use a custom delimiter instead, combine the [`lines: false`](api.md#optionslines) option with a [transform](transform.md) that splits on your desired delimiter. + +### Array output with custom delimiter + +```js +import {execa} from 'execa'; + +// Split output by null character (useful for filenames with spaces) +const splitByNull = function * (chunk) { + if (typeof chunk !== 'string') { + yield chunk; + return; + } + + const parts = chunk.split('\0'); + for (const part of parts) { + if (part.length > 0) { + yield part; + } + } +}; + +const {stdout: filenames} = await execa({ + stdout: {transform: splitByNull, objectMode: true}, +})`find . -print0`; +// filenames is now an array of strings, split by null character +console.log(filenames); // ['.', './file1.txt', './file2.txt', ...] +``` + +### Progressive iteration with custom delimiter + +```js +import {execa} from 'execa'; + +// Custom transform that splits by a specific delimiter +const createDelimiterTransform = delimiter => function * (chunk) { + if (typeof chunk !== 'string') { + yield chunk; + return; + } + + // Keep track of partial chunks between iterations + if (!this.buffer) { + this.buffer = ''; + } + + this.buffer += chunk; + const parts = this.buffer.split(delimiter); + + // Yield all complete parts except the last one + for (let i = 0; i < parts.length - 1; i++) { + yield parts[i]; + } + + // Keep the last (potentially incomplete) part in the buffer + this.buffer = parts[parts.length - 1]; +}; + +// Usage example: processing CSV-like output with semicolon delimiter +const subprocess = execa({ + stdout: {transform: createDelimiterTransform(';'), objectMode: true}, +})`some-command-outputting-semicolon-separated-values`; + +for await (const value of subprocess) { + console.log('Got value:', value); +} +``` + +### Common delimiter examples + +```js +import {execa} from 'execa'; + +// Comma-separated values +const csvTransform = function * (chunk) { + if (typeof chunk !== 'string') { + yield chunk; + return; + } + for (const value of chunk.split(',')) { + if (value.trim()) yield value.trim(); + } +}; + +// Tab-separated values +const tsvTransform = function * (chunk) { + if (typeof chunk !== 'string') { + yield chunk; + return; + } + for (const value of chunk.split('\t')) { + if (value) yield value; + } +}; + +// Double-newline (paragraph) separator +const paragraphTransform = function * (chunk) { + if (typeof chunk !== 'string') { + yield chunk; + return; + } + for (const para of chunk.split('\n\n')) { + if (para.trim()) yield para.trim(); + } +}; + +// Example: parse comma-separated output +const {stdout: items} = await execa({ + stdout: {transform: csvTransform, objectMode: true}, +})`echo "apple,banana,cherry,date"`; +console.log(items); // ['apple', 'banana', 'cherry', 'date'] +``` + ## Newlines ### Final newline diff --git a/examples/build-script.js b/examples/build-script.js new file mode 100644 index 0000000000..e7361dbd56 --- /dev/null +++ b/examples/build-script.js @@ -0,0 +1,33 @@ +import {$} from 'execa'; + +// Example: Build script with error handling +// Demonstrates common patterns for build automation + +async function build() { + console.log('šŸš€ Starting build process...\n'); + + try { + // Clean previous build + console.log('🧹 Cleaning previous build...'); + await $`rm -rf dist`; + + // Run linting + console.log('šŸ” Running linter...'); + await $`eslint src/`; + + // Run tests + console.log('🧪 Running tests...'); + await $`npm test`; + + // Build the project + console.log('šŸ“¦ Building project...'); + await $`npm run build`; + + console.log('\nāœ… Build completed successfully!'); + } catch (error) { + console.error('\nāŒ Build failed:', error.shortMessage || error.message); + process.exit(1); + } +} + +build(); diff --git a/examples/git-helpers.js b/examples/git-helpers.js new file mode 100644 index 0000000000..e8246919b7 --- /dev/null +++ b/examples/git-helpers.js @@ -0,0 +1,58 @@ +import {execa} from 'execa'; + +// Example: Git helper functions +// Common git operations wrapped for programmatic usage + +/** + * Get the current git branch + */ +export async function getCurrentBranch() { + const {stdout} = await execa`git branch --show-current`; + return stdout; +} + +/** + * Check if working directory is clean + */ +export async function isWorkingDirectoryClean() { + try { + await execa`git diff --quiet`; + return true; + } catch { + return false; + } +} + +/** + * Get the latest commit message + */ +export async function getLastCommitMessage() { + const {stdout} = await execa`git log -1 --pretty=%B`; + return stdout.trim(); +} + +/** + * Get list of changed files + */ +export async function getChangedFiles() { + const {stdout} = await execa`git status --porcelain`; + if (!stdout) return []; + return stdout.split('\n').map(line => line.slice(3).trim()).filter(Boolean); +} + +// Demo: Run examples if executed directly +if (import.meta.url === `file://${process.argv[1]}`) { + console.log('šŸ“ Git Helpers Demo\n'); + + const branch = await getCurrentBranch(); + console.log(`Current branch: ${branch}`); + + const isClean = await isWorkingDirectoryClean(); + console.log(`Working directory clean: ${isClean ? 'āœ…' : 'āš ļø Has uncommitted changes'}`); + + const lastCommit = await getLastCommitMessage(); + console.log(`Last commit: ${lastCommit.slice(0, 50)}${lastCommit.length > 50 ? '...' : ''}`); + + const changedFiles = await getChangedFiles(); + console.log(`Changed files: ${changedFiles.length > 0 ? changedFiles.join(', ') : 'None'}`); +} diff --git a/examples/parallel-tasks.js b/examples/parallel-tasks.js new file mode 100644 index 0000000000..5c830c0bfd --- /dev/null +++ b/examples/parallel-tasks.js @@ -0,0 +1,50 @@ +import {execa} from 'execa'; + +// Example: Running multiple commands in parallel +// Demonstrates Promise.all() patterns with Execa + +async function runParallel() { + console.log('⚔ Running parallel tasks...\n'); + + // Example 1: Run independent commands in parallel + console.log('1ļøāƒ£ Running independent commands in parallel:'); + const startTime = Date.now(); + + const [nodeVersion, npmVersion, gitVersion] = await Promise.all([ + execa`node --version`, + execa`npm --version`, + execa`git --version`, + ]); + + console.log(` Node: ${nodeVersion.stdout}`); + console.log(` npm: v${npmVersion.stdout}`); + console.log(` Git: ${gitVersion.stdout}`); + console.log(` ā±ļø Time: ${Date.now() - startTime}ms\n`); + + // Example 2: Parallel with error handling + console.log('2ļøāƒ£ Parallel with individual error handling:'); + const results = await Promise.allSettled([ + execa`node --version`, + execa`nonexistent-command`, + execa`git --version`, + ]); + + results.forEach((result, index) => { + if (result.status === 'fulfilled') { + console.log(` āœ… Task ${index + 1}: ${result.value.stdout.slice(0, 30)}`); + } else { + console.log(` āŒ Task ${index + 1}: Failed`); + } + }); + + // Example 3: Parallel with timeout + console.log('\n3ļøāƒ£ Parallel with timeout:'); + try { + const {stdout} = await execa({timeout: 5000})`sleep 1 && echo "Completed"`; + console.log(` āœ… Result: ${stdout}`); + } catch (error) { + console.log(` ā±ļø Timed out or failed: ${error.shortMessage}`); + } +} + +runParallel().catch(console.error); diff --git a/examples/readme.md b/examples/readme.md new file mode 100644 index 0000000000..f66429cfe6 --- /dev/null +++ b/examples/readme.md @@ -0,0 +1,24 @@ +# Execa Examples + +This directory contains practical examples demonstrating common use cases of Execa. + +## Examples + +- [`build-script.js`](build-script.js) - A build script with error handling and logging +- [`git-helpers.js`](git-helpers.js) - Git command wrappers for Node.js projects +- [`parallel-tasks.js`](parallel-tasks.js) - Running multiple commands in parallel +- [`stream-processing.js`](stream-processing.js) - Processing command output with transforms + +## Running Examples + +All examples use ES modules. Run them with: + +```bash +node example-name.js +``` + +Make sure you have `execa` installed in your project first: + +```bash +npm install execa +``` diff --git a/examples/stream-processing.js b/examples/stream-processing.js new file mode 100644 index 0000000000..49f8e5392b --- /dev/null +++ b/examples/stream-processing.js @@ -0,0 +1,53 @@ +import {execa} from 'execa'; + +// Example: Processing command output with transforms +// Shows how to filter and transform stdout/stderr streams + +async function streamProcessing() { + console.log('šŸ”„ Stream Processing Examples\n'); + + // Example 1: Simple transform - prefix each line + console.log('1ļøāƒ£ Prefixing output lines:'); + const prefixTransform = function* (line) { + yield `[${new Date().toISOString()}] ${line}`; + }; + + await execa({stdout: prefixTransform})`echo -e "line1\nline2\nline3"`; + + // Example 2: Filter lines containing specific text + console.log('\n2ļøāƒ£ Filtering npm audit output for high severity:'); + const filterHighSeverity = function* (line) { + if (line.includes('high') || line.includes('critical')) { + yield line; + } + }; + + try { + // Simulated: in real usage, this would be `npm audit` + await execa({stdout: filterHighSeverity})`echo -e "low: package1\nhigh: vulnerable-package\ninfo: package2"`; + } catch (error) { + // npm audit exits with non-zero on vulnerabilities + console.log(' (Example output only)'); + } + + // Example 3: Count lines transform + console.log('\n3ļøāƒ£ Line counter transform:'); + let lineCount = 0; + const countTransform = function* (line) { + lineCount++; + yield `${lineCount}: ${line}`; + }; + + const {stdout} = await execa({stdout: countTransform, lines: true})`ls -la`; + console.log(` Total lines processed: ${lineCount}`); + + // Example 4: Transform to uppercase + console.log('\n4ļøāƒ£ Uppercase transform:'); + const upperTransform = function* (line) { + yield line.toUpperCase(); + }; + + await execa({stdout: upperTransform})`echo "hello world"`; +} + +streamProcessing().catch(console.error);