Skip to content

Instantly share code, notes, and snippets.

@igorlima
Last active August 29, 2015 14:07
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save igorlima/9be600c9d4d906c7335d to your computer and use it in GitHub Desktop.
Save igorlima/9be600c9d4d906c7335d to your computer and use it in GitHub Desktop.

Usar uma API assíncrona para escrever scripts shell e linha de comando utilitário em NodeJS é um tanto inconveniente. Pelo fato da maioria dos scripts Shell serem executados na ordem de cima pra baixo; o que torna mais fácil bloquear a execução do script e torná-lo síncrono, ao invés de gerenciar o estado assíncrono com retornos de chamada (usando callbacks).

Felizmente, uma implementação nativa mais rápida vai estar disponível no Node v0.12 para processos filhos síncronos. Mas, isso também pode ser feito com a versão instável 0.11 (uma maneira de alternar de versão é usando o nvm).

Os scripts Shell conhecem o ecossistema npm

Pode-se dizer que só morrendo para saber qual será a próxima versão estável do Node (nesse caso podemos ficar de olho na versão 0.12). O pequeno script shell abaixo pode ser utilizado para verificar qual é a última versão estável disponível.

#!/bin/sh
curl "http://nodejs.org/" | grep "Current Version"   | awk '{ print substr($4,0,8) }'

Para executar o script acima, digite:

$ chmod +x check-node.sh
$ ./check-node.sh
v0.10.32

Isto pode ser simples para quem gosta ❤ do awk, mas o uso do grep e awk para analisar uma página web pode não ser uma boa idéia. Algo mais fácil seria usar a biblioteca "jQuery", usando um módulo Node como o cheerio. Também, o script acima utiliza comandos UNIX, o que provavelmente não funcionaria em outros ambientes. Abaixo há um exemplo do que pode ser feito utilizando processos filho síncrono:

var exec = require('child_process').execFileSync
var $ = require('cheerio')
 
var html = exec('curl', ['-s', 'http://nodejs.org/']).toString()
var version = $(html).find('.version')
                     .text()
                     .replace(/^.*\s(?=v)/,'')
console.log(version)

A função execFileSync é bastante útil neste caso por possibilitar a execução do comando curl de forma síncrona para (i) obter o conteúdo da página, (ii) converter o buffer para uma seqüência de caracteres utf8 e (iii) armazená-lo na variável html. Tudo isso em apenas uma linha, confira abaixo:

var html = exec('curl', ['-s', 'http://nodejs.org/']).toString()

Este exemplo terá o mesmo resultado que o primeiro script apresentado acima:

$ node check-node.js
v0.10.31

Tratamento de erros com o execFileSync

Se a execução do comando curl retornar um valor diferente de zero, a função execFileSync lançará uma exceção e a execução do programa será interrompida, o que geralmente faz todo sentido em se tratando de scripts shell. No entanto, se o erro tiver que ser tratado, este pode ser simplesmente capturado em um bloco try/catch:

#!/usr/bin/env node
var exec = require('child_process').execFileSync
var $ = require('cheerio')
 
try {
  var html = exec('curl', ['-s', 'http://nodejs.org/']).toString()
  var version = $(html).find('.version')
                       .text()
                       .replace(/^.*\s(?=v)/,'')
  console.log(version)
}
catch (er) {
   console.error(er.stack)
   if (er.pid) console.log('%s (pid: %d) exited with status %d',          
                                       er.file, er.pid, er.status)
}

Observe como as informações do processo foram impressas no terminal (como por exemplo o er.file). Esse objeto erro, que é gerado caso haja alguma exceção, é um objeto especial do processo filho síncrono, que contém as seguintes propriedades:

file     : String        Comando executado (caminho do comando se for 'execSync')
args     : String        Argumentos utilizados no comando
envPairs : Object        Pares chave/valor de variáveis ambientes
pid      : Number        Id do processo filho
output   : Array         Array dos resultados da saída 'stdio'
stdout   : Buffer|String O conteúdo de output[1]
stderr   : Buffer|String O conteúdo de output[2]
status   : Number        O código após o término da execução do processo filho
signal   : String        O sinal usado para matar o processo filho

