Criando executáveis com Thor
Leia em 6 minutos
Eu já falei sobre o uso do Thor para criar geradores em um artigo aqui do blog. Desta vez, vamos ver como ele pode ser usado para criar uma CLI.
O Ruby possui suporte para parsing de opções de CLI através da biblioteca OptionParser. No entanto, ela não é muito simples de usar quando sua interface é um pouco mais complicada.
Para criar um arquivo executável, primeiro defina uma classe que herde da classe Thor
; ela possui todas as facilidades para se criar uma CLI.
require 'thor'
module Troy
class Cli < Thor
end
end
No seu arquivo binário, execute essa classe como no código à seguir:
#!/usr/bin/env ruby
require 'troy'
Troy::Cli.start
O seu arquivo binário deve ter permissão de execução; você pode fazer isso com o comando chmod
.
chmod +x bin/troy
Pronto! Você já pode executar o script troy
.
$ ./bin/troy
Commands:
troy help [COMMAND] # Describe available commands or one specific command
Nosso comando ainda não faz nada. Vamos adicionar uma opção para exibir a versão de nosso programa. Para definir um novo switch, você precisa dar uma descrição que será utilizada na ajuda. Você também vai precisar especificar os switches aceitos com o método Thor.map
, especificando qual método deve ser executado.
module Troy
VERSION = '0.1.0'
class Cli < Thor
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
end
end
Se você executar o comando troy
, verá que o sub-comando version
será exibido na ajuda. Isso acontece porque todos os métodos públicos são considerados como sub-comandos. Se você não quiser disponibilizar um comando, basta definí-lo como privado.
$ ./bin/troy
Commands:
troy help [COMMAND] # Describe available commands or one specific command
troy version # Display Troy version
Agora você pode exibir a versão de seu programa com umas das três variações à seguir.
$ ./bin/troy -v
Troy 0.1.0
$ ./bin/troy --version
Troy 0.1.0
$ ./bin/troy version
Troy 0.1.0
Se você pretende criar um gerador, provavelmente irá implementar algo como troy new <caminho>
, que recebe argumentos posicionais. Neste caso, basta definir um método que recebe os argumentos que precisa.
require 'thor'
module Troy
VERSION = '0.1.0'
class Cli < Thor
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
def new(path)
path = File.expand_path(path)
say "Creating Troy site at #{path}"
end
end
end
Agora você pode executar o sub-comando new
. Caso o argumento não seja passado, uma mensagem será exibida.
$ ./bin/troy new foo
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/foo
$ ./bin/troy new
ERROR: "troy new" was called with no arguments
Usage: "troy new PATH"
Imagine que você quer permitir que o gerador seja configurado através de switches. O Rails faz isso constantemente; por exemplo, é possível especificar o banco de dados com rails new myapp -d postgresql
. Para definir opções de um comando, utilize o método Thor.option
.
require 'thor'
module Troy
VERSION = '0.1.0'
class Cli < Thor
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
say "Creating Troy site at #{path}"
say options
end
end
end
No exemplo anterior é possível especificar a engine de JavaScript com o switch --javascript-engine
, ou seu alias -j
. Isso mesmo; o Thor converte automaticamente o nome da opção com underscores para uma versão com hífen.
$ ./bin/troy new foo
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/foo
{"javascript_engine"=>"babeljs"}
$ ./bin/troy new foo --javascript-engine coffeescript
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/foo
{"javascript_engine"=>"coffeescript"}
$ ./bin/troy new foo -j coffeescript
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/foo
{"javascript_engine"=>"coffeescript"}
Se você quiser, pode especificar o tipo de cada opção, além de definir se é uma opção obrigatória ou não. Veja alguns exemplos de definição de opções.
option :file, :type => :array, :aliases => :files
option :force, :type => :boolean, :default => false
option :database, :required => true
Por padrão o Thor não valida quais opções foram passadas para um comando. Isso significa que embora o comando thor new
espere um argumento posicional, nós podemos erroneamente passar uma opção em seu lugar. Veja o exemplo abaixo:
$ ./bin/troy new -h
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/-h
{"javascript_engine"=>"babeljs"}
Perceba como o Thor pensou que o switch -h
fosse o argumento posicional. Para resolver este problema, basta fazer a validação das opções passadas com o método Thor.check_unknown_options!
.
module Troy
VERSION = '0.1.0'
class Cli < Thor
check_unknown_options!
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
say "Creating Troy site at #{path}"
say options
end
end
end
Se você executar o mesmo comando, verá uma mensagem de erro.
$ ./bin/troy new -h
Unknown switches '-h'
Imagine que você quer dar a opção de executar todos os comandos com um nível de verbosidade maior. Você pode fazer isso com o método Thor.class_option
.
module Troy
VERSION = '0.1.0'
class Cli < Thor
check_unknown_options!
class_option 'verbose', :type => :boolean, :default => false
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
say "Creating Troy site at #{path}"
say options
end
end
end
Se você quiser, agora pode definir a opção --verbose
.
$ ./bin/troy new foo --verbose
Creating Troy site at /Users/fnando/Projects/samples/thor-bin-sample/foo
{"verbose"=>true, "javascript_engine"=>"babeljs"}
Eventualmente você precisará validar os valores dos argumentos passados. Você pode inclusive querer interromper a execução, exibindo uma mensagem de erro. Para fazer isso, basta lançar uma exceção com a classe Thor::Error
. No exemplo à seguir, vamos validar se o diretório de output já existe ou não.
module Troy
VERSION = '0.1.0'
class Cli < Thor
check_unknown_options!
class_option 'verbose', :type => :boolean, :default => false
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
raise Error, "ERROR: #{path} already exists." if File.exist?(path)
say "Creating Troy site at #{path}"
say options
end
end
end
Agora, caso o diretório de output exista, a execução será interrompida e uma mensagem será exibida. No entanto, o código de saída do comando será definido como 0
, o que no mundo *nix significa sucesso.
$ ./bin/troy new cli.rb --verbose
ERROR: /Users/fnando/Projects/samples/thor-bin-sample/cli.rb already exists.
$ echo $?
0
Para resolver este problema, defina o método Troy::<abbr title="Command-Line Interface">CLI</abbr>.exit_on_failure?
, retornando sempre true
como resultado.
module Troy
VERSION = '0.1.0'
class Cli < Thor
check_unknown_options!
def self.exit_on_failure?
true
end
class_option 'verbose', :type => :boolean, :default => false
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
raise Error, "ERROR: #{path} already exists." if File.exist?(path)
say "Creating Troy site at #{path}"
say options
end
end
end
Ao executar o mesmo comando você verá que o código de saída agora é 1
.
$ ./bin/troy new cli.rb --verbose
ERROR: /Users/fnando/Projects/samples/thor-bin-sample/cli.rb already exists.
$ echo $?
1
Você pode inclusive fazer a saída do comando ser colorida. Basta usar o método set_color
.
module Troy
VERSION = '0.1.0'
class Cli < Thor
check_unknown_options!
def self.exit_on_failure?
true
end
class_option 'verbose', :type => :boolean, :default => false
desc 'version', 'Display Troy version'
map %w[-v --version] => :version
def version
say "Troy #{Troy::VERSION}"
end
desc 'new PATH', 'Create a new Troy static website'
option :javascript_engine, :default => 'babeljs', :aliases => '-j'
def new(path)
path = File.expand_path(path)
raise Error, set_color("ERROR: #{path} already exists.", :red) if File.exist?(path)
say "Creating Troy site at #{path}"
say options
end
end
end
Agora, a mesma mensagem de erro será exibida com a cor vermelha.
Finalizando
O Thor é a lib que mais gosto de usar para criar CLIs e geradores de arquivos. Embora existam outras alternativas, poder usar a mesma biblioteca que um projeto tão grande e importante como o Rails usa nos traz um nível de segurança e confiabilidade maior.
Vale lembrar que a melhor maneira de distribuir o seu executável e gerador de código é empacotando-o como uma gem. Para saber mais sobre como fazer isso, leia o artigo que publiquei aqui no blog.