Rakefile, parte 1 - publicar rascunho
Estou bem acostumado com Makefile, e se eu fosse customizar minhas ações com Rakefile? Vamos ver como seria?
O objetivo aqui é substituir o Makefile (que atualmente conta com duas ações) por um Rakefile, e de preferência também permitir outras automatizações que por enquanto não são Makefile também se tornarem Rakefile. Fazendo isso enquanto me baseio na documentação.
O artigo se dividirá nas seguintes seções:
- apanhado geral do Rakefile
- rodar o Jekyll
- fazer a ação de publicar (leia artigo Movendo de draft para post)
Na parte 2 teremos:
- regras e patterns simples
- criar um novo post (leia artigo Criando posts com Makefile)
- pegar menção de imagem (leia artigo Automatizando menção de imagem)
E por fim, na parte 3:
- iremos remover chamadas de bash e ficar apenas com ruby
Apanhado geral do Rakefile
Antes de mais nada, o Rakefile é um código Ruby. Portanto, qualquer coisa Ruby que você imaginar pode ser colocado.
Rakefile é dividido por algumas regrinhas para interação com a linha de comando. Entre elas:
task
: tarefas, multi propósito; se quiser algo realizado, queira uma tarefafile
: uma especificação de tarefa, mas para a geração de um arquivorule
: uma generalização defile
, mas com padrões, similar ao%
do Makefile
Uma task tem um nome. Existe a task especial :default
. Chamar o comando
rake
sem nada fará chamar a task :default
.
Opcionalmente, uma task pode ter uma lista de tasks as quais a task original depende.
Por exemplo:
task :default => :run
Aqui tá dizendo que a task default
depende da task run
.
Além disso, você pode dizer como se executa uma task:
task :hello do |t|
p 'hello'
end
Rodar o Jekyll
Meu primeiro objetivo é substituir o meu comando make
.
Atualmente, ele está assim:
run:
bundle exec jekyll s --drafts -w
Eu até posso invocar a gem diretamente, mas isso implica chamar a shell do sistema e outras indireções que eu não gostaria.
Bem, Jekyll é uma gem ruby, né? Então, o código dela é plausível de ser chamado programaticamente dentro do Ruby. No caso específico do Jekyll, ele usa Mercenary para lidar com a linha de comando, e eu já tive experiência com ela.
Poderia tentar fazer uma gambiarra e chamar o Mercenary? Bem, sim, mas não era a minha intenção. Eu queria uma chamada mais direta, e achei esta resposta no Stack Overflow que me deu a ideia de como fazer a engenharia reversa.
Depois de várias experimentações, o que me chamou a atenção de modo mais efetivo
foi essa linha exe/jekyll#L41
:
Jekyll::Command.subclasses.each { |c| c.init_with_program(p) }
Aqui ele está adicionando todas as subclasses de Jekyll::Command
para
inicializar o program
. Não fui atrás exatamente como que se faz para se
obter esses detalhes, mas fui atrás do comando
(lib/jekyll/commands/
)
que me interessava: serve
.
Como pegar esse comando? Bem, por sorte existe um
lib/jekyll/commands/serve.rb
.
E aqui peguei as seguintes informações:
- ele na real se chama
serve
(#L62
)prog.command(:serve) do |cmd|
- ele tem alias para
s
eserver
(#L65-66
)cmd.alias :server cmd.alias :s
- de fato ele chama uma função chamada
process_with_graceful_failure
passando como argumentosBuild
e, também,Serve
(#L86
)process_with_graceful_fail(cmd, config, Build, Serve)
Tá, mas e o que essa função faz? Bem, sinceramente, não liguei muito inicialmente
não. Vi que na resposta do Stack Overflow ele construía um
Jekyll::Configuration
e chamava o método process
. Para não ficar
apenas tentando adivinhar nas cegas, abri o irb
, fiz o require
da gem
e fiquei testando até encontrar alguma coisa interessante.
A primeira coisa interessante que encontrei era que a classe meio que funciona
como um singleton, diferentemente do que tem na resposta. Na
versão do Jekyll que ele usava, ele precisa instanciar o command
específico,
mas aqui não preciso disso. Outro ponto é que a configuração agora é passada para
o método específico de servir, não está na contrução do objeto.
Então, para chamar o Serve
, para uma configuração abstrata conf
:
Jekyll::Commands::Serve.process conf
Beleza, até aqui tudo bom. Mas… eu estava sentindo falta da capacidade
do Jekyll de fazer o build. Então, fui olhar a origem do
process_with_graceful_failure
:
def process_with_graceful_fail(cmd, options, *klass)
klass.each { |k| k.process(options) if k.respond_to?(:process) }
rescue Exception => e
raise e if cmd.trace
msg = " Please append `--trace` to the `#{cmd.name}` command "
dashes = "-" * msg.length
Jekyll.logger.error "", dashes
Jekyll.logger.error "Jekyll #{Jekyll::VERSION} ", msg
Jekyll.logger.error "", " for any additional information or backtrace. "
Jekyll.logger.abort_with "", dashes
end
O cmd
ele usa apenas como argumento para obter o nome do comando ou
para imprimir o stack trace em eventual exceção em alguma configuração.
De resto, ele investiga todas as clases passadas como argumento (note que
o último argumento é *klass
com asterisco, portanto indicando que
klass
é vararg).
Em cima de klass
, ele pergunta para cada argumento se ele é capaz de
atender à chamada do método .process
(if k.respond_to?(:process)
).
Se responder, então ele chama passando a configuração como
argumento (k.process(conf)
).
Então… vamos lá?
Se é para simular como o Serve
funciona…
Jekyll::Commands::Build.process conf
Jekyll::Commands::Serve.process conf
Mas… ele não fica observando para postar, nem posta rascunhos.
As opções que usei para lidar com isso na CLI foram -w
e --drafts
.
Essas seriam opções específicas do Serve
? Hmmm, não. Mas estão localizadas
no global
command
:
cmd.option "watch", "-w", "--[no-]watch", "Watch for changes and rebuild"
#...
cmd.option "show_drafts", "-D", "--drafts", "Render posts in the _drafts folder"
O mercenary lida com isso colocando na configuração um mapa, no caso de
configurações sem argumentos (como essas) ao ligar a configuração ele
coloca true
. Portanto…
conf = Jekyll.configuration({
'show_drafts' => true,
'watch' => true
})
Jekyll::Commands::Build.process conf
Jekyll::Commands::Serve.process conf
é… não saiu bem como esperado
Generating...
done in 7.051 seconds.
Auto-regeneration: enabled for 'C:/repos/computaria/blog'
Ao dar um ctrl+c
aparece a seguinte mensagem:
Server address: http://127.0.0.1:4000/blog/
Server running... press ctrl-c to stop.
Pelo visto alguma coisa não deixou satisfeito o Build
… Ao examinar
como o Build
responde ao .process
percebi que ele coloca um default
nas opções, opt["serving"] = false
na ação do Mercenary. E que no Serve
a ação do mercenary coloca justamente o contrário, opt["serving"] = true
.
Então fiquei curioso para ver o que acontecia ao chamar o 'watch' => true
e encontrei isto, no jekyll-watch
:
unless options["serving"]
trap("INT") do
listener.stop
Jekyll.logger.info "", "Halting auto-regeneration."
exit 0
end
sleep_forever
end
basicamente aqui ele indica que, caso opt["serving"]
não for verdade,
então ele vai entrar no laço infinito, quebrável justamente pelo
SIGINT
disparado pelo ctrl+c
.
Então, qual minha conclusão? Vamos tentar adicionar o "serving" => true
nas configurações:
conf = Jekyll.configuration({
'show_drafts' => true,
'watch' => true,
'serving' => true
})
Jekyll::Commands::Build.process conf
Jekyll::Commands::Serve.process conf
E pronto, com isso conseguimos fazer o site ficar se auto-construindo de modo suave.
Ação de publicar
Bem, a ação de publicar atualmente consiste em chamar o script bin/publish.sh
passado como argumento o arquivo a ser transformado em publicação, seja com
caminho completo. E antes ficava como argumento de CLI quem seria publicado.
Bem, posso continuar chamando o script. Assim como posso criar a ação :publish
e, dentro dessa ação, listar o que eu tenho para publicar e chamar o tal
script (ao menos por hora) com a informação selecionada.
Rapidamente numa procura por “terminal ui ruby” encontrei a gem
cli-ui
, e logo de cara ele tem um exemplo
com uma seleção:
CLI::UI.ask('What language/framework do you use?', options: %w(rails go ruby python))
Testando aqui, descobri que CLI::UI.ask
retorna a opção selecionada. Ele também
fornece como exemplo chamar alguma função de callback baseado na escolha.
No meu caso, quero listar todos os arquivos dentro de _drafts
. Descobri que
o ruby já fornece isso:
Dir["_drafts/*.md"]
Agora, só mapear para remover a extenção do final e o _drafts/
do começo e .md
do fim. Posso tacar uma regexp para sanar isso. Eis um experimento:
irb(main):012:0> "_drafts/abc.md".sub(/^_drafts\/(.*)\.md$/, '\1')
=> "abc"
Juntando tudo isso tenho:
Dir["_drafts/*.md"].map {|s| s.sub(/^_drafts\/(.*)\.md$/, '\1') }
Agora sim, pronto para por nas opções da task publish
do Rakefile:
task :publish do |t|
require "cli/ui"
draft2publish = CLI::UI::Prompt.ask('What language/framework do you use?', options: Dir["_drafts/*.md"].map {|s| s.sub(/^_drafts\/(.*)\.md$/, '\1') })
sh "#{"bash " if Gem.win_platform?}bin/publish.sh #{draft2publish}"
end
Como o script chamado é um script bash, precisei invocar a bash na mão no caso de se estar no windows.