Zx — Escreva Scripts Shell Melhores com JavaScript
Visão Geral
zx é a ferramenta do Google para escrever scripts shell em JavaScript/TypeScript. Ela encapsula child_process, faz escape automático de argumentos e fornece padrões sensatos — dando a você o poder do ecossistema JavaScript em seus 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}` // Sem necessidade de aspas — escape automático
Bash é ótimo para tarefas simples, mas quando os scripts ficam complexos, uma linguagem de programação completa ajuda. zx adiciona encapsulamentos úteis ao redor de child_process, faz escape de argumentos e fornece padrões sensatos. Pense: bash + JavaScript em um único script.
Gatilhos
Também é acionado quando usuários perguntam sobre execução de comandos shell em JavaScript, conversão de scripts bash para zx, execução de scripts remotos, scripts Markdown ou scripts shell TypeScript.
Início Rápido
npm install zx
Escreva scripts como arquivos .mjs (suporta await de nível superior). Adicione o shebang #!/usr/bin/env zx ou execute via CLI:
zx ./script.mjs # Execução direta
npx zx ./script.mjs # Via npx
node --import zx/globals # Como loader do Node.js
Todas as funções ($, cd, fetch, etc.) estão globalmente disponíveis em scripts zx sem necessidade de importação. Para importações explícitas (melhor autocompletar do VS Code):
import 'zx/globals'
Conceitos Principais
$`comando` — Execute Comandos Shell
O template literal com tag é o coração do zx. Tudo dentro de ${...} é escapado e entre aspas automaticamente.
// Assíncrono (padrão) — retorna ProcessPromise
const output = await $`ls -la`
// Variante síncrona — retorna ProcessOutput diretamente
const dir = $.sync`pwd`
// Arrays são achatados
const flags = ['--oneline', '--decorate', '--color']
await $`git log ${flags}`
// Códigos de saída diferentes de zero lançam ProcessOutput
try {
await $`exit 1`
} catch (p) {
console.log(`Exit: ${p.exitCode}, Error: ${p.stderr}`)
}
Configuração Predefinida com $({...})
Crie instâncias $ personalizadas com opções predefinidas — encadeáveis e combináveis:
const $$ = $({ verbose: false, env: { NODE_ENV: 'production' } })
const pwd = $$.sync`pwd`
// Predefinições são encadeáveis
const $1 = $({ nothrow: true })
const $2 = $1({ sync: true }) // Ambas as opções nothrow + sync aplicadas
ProcessPromise e ProcessOutput
$`cmd` ProcessPromise (estende Promise)
├── .pipe() Encadeamento de streams
├── .kill() Encerra o processo
├── .text() Saída como string
├── .json() Saída como JSON analisado
├── .lines() Saída dividida por linhas
├── .nothrow() Suprime erros para este comando
├── .quiet() Suprime saída para este comando
├── .timeout() Auto-encerramento após duração
├── .stdio() Configura E/S
├── .exitCode Promise<código de saída>
├── .stdout Stream legível
├── .stderr Stream legível
├── .stdin Stream gravável
├── .pid / .cmd Metadados do processo
└── await → ProcessOutput
├── .stdout string
├── .stderr string
├── .exitCode número
├── .signal string|null
├── .text() / .json() / .lines() / .buffer() / .blob()
└── .ok booleano (quando nothrow)
Árvore de Decisão
Ao escrever scripts zx, use esta árvore de decisão:
| Objetivo | Abordagem |
|---|---|
| Executar um comando | await $`cmd` |
| Executar sincronamente | $.sync`cmd` |
| Encadear saída | .pipe($`proximo`) / .pipe('arquivo.txt') |
| Lidar com erros graciosamente | $({nothrow: true}) / .nothrow() |
| Definir tempo limite | $({timeout: '30s'}) / .timeout('30s') |
| Analisar saída JSON | (await $`cmd`).json() |
| Streaming em tempo real | for await (const line of $`cmd`) |
| Repetir em caso de falha | retry(5, () => $`cmd`) |
| Prompt para usuário | question('Nome: ') |
| Indicador de progresso | await spinner('Trabalhando...', () => $`cmd`) |
| Mudar diretório | cd('/caminho') ou within(() => { $.cwd = '/tmp' }) |
| Arquivos/diretórios temporários | tmpfile() / tmpdir() |
| Analisar argumentos CLI | argv.flag ou minimist(process.argv.slice(2)) |
| Carregar arquivo .env | dotenv.config('.env') |
Escrevendo Scripts zx Eficazes
Execução Paralela
const results = await Promise.all([
$`sleep 1; echo 1`,
$`sleep 2; echo 2`,
$`sleep 3; echo 3`,
])
Tratamento de Erros com 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('Erros:', errors.join('\n'))
Encadeamento de Streams
// Encadeie comandos como pipes do bash
const greeting = await $`printf "hello"`
.pipe($`awk '{printf $1", world!"}'`)
.pipe($`tr '[a-z]' '[A-Z]'`)
// Pipe para arquivo
await $`echo "Olá!"`.pipe('/tmp/saida.txt')
// Saída em tempo real para o terminal
await $`echo 1; sleep 1; echo 2; sleep 1; echo 3`.pipe(process.stdout)
Divisão e Mesclagem de Streams
// Dividir uma fonte para múltiplos consumidores
const p = $`algum-comando`
const [o1, o2] = await Promise.all([
p.pipe`log`,
p.pipe`extrair`,
])
// Mesclar múltiplas fontes
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()
Formatadores de Saída
const p = $`echo '{"foo":"bar"}\nline2'`
await p.json() // { foo: 'bar' }
await p.lines() // ['{"foo":"bar"}', 'line2']
await p.text() // '{"foo":"bar"}\nline2\n'
Iteração Assíncrona
for await (const line of $`git log --oneline --max-count=5`) {
console.log(line)
}
Configuração do Shell
O zx usa bash como padrão. Troque de shell conforme necessário:
import { useBash, usePowerShell, usePwsh } from 'zx'
usePowerShell() // PowerShell.exe
usePwsh() // PowerShell v7+
useBash() // Voltar ao bash
// Substituição manual
$.shell = '/bin/zsh'
No Windows, considere usar WSL ou Git Bash para suporte a bash, ou mude para PowerShell via usePowerShell().
Resumo de Helpers Integrados
| Helper | Propósito | Exemplo |
|---|---|---|
cd() | Mudar diretório | cd('/tmp') |
fetch() | Requisições HTTP, suporta .pipe() | fetch('https://api.exemplo.com') |
question() | Entrada interativa do usuário | question('Nome: ') |
sleep() | Atrasar execução | await sleep(1000) |
echo() | Imprimir no stdout | echo`Status: ${p}` |
stdin() | Ler stdin | JSON.parse(await stdin()) |
within() | Contexto de configuração assíncrona isolado | within(() => { $.cwd = '/tmp' }) |
retry() | Repetir com atraso/backoff | retry(5, () => $curl url) |
spinner() | Indicador de progresso CLI | await spinner(() => $comando-longo) |
glob() | Correspondência de arquivos glob (globby) | glob('**/*.js') |
which() | Encontrar caminho do executável | await which('node') |
ps | Lista de processos multiplataforma | ps.lookup({ command: 'node' }) |
tmpdir() | Diretório temporário | tmpdir('sub') |
tmpfile() | Arquivo temporário | tmpfile('f.txt', 'conteudo') |
argv | Argumentos CLI analisados | argv.verbose |
dotenv | Carregamento de arquivo .env | dotenv.config('.env') |
Pacotes npm expostos: chalk (cores), fs (fs-extra), os, path, YAML (yaml), minimist.
Recursos
- references/api.md — Referência completa da API: opções 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 as
$.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 da CLI: flags, variáveis de ambiente, scripts Markdown, scripts remotos, execução via stdin, modo REPL
- references/process.md — Ciclo de vida de ProcessPromise/ProcessOutput, encadeamento, encerramento, aborto, formatadores de saída, manipulação de streams