Usando a função spawnSync

Na maioria dos casos, é suficiente (e recomendado) ter uma saída output de um comando e ter as exceções tratadas. No entanto, às vezes é bom inspecionar o processo filho sem ter que lançar nenhuma exceção. Como por exemplo, debugar a saída de canais como STDIO e STDERR, ou um outro canal como IPC.

Nesse caso, a função spawnSync pode ser usada para obter todo o objeto do processo filho síncrono. Para isso modifique o código para:

var spawn = require('child_process').spawnSync
var $ = require('cheerio')

var child = spawn('curl', ['-s', 'http://nodejs.org/'])
console.log('curl exited with status %d', child.status)
var html = child.stdout.toString()
var version = $(html).find('.version')
                     .text()
                     .replace(/^.*\s(?=v)/,'')
console.log(version)

Com a função spawnSync, já é possível ter acesso à saída do comando STDOUT, o que já permite trabalhar com os dados explicitamente. Também, é possível imprimir informações no terminal sobre o processo, independentemente de como o comando foi encerrado. Tenha em mente que é responsabilidade do desenvolvedor lidar com os erros, uma vez que as exceções não serão lançadas por conta própria.

Que tal usar execSync?

Como foi visto, o foco de um dos exemplos acima foi a função execFileSync. Comportamento que também pode ser obtido com a função execSync (ou seja, o STDOUT também é retornado, quando um erro é lançado). Uma distinção importante é que um executa o comando dentro de um shell. No caso do UNIX, utiliza o /bin/sh; já no Windows, usa o cmd.exe. Isto significa que é possível usar as peculiaridades oferecidas por cada shell (como pipe, redirecionamento e etc), mas por outro lado há a vulnerabilidade de ataques de injeção de script shell.

Veja abaixo os dois métodos lado a lado:

cp.execFileSync('curl', ['-s', 'http://nodejs.org/'])
cp.execSync('curl -s http://nodejs.org')

A ideia da API execFileSync é de: executar o programa (primeiro argumento) com uma lista de opções (segundo argumento). Já a API execSync é de: executar uma determinada linha de comando, como se estivesse em um prompt de um terminal.

Como sugestão, utilize o execFileSync (e execFile) sempre que possível e utilize o execSync (e exec) quando não for possível usar algumas funcionalidades adicionais do shell (geralmente pipe).

Conclusão

Processos filhos síncronos permitem escrever linha de comando utilitário de forma mais fácil, rápida e clara. Juntando essa facilidade e a abrangência de módulos npm disponíveis, é possível prever que mais pessoas vão utilizar o NodeJS para escrever script shell.

Agora, quem vai escrever um wrapper síncrono bacana com uma API semelhante a superagent ou request? Ou talvez o que precisamos é apenas um núcleo com módulo de rede síncrono...

Cuidado com a "sincronização”. Como as outras funções "síncronas" encontradas no módulo fs, processos filho síncrono são bloqueantes; Como o Node é single-threaded, caso haja um processo bloqueante, o Node não vai fazer mais nada até que esse processo termine. É aconselhado não usar métodos "síncronos" em nenhum contexto em que é preciso ficar respondendo chamadas de eventos (como solicitações de servidor web).

O que tem por vir?

  • O que está no lançamento do Node v0.12? Seis novos recursos, além de APIs novas e depreciadas.
  • Pronto para desenvolver APIs e deixá-las conectadas aos seus dados? Confira o framework LoopBack do NodeJS. E veja como que fica mais fácil começar uma API tanto localmente quanto na nuvem, simplesmente com um npm install.
  • Precisa de treinamento e certificação NodeJS? Saiba mais sobre ambos com as opções de ofertas da StrongLoop.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment