Zx — Escribe mejores scripts de shell con JavaScript
Resumen
zx es la herramienta de Google para escribir scripts de shell en JavaScript/TypeScript. Envuelve child_process, escapa argumentos automáticamente y proporciona valores predeterminados sensatos, dándote el poder del ecosistema JavaScript en tus scripts.
#!/usr/bin/env zx
await $`cat package.json | grep name`
const branch = await $`git branch --show-current`
await $`dep deploy --branch=${branch}`
const name = 'foo & bar'
await $`mkdir /tmp/${name}` // No se necesitan comillas — escapado automáticamente
Bash es excelente para tareas simples, pero cuando los scripts se vuelven complejos, un lenguaje de programación completo ayuda. zx añade envoltorios útiles alrededor de child_process, escapa argumentos y proporciona valores predeterminados sensatos. Piensa: bash + JavaScript en un solo script.
Activadores
También se activa cuando los usuarios preguntan sobre cómo ejecutar comandos de shell en JavaScript, convertir scripts de bash a zx, ejecutar scripts remotos, scripts Markdown o scripts de shell en TypeScript.
Inicio rápido
npm install zx
Escribe scripts como archivos .mjs (soporta await de nivel superior). Añade el shebang #!/usr/bin/env zx o ejecuta mediante CLI:
zx ./script.mjs # Ejecución directa
npx zx ./script.mjs # Mediante npx
node --import zx/globals # Como cargador de Node.js
Todas las funciones ($, cd, fetch, etc.) están disponibles globalmente en scripts zx sin necesidad de importaciones. Para importaciones explícitas (mejor autocompletado en VS Code):
import 'zx/globals'
Conceptos principales
$`comando` — Ejecutar comandos de shell
El template literal etiquetado es el corazón de zx. Todo lo que está dentro de ${...} se escapa y entrecomilla automáticamente.
// Asíncrono (estándar) — devuelve ProcessPromise
const output = await $`ls -la`
// Variante síncrona — devuelve ProcessOutput directamente
const dir = $.sync`pwd`
// Los arrays se aplanan
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
// Los códigos de salida distintos de cero lanzan ProcessOutput
try {
await $`exit 1`
} catch (p) {
console.log(`Salida: ${p.exitCode}, Error: ${p.stderr}`)
}
Configuración preestablecida con $({...})
Crea instancias personalizadas de $ con opciones preestablecidas — encadenables y componibles:
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
const pwd = $$.sync`pwd`
// Los preajustes son encadenables
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true }) // Se aplican tanto nothrow como sync
ProcessPromise y ProcessOutput
$`cmd` ProcessPromise (extiende Promise)
├── .pipe() Canalización de flujos
├── .kill() Terminar proceso
├── .text() Salida como cadena
├── .json() Salida como JSON analizado
├── .lines() Salida dividida por líneas
├── .nothrow() Suprimir errores para este comando
├── .quiet() Suprimir salida para este comando
├── .timeout() Auto-terminar después de una duración
├── .stdio() Configurar E/S
├── .exitCode Promise<código de salida>
├── .stdout Flujo legible
├── .stderr Flujo legible
├── .stdin Flujo escribible
├── .pid / .cmd Metadatos del proceso
└── await → ProcessOutput
├── .stdout cadena
├── .stderr cadena
├── .exitCode número
├── .signal cadena|null
├── .text() / .json() / .lines() / .buffer() / .blob()
└── .ok booleano (cuando nothrow)
Árbol de decisión
Al escribir scripts zx, utiliza este árbol de decisión:
| Objetivo | Enfoque |
|---|---|
| Ejecutar un comando | await $`cmd` |
| Ejecutar sincrónicamente | $.sync`cmd` |
| Canalizar salida | .pipe($`next`) / .pipe('archivo.txt') |
| Manejar errores correctamente | $({nothrow: true}) / .nothrow() |
| Establecer tiempo de espera | $({timeout: '30s'}) / .timeout('30s') |
| Analizar salida JSON | (await $`cmd`).json() |
| Transmisión en tiempo real | for await (const line of $`cmd`) |
| Reintentar en caso de fallo | retry(5, () => $`cmd`) |
| Solicitud al usuario | question('Nombre: ') |
| Indicador de progreso | await spinner('Trabajando...', () => $`cmd`) |
| Cambiar directorio | cd('/ruta') o within(() => { $.cwd = '/tmp' }) |
| Archivos/directorios temporales | tmpfile() / tmpdir() |
| Analizar argumentos CLI | argv.bandera o minimist(process.argv.slice(2)) |
| Cargar archivo .env | dotenv.config('.env') |
Escribir scripts zx efectivos
Ejecución paralela
const results = await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
Manejo de errores con nothrow
$.nothrow = true
const repos = ['zx', 'webpod']
const clones = repos.map(n => $`git clone https://github.com/google/${n}`)
const results = await Promise.all(clones)
const errors = results.filter(o => !o.ok).map(o => o.stderr.trim())
console.log('Errores:', errors.join('\n'))
Canalización de flujos
// Encadenar comandos como pipes de bash
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
// Canalizar a archivo
await $`echo "Hola!"`.pipe('/tmp/salida.txt')
// Salida en tiempo real a terminal
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3`.pipe(process.stdout)
División y combinación de flujos
// Dividir una fuente en múltiples consumidores
const p = $`some-command`
const [o1, o2] = await Promise.all([
p.pipe`log`,
p.pipe`extract`,
])
// Combinar múltiples fuentes
const $h = $({ halt: true })
const p1 = $`echo foo`
const p2 = $h`echo a && sleep 0.1 && echo b`
const p3 = $h`echo c && sleep 0.1 && echo d`
const cat = $h`cat`
p1.pipe(cat); p2.pipe(cat); p3.pipe(cat)
await cat.run()
Formateadores de salida
const p = $`echo '{"foo":"bar"}\nline2'`
await p.json() // { foo: 'bar' }
await p.lines() // ['{"foo":"bar"}', 'line2']
await p.text() // '{"foo":"bar"}\nline2\n'
Iteración asíncrona
for await (const line of $`git log --oneline --max-count=5`) {
console.log(line)
}
Configuración del shell
zx usa bash por defecto. Cambia de shell según sea necesario:
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // PowerShell.exe
usePwsh() // PowerShell v7+
useBash() // Volver a bash
// Anulación manual
$.shell = '/bin/zsh'
En Windows, considera usar WSL o Git Bash para soporte de bash, o cambiar a PowerShell mediante usePowerShell().
Resumen de asistentes integrados
| Asistente | Propósito | Ejemplo |
|---|---|---|
cd() | Cambiar directorio | cd('/tmp') |
fetch() | Solicitudes HTTP, soporta .pipe() | fetch('https://api.example.com') |
question() | Entrada interactiva del usuario | question('Nombre: ') |
sleep() | Retrasar ejecución | await sleep(1000) |
echo() | Imprimir en stdout | echo`Estado: ${p}` |
stdin() | Leer stdin | JSON.parse(await stdin()) |
within() | Contexto de configuración asíncrono aislado | within(() => { $.cwd = '/tmp' }) |
retry() | Reintentar con retraso/backoff | retry(5, () => $curl url) |
spinner() | Indicador de progreso CLI | await spinner(() => $long-cmd) |
glob() | Coincidencia de archivos glob (globby) | glob('**/*.js') |
which() | Encontrar ruta de ejecutable | await which('node') |
ps | Lista de procesos multiplataforma | ps.lookup({ command: 'node' }) |
tmpdir() | Directorio temporal | tmpdir('sub') |
tmpfile() | Archivo temporal | tmpfile('f.txt', 'contenido') |
argv | Argumentos CLI analizados | argv.verbose |
dotenv | Carga de archivo .env | dotenv.config('.env') |
Paquetes npm expuestos: chalk (colores), fs (fs-extra), os, path, YAML (yaml), minimist.
Recursos
- references/api.md — Referencia completa de API: opciones de
$,cd(),fetch(),question(),sleep(),echo(),stdin(),within(),retry(),spinner(),glob(),which(),ps,kill(),tmpdir(),tmpfile(),minimist,argv,chalk,fs,os,path,YAML,dotenv,quote(),useBash(),usePowerShell(),usePwsh() - references/configuration.md — Todas las
$.options:shell,prefix,postfix,quote,verbose,quiet,env,cwd,timeout,nothrow,detached,preferLocal,spawn,kill,log,input,signal,stdio,halt,delimiter,defaults - references/cli.md — Uso de CLI: banderas, variables de entorno, scripts Markdown, scripts remotos, ejecución desde stdin, modo REPL
- references/process.md — Ciclo de vida de ProcessPromise/ProcessOutput, canalización, eliminación, cancelación, formateadores de salida, manejo de flujos