Quando se trata de desenvolvimento de software, a maioria das ferramentas disponíveis para nós são aplicações de linha de comando. Também vale a pena notar que muitas das ferramentas utilizadas na linha de comando são bastante poderosas no que podem realizar, desde as triviais até às tediosas. Levando isto além, você pode combinar aplicações de linha de comando para encadear uma seqüência de trabalho para atingir um resultado desejado. Aprender os comandos de linha de comando existentes permite aumentar sua produtividade e entender melhor quais capacidades estão à mão e quais tarefas você pode precisar implementar por conta própria.
Quando você projeta uma ferramenta de linha de comando, você realmente precisa prestar atenção a quem, ou o que, seu público-alvo é. Por exemplo, se todas as pessoas que vão usar esta ferramenta usam o mesmo sistema operacional e ambiente, então você tem a maior flexibilidade para tirar proveito de todo o ecossistema. Entretanto, se você precisar implementar seu aplicativo de linha de comando para trabalhar em vários sistemas operacionais, você agora restringiu as ferramentas que você pode usar ao que está disponível em todos esses sistemas — ou você tem que implementar exceções para cada sistema operacional em seu aplicativo. (Isso pode ser muito tedioso de pesquisar e testar para.)
A solução mais usada para lidar com diversos ambientes de SO é evitar qualquer ferramenta externa específica para SO quando possível ou delegar a responsabilidade a bibliotecas que já fizeram o trabalho duro de implementar recursos para trabalhar em múltiplas arquiteturas. Quanto mais você puder manter a implementação em uma linguagem central sem depender de dependências externas, mais fácil será manter seu projeto.
Os Componentes de uma Aplicação de Linha de Comando em Ruby
Existem três áreas de preocupação a serem abordadas ao construir uma aplicação de linha de comando: a entrada, saída e tudo o que estiver entre elas. Se você está desenvolvendo uma ferramenta única de linha de comando que simplesmente pega uma entrada ou fonte e a processa/formata e cospe a informação, então seu trabalho é bastante simples. Entretanto, se você estiver desenvolvendo uma interface de usuário com menus para navegar, as coisas começam a ficar mais complicadas.
Entrada de processamento
Para começar a processar a entrada, temos parâmetros que podem ser dados à sua aplicação. Estes argumentos são a forma mais típica de iniciar um comando, com detalhes de como você quer que ele seja executado e evitar a necessidade de sistemas de menus. Neste exemplo:
ruby -e "puts RUBY_VERSION"
Ruby é o programa de linha de comando sendo executado, -e
é chamado de flag, e "puts RUBY_VERSION"
é o valor dado para o flag. Neste caso, o flag do Ruby -e
significa executar o seguinte como código Ruby. Quando eu corro o acima na linha de comando, eu volto 2.4.0
impresso na saída padrão (que simplesmente mostra na próxima linha).
Entrada de documento
Entrada de documento, ou parâmetros, são todos os bits extras de texto inseridos após um comando e separados por um espaço. Praticamente todos os comandos permitem um argumento de marcação de ajuda. Um argumento de flag tem um traço ou dois à sua frente.
Os flags de ajuda padrão são -h
, e --help
. Nos dias do MSDOS, era (e ainda pode ser) /?
. Eu não sei se a tendência para flags no Windows continuou usando forward-slash como marcador de flag, mas scripts de linha de comando multi-plataforma hoje em dia usam traços.
Em Ruby, o input de uma linha de comando é colocado em duas variáveis diferentes: ARGV e ARGF. ARGV trata os parâmetros de entrada como um array de strings; ARGF é para tratar fluxos de dados. Você pode usar ARGV para parâmetros diretamente, mas isso pode ser mais trabalhoso do que você precisa fazer. Existem algumas bibliotecas Ruby construídas para trabalhar com argumentos de linha de comandos.
OptionParser
OptionParser tem a vantagem de ser incluído com Ruby, por isso não é uma dependência externa. OptionParser dá-lhe uma forma fácil de mostrar quais as opções de linha de comandos disponíveis e processar a entrada em qualquer objecto Ruby que deseje. Aqui está um excerto de uma das minhas ferramentas de linha de comandos dfm:
require 'optionparser'options = {}printers = Array.newOptionParser.new do |opts| opts.banner = "Usage: dfm \nDefaults: dfm -xd ." + File::SEPARATOR opts.on("-f", "--filters FILTERS", Array, "File extension filters") do |filters| options = filters end opts.on("-x", "--duplicates-hex", "Prints duplicate files by MD5 hexdigest") do |dh| printers << "dh" end opts.on("-d", "--duplicates-name", "Prints duplicate files by file name") do |dh| printers << "dn" end opts.on("-s", "--singles-hex", "Prints non-duplicate files by MD5 hexdigest") do |dh| printers << "sh" end opts.on("-n", "--singles-name", "Prints non-duplicate files by file name") do |dh| printers << "sn" endend.parse!
Neste exemplo, cada bloco opts.on
tem código para executar se o flag foi passado na linha de comandos. As quatro opções abaixo são (dentro do bloco deles) simplesmente anexando a informação do flag em um array para eu usar depois.
O primeiro tem Array
dado como um dos parâmetros para on
, então a entrada para este flag será convertida para um array Ruby e armazenada no meu hash chamado options
sob a chave filters
.
O resto dos parâmetros dados para o método on
são os detalhes do flag e a descrição. Tanto as bandeiras curta como longa funcionarão para executar o código dado no seguinte bloco.
OptionParser também tem as bandeiras -h
e --help
incorporadas por defeito, por isso não é necessário reinventar a roda. Basta digitar dfm -h
para a ferramenta de linha de comando acima, e ela fornece uma descrição útil:
Usage: dfm Defaults: dfm -xd ./ -f, --filters FILTERS File extension filters -x, --duplicates-hex Prints duplicate files by MD5 hexdigest -d, --duplicates-name Prints duplicate files by file name -s, --singles-hex Prints non-duplicate files by MD5 hexdigest -n, --singles-name Prints non-duplicate files by file name
Slop
OptionParser é um pouco verboso quando se trata de escrever as definições da linha de comando. O Slop gem é projetado para permitir que você escreva suas próprias definições de linha de comando com muito menos esforço. Ao invés de você fornecer os blocos de código na definição de flag, o Slop simplesmente cria um objeto que você pode consultar na sua aplicação para ver se um flag foi dado e que valor(es) pode(m) ter sido fornecido(s) para ele.
# Excerpt from https://github.com/leejarvis/slopopts = Slop.parse do |o| o.string '-h', '--host', 'a hostname' o.integer '--port', 'custom port', default: 80 o.bool '-v', '--verbose', 'enable verbose mode' o.bool '-q', '--quiet', 'suppress output (quiet mode)' o.bool '-c', '--check-ssl-certificate', 'check SSL certificate for host' o.on '--version', 'print the version' do puts Slop::VERSION exit endendARGV #=> -v --host 192.168.0.1 --check-ssl-certificateopts #=> 192.168.0.1opts.verbose? #=> trueopts.quiet? #=> falseopts.check_ssl_certificate? #=> trueopts.to_hash #=> { host: "192.168.0.1", port: 80, verbose: true, quiet: false, check_ssl_certificate: true }
Esta simplicidade pode ajudar a simplificar a sua base de código e a sua suite de testes, assim como acelerar o seu tempo de desenvolvimento.
Saída (STDOUT)
Quando escreve uma simples ferramenta de linha de comando, muitas vezes quer que ela produza os resultados do trabalho realizado. Tendo em mente qual é o alvo para esta ferramenta irá determinar em grande parte como você quer que a saída seja.
Você poderia formatar os dados de saída como uma simples string, lista, hash, array aninhado, JSON, XML, ou outra forma de dados a serem consumidos. Se seus dados vão ser transmitidos por uma conexão de rede, então você vai querer empacotar os dados em uma string de dados bem empacotada. Se é para os olhos de um usuário ver, então você vai querer expandi-lo de uma forma apresentável.
Muitas ferramentas de linha de comando Linux/Mac existentes podem imprimir detalhes em pares ou conjuntos de valores. As informações podem ser separadas por dois pontos, espaços, abas e blocos de recuo. Quando você não estiver certo de como exatamente pode ser usado, vá para a maneira mais simples e apresentável de apresentar os dados.
Um exemplo de um alvo que você pode precisar considerar é uma ferramenta de teste de API. Muitas APIs fornecem uma resposta JSON e podem ser acessadas com uma ferramenta de linha de comando como curl
. Se a largura de banda é uma preocupação, então use o método JSON’s to_json
, mas se for para o trabalho da máquina local, use pretty_generate
.
x = {"hello" => "world", this: {"apple" => 4, tastes: "delicious"}}require 'json'puts x.to_json# {"hello":"world","this":{"apple":4,"tastes":"delicious"}}puts JSON.pretty_generate( x )# {# "hello": "world",# "this": {# "apple": 4,# "tastes": "delicious"# }# }
Você também pode usar YAML para dados.
require 'yaml'puts x.to_yaml# ---# hello: world# :this:# apple: 4# :tastes: delicious
Se você quiser ter uma saída mais complexa bem formatada, então eu recomendo altamente o uso da fantástica_impressão gem. Isto lhe dará controle específico da sua apresentação na saída.
Erro Padrão (STDERR)
Este é outro tipo de saída que pode ocorrer a partir de aplicações de linha de comando. Quando algo está errado ou deu errado, é costume ter a ferramenta de linha de comando escrevendo para o output conhecido como STDERR. Ela aparecerá como saída regular, mas outras ferramentas podem verificar que o comando não foi bem sucedido.
STDERR.puts "Oops! You broke it!"
A prática mais comum é usar um Logger para escrever detalhes específicos sobre erros. Você pode encaminhar isso para a saída STDERR na linha de comando ou possivelmente um arquivo de log.
Interface de usuário (STDIN e STDOUT)
Escrever uma interface de usuário pode ser uma das coisas mais gratificantes de se realizar. Ela permite que você aplique algum design artístico e fornece diferentes maneiras para um usuário interagir.
A interface mínima de usuário é uma exibição de algum texto com um prompt à espera de entrada. Pode parecer tão simples como:
Who is your favorite super hero /Batman/Wonder-Woman ?
O acima indica a pergunta, as opções, e uma opção padrão caso o usuário opte por pressionar a tecla enter sem entrar nada. Em Ruby simples, isto pareceria:
favorite = "Superman"printf "Who is your favorite hero /Batman/Wonder Woman?"input = gets.chompfavorite = input unless input.empty?
Este é um código muito rude para ser usado para entrada e saída, e se você vai estar escrevendo uma IU, eu recomendo altamente que você experimente uma gem como highline ou tty.
Com a gema highline, você poderia escrever o acima como:
require 'highline/import'favorite = ask("Who is your favorite hero Superman/Batman/Wonder Woman?") {|question| question.in = question.default = "Superman"}
Aqui, highline mostrará a pergunta, mostrará o padrão, e pegará qualquer opção incorreta digitada, notificando o usuário que ele não selecionou uma das opções fornecidas.
Both highline e tty têm uma grande quantidade de coisas adicionais que você pode fazer para um menu e uma experiência do usuário, com gentilezas como adicionar cores ao seu display. Mas mais uma vez você precisa levar em consideração quem é seu público-alvo.
Quanto mais experiência visual você fornecer, mais você precisa prestar atenção às capacidades de cross-plataforma dessas ferramentas. Nem todas as linhas de comando lidam com os mesmos dados de apresentação da mesma forma, o que cria uma má experiência do utilizador.
Compatibilidade multiplataforma
A grande novidade é que o Ruby tem muitas das soluções necessárias para a utilização de ferramentas em diferentes sistemas operativos. Quando o Ruby é compilado para qualquer sistema operacional específico, o arquivo fonte do RbConfig é gerado com valores absolutos nativos do sistema em que foi construído, salvo no formato hash. É portanto a chave para detectar e usar os recursos do SO.
Para ver este arquivo com seu editor de texto favorito, você pode executar o seguinte código Ruby:
editor = "sublime" # your preferred editor hereexec "#{editor} #{RbConfig.method(:ruby).source_location}"
Isto irá mostrar tudo de uma maneira apresentável, o que eu sinto que é melhor do que ver o hash via RbConfig::CONFIG
. Este hash inclui coisas úteis como os comandos usados para lidar com o sistema de ficheiros, versão Ruby, arquitectura do sistema, onde tudo o que é importante para o Ruby reside, e outras coisas como esta.
Para verificar o sistema operacional, você pode fazer:
case RbConfig::CONFIGwhen /mingw32|mswin/ # Probably a Windows operating systemelse # Most likely a Unix compatible system like BSD/Mac/Linuxend
Para outros valores específicos do sistema operacional, você pode olhar o código fonte para Gem::Platform.
Agora, os comandos específicos do sistema de arquivos armazenados aqui não são destinados a serem usados a partir deste recurso. Ruby tem as classes Dir, File, e Pathname escritas para isso.
Quando você precisar saber se um executável existe no PATH do sistema, então você vai querer usar MakeMakefile.find_executable. O Ruby suporta a construção de extensões C e uma das funcionalidades boas que eles adicionaram para isso é esta capacidade de descobrir se o executável existe para chamar.
Mas isto irá gerar um ficheiro de log no directório actual do seu sistema cada vez que o executar. Então para evitar escrever esse arquivo de log, você precisará fazer o seguinte:
require 'mkmf'MakeMakefile::Logging.instance_variable_set(:@log, File.open(File::NULL, 'w'))executable = MakeMakefile.find_executable('clear') # or whatever executable you're looking for
Quando você desenha um menu visual em uma linha de comando, é preferível que o menu fique em uma posição fixa quando o display for atualizado. A maneira mais simples de fazer isso é limpar a linha de comando entre cada escrita do display completo.
No exemplo acima, eu procurei pelo comando clear
. No Windows, o comando shell para limpar a linha de comando é cls
, mas você não será capaz de encontrar um executável para ele porque ele é parte do código interno de command.com
.
A boa notícia é que pegando a saída de string do comando clear
do Linux produz esta seqüência de código de escape: \e[3J\e[H\e[2J
. Eu e outros testamos isso no Windows, Mac e Linux, e isso faz exatamente o que queremos: limpar a tela para redesenhar nela.
Esta seqüência de escape tem três ações diferentes que está executando:
CLEAR = (ERASE_SCOLLBACK = "\e[3J") + (CURSOR_HOME = "\e[H") + (ERASE_DISPLAY = "\e[2J")
No entanto, é preferível usar uma biblioteca de linha de comando em vez de escrever códigos de escape você mesmo. Mas esta vale bem a pena mencionar.
Testing STDIN/STDOUT/STDERR
Com qualquer coisa que escreva em algo como STDOUT ou STDERR, seria mais sensato criar uma variável interna no seu objecto UI Ruby que pode ser alterada com um parâmetro opcional para new
. Assim, quando o seu programa normalmente corre, o valor será STDOUT
. Mas quando escreve testes, passará em StringIO.new
, que pode facilmente testar contra.
Quando tenta ler IO a partir de um comando executado fora de Ruby, o teste está um pouco mais envolvido. Você provavelmente precisará de olhar para o Open3.popen3 para lidar com cada stream IO STDIN, STDOUT, e STDERR.
Usando comandos Bash
Bash chegou ao Windows (WSL)! Isto significa que tem acesso a comandos bash na grande maioria dos sistemas operativos utilizados em todo o mundo. Isto abre mais possibilidades para a sua própria ferramenta de linha de comando.
É típico dos comandos “pipe” entre si, enviando a saída de um comando como um stream para o próximo. Sabendo disso, você pode querer considerar adicionar suporte a entrada de streaming, ou pensar em como formatar melhor a saída para o consumo de outras ferramentas de linha de comando.
Aqui está um exemplo de uma substituição regex com o comando Bash’s sed
recebendo seu input piped de echo:
echo hello | sed "s/ll/ck n/g"
Esta saída heck no
. Quanto mais você aprender sobre o Bash, melhor habilitado você estará para fazer as coisas em uma linha de comando e melhor equipado você estará para escrever melhores ferramentas de linha de comando próprias.
Sumário
Há muito a aprender quando se trata de escrever suas próprias ferramentas de linha de comando. Quanto mais situações você tiver que explicar, mais complexas as coisas se tornam.
Tire o tempo para aprender as ferramentas que você tem em mãos, e você vai colher muitas recompensas que você não sabia que estava faltando. Desejo-lhe tudo de bom!