ezx
v0.0.18
Published
Enhanced zx - Write better shell scripts with markdown preprocessing features including directory context persistence, file extraction, and fixes for zx's indentation parsing issues
Maintainers
Readme
ezx - Enhanced zx
An enhanced version of zx with markdown preprocessing features to solve common annoyances when writing shell scripts in markdown.
Features
1. Markdown Parsing Fixes (Indentation Issues)
ezx fixes known markdown parsing issues in zx related to indentation:
- Indented code blocks within lists - Code blocks indented within list items (e.g.,
```bashafter a bullet point) are now correctly recognized and executed - Indented prose - List item continuations and other indented prose won't be misinterpreted as code
These issues are tracked in the zx project:
- Issue #1388 - Indented code blocks in lists not executed
- Issue #1389 - Indented prose after empty line causes parsing error
Example that works in ezx but not in zx:
### deploy your contract
- on localhost
```bash
pnpm run contracts:deploy localhoston a network of your choice
pnpm run contracts:deploy <network>
### 2. Directory Context Persistence
In standard zx, each bash code block runs in a fresh context, so `cd` commands don't persist across blocks. ezx solves this with a special comment syntax:
```bash
# set the current directory
mkdir -p my-project
cd my-projectAll subsequent bash blocks will automatically prepend cd my-project:
echo "We're still in my-project!"
pwdNote: The directory context also affects file extraction blocks. Files created after # set the current directory will have their paths relative to the original directory, but the command will prepend cd to create them in the correct location.
3. File Extraction
Extract code blocks to files using the file: directive in a preceding blockquote. The file path can optionally be wrapped in backticks.
⚠️ Breaking Change: The
file:prefix is now required. The old syntax>filename`` without the prefix no longer works.
> file: `package.json`
```json
{
"name": "my-project",
"version": "1.0.0"
}
```Or without backticks:
> file: package.json
```json
{
"name": "my-project"
}
```JSON:
file:
my-project/package.json
{
"name": "my-project",
"version": "1.0.0"
}TypeScript/JavaScript:
file:
my-project/index.ts
console.log("Hello world")Bash/Shell:
file:
my-project/.npmrc
shared-workspace-lockfile=falseMarkdown:
file:
my-project/README.md
# My ProjectAny language: The file directive works with any language, including unknown languages, because it's specified in a preceding blockquote, not inside the code content.
Note: zx executes all code blocks in markdown files. If you have code blocks that are for documentation only (examples to show users, not to run or write as files), add the no-eval flag to prevent execution:
```typescript no-eval
interface User {
name: string;
age: number;
}
```File extraction blocks are transformed to bash heredoc commands that execute in order with the rest of the markdown. If a directory context is active, the heredoc command will prepend cd:
# Without directory context:
cat > 'path/to/file' << 'EZXEOF'
content here
EZXEOF
# With directory context (after "# set the current directory my-project"):
cd my-project && cat > 'path/to/file' << 'EZXEOF'
content here
EZXEOFThis means if a parent directory doesn't exist, the command will fail (as expected). You should create directories first using mkdir -p in a bash block.
4. Async Bash Execution
Run bash commands asynchronously using the async: directive. This is useful for starting background processes like development servers.
💡 zx doesn't support background bash tasks with
&. Theasync:directive solves this by transforming the bash block to a JavaScript$template that runs withoutawait.
> async: `start dev server`
```bash
npm run dev
```The text after async: is optional and serves as documentation only. Both formats work:
> async: `start dev server`
> async: start dev server
> async:Example: Running a server in the background while continuing with other tasks:
> async: `start server`
```bash
npm run dev
```
```bash
# This runs immediately, doesn't wait for the server
echo "Server starting in background..."
sleep 2
curl http://localhost:3000
```How it works: The async directive transforms bash blocks into JavaScript blocks using zx's $ template literal:
// Input: async bash block with "npm run dev"
// Output: JavaScript block
$`npm run dev`The command runs without await, so execution continues immediately. zx automatically awaits all pending promises at the end of the script.
Kill on Complete: Use the --kill-on-complete (or -k) CLI option to kill all async processes when the script completes, instead of waiting for them:
ezx --kill-on-complete script.md
ezx -k script.mdThis is useful for development servers or other long-running processes that should be terminated after the script runs.
Important: The async: directive only works with bash/sh/shell blocks. Using it with other languages will result in an error.
Installation
npm install ezxUsage
CLI Usage
Run markdown scripts with ezx enhancements:
ezx script.md
ezx --verbose script.md
ezx --shell=/bin/bash script.mdExecute Specific Sections
You can execute only specific sections of a markdown file by specifying their heading titles. This is useful for running parts of a larger documentation file:
# Run only the "Create a new project" section
ezx --sections "Create a new project" docs/getting-started.md
# Run multiple sections
ezx --sections "Prerequisites" "Create a new project" docs/getting-started.md
# Short form
ezx -s "Create a new project" docs/getting-started.mdSection matching is case-insensitive and includes all content under the specified heading and its subheadings until another heading of the same or higher level is encountered.
Programmatic Usage
import { runMarkdownFile, runMarkdownContent } from 'ezx'
// Run markdown file
await runMarkdownFile('./script.md', { verbose: true })
// Run markdown content directly
await runMarkdownContent(`
\`\`\`bash
# set the current directory
mkdir -p my-project
cd my-project
\`\`\`
\`\`\`bash
echo "Still in my-project!"
\`\`\`
`, { verbose: true })Preprocessor API
You can also use the preprocessor directly:
import { preprocessMarkdown, preprocessFile } from 'ezx'
// Preprocess markdown content
const { preprocessedMarkdown, extractedFiles } = preprocessMarkdown(markdown)
// Preprocess markdown file
const result = preprocessFile('./script.md', {
cwd: process.cwd()
})Example
Here's a complete example showing all features:
# Setup a new project
\`\`\`bash
# set the current directory
mkdir -p my-app
cd my-app
npm init -y
\`\`\`
> file: `my-app/package.json`
\`\`\`json
{
"name": "my-app",
"version": "1.0.0",
"scripts": {
"start": "node index.js",
"dev": "node --watch index.js"
}
}
\`\`\`
> file: `my-app/index.js`
\`\`\`javascript
const http = require('http');
const server = http.createServer((req, res) => {
res.end('Hello from ezx!');
});
server.listen(3000, () => console.log('Server running on port 3000'));
\`\`\`
> async: `start dev server`
\`\`\`bash
npm run dev
\`\`\`
\`\`\`bash
# This runs immediately while server starts in background
sleep 2
curl http://localhost:3000
\`\`\`
\`\`\`bash
cd ..
rm -rf my-app
\`\`\`API Reference
runMarkdown(markdown, options)
Run markdown content or file with CLI-like options.
Parameters:
markdown(string | Buffer) - Markdown content or file pathoptions(RunMarkdownOptions) - CLI-like options
Options:
verbose(boolean) - Enable verbose modequiet(boolean) - Suppress outputshell(string) - Custom shell binaryprefix(string) - Prefix all commandspostfix(string) - Postfix all commandscwd(string) - Current working directorypreferLocal(boolean) - Prefer locally installed packagessections(string[]) - Execute only sections under these heading titleskillOnComplete(boolean) - Kill all spawned async processes when script completes
runMarkdownFile(filePath, options)
Convenience function to run a markdown file from disk.
runMarkdownContent(markdownContent, options)
Convenience function to run markdown content directly as a string.
preprocessMarkdown(input, options)
Preprocess markdown content.
Parameters:
input(string) - Markdown contentoptions(object)cwd(string) - Current working directory (default:process.cwd())
Returns: PreprocessResult
preprocessedMarkdown(string) - The preprocessed markdown with file directive blocks transformed to bash heredocsextractedFiles(FileExtraction[]) - Array of extracted file information (for informational purposes only)
preprocessFile(filePath, options)
Read and preprocess a markdown file.
Development
# Install dependencies
npm install
# Build
npm run build
# Watch mode
npm run dev
# Run tests
node dist/cli.js test-preprocessor.md --verboseLicense
MIT
