Usando vim como interpretador de código

Pra quem traballha com programação, escrever código pode representar grande parte das atividades do dia-a-dia. À medida que o tempo vai passando vamos escrevendo código e nem mais percebendo o que é necessário para rodar esss códigos.

Mas quando paramos para pensar nisso, independente da linguagem que usamos, tudo acontece meio que da mesma forma: O código que escrevemos deve ser interpretado por “alguém”, seja esse “alguém” um outro código (um interpretador, por exemplo) ou um hardware (um processador, por exemplo).

O Vim é um editor de texto extremamente extensível e isso acontece pois a forma de extendê-lo é através de uma linguagem de programação. Isso praticamente não te impõe limites no que é possível fazer.

A linguagem criada para extender o vim é chamada de vimscript ou vimL. É uma lingugem de programação como qualquer outra. Possui, dentre outras características:

  • Variáveis (locais, globais, somente-leitura, etc);
  • Condicionais (if, else, elseif);
  • Operações lógicas (>, <, >=, <=, ==, etc);
  • Expressoes matemáticas (+, -, *, /);
  • Expressões booleanas ((a + 30) > 5, etc);
  • Expressões bitwise (and(), or(), xor());
  • Funções (inclusive com númemro variável de parametros) e Ponteiros para funções (FuncRef);
  • Variáveis complexas: (List, Dict);
  • Comunicação assíncrona com outros processos (Channels);
  • Genrenciamento de sub-processos (job);
  • Exceptions (try/catch/endtry);
  • Funções built-in.

Alguns exemplos de funcções built-in:

  • Manupilação de strings (tolower(), toupper(), stridx());
  • Manipulação de Listas (len(), get(), empty(), insert(), add(), split());
  • Manipulação de Dicionários (get(), len(), has_key(), filter(), items());
  • Input/Output (input(), getchar(), confirm()), passagem de parametros na linha de comando (argc(), argv());
  • Assert funcions (assert_true(), assert_equal(), assert_exception());

Na massiva maioria das vezes (todas?) onde vemos vimL o código está sendo aplicado para extender o próprio vim, geralmente em forma de um plugin. Mas vimL é uma linguagem bastante completa e podemos escrever scripts genéricos, para uso geral assim como escrevemos bash ou python.

E é aqui que começa esse post. Vamos escrever um script simples e executá-lo como se fosse um script shell, mas o código estará escrito em vimL.

Anatomia de um script

Um script é composto geralmente apenas por código. E para rodá-lo precisamos de um interpretador que entenda esse código. Um exemplo simples de um script shell:

echo "Hello World"

E podemos rodar com qualquer shell que endenda esse código (pode ser bash, zsh, etc):

$ bash meu-script.sh
Hello World

Indicando o interpretador no código do próprio script

Existe uma forma de já pré-escolher qual será o interpretador usado para rodar nosso script. Essa forma é usando o que chamamos de shebang. O que fazemos é adicionar um comentário funcional na primeira linha do nosso script, assim:

#!/bin/bash
echo "Hello World"

A partir de agora podemos chamar nosso script apenas pelo nome, assim:

./meu-script.sh
Hello World

O que pouca gente sabe é que nessa linha podemos colocar o caminho de qualquer programa, inclusive de outro script. Porque não colocar ali uma chamada ao próprio vim?

É isso que vamos fazer!

Usando vim para rodar um script vimL

Vamos usar o seguinte script como prova de conceito:

echo "Hello World"

Gravamos esse código em meu-script.vim.

Olhando o exemplo acima (do shell script), pensamos automaticamente em fazer apenas:

$ vim meu-script.vim

O problema começa pois como o vim é originalmente um editor de texto, essa linha que chamamos vai na verdade abrir o vim e nos mostrar o conteúdo do nosso script, que é exatamente o que fazemos quando estamos usando o vim no dia-a-dia. Então como dizer ao vim que queremos, na verdade, executar o script?

O vim possui um flag, --cmd que permite que ele execute um comando qualquer. Então nossa primeira tentativa pode ser:

$ vim --cmd "source ./hello-world.vim"
Hello World
Press ENTER or type command to continue

De fato FUNCIONA, mas apenas parcialmente. Isso pois o vim executa nosso comando mas depois continua seu caminho normal, que é ser um editor de texto. Então aqui, depois de pertar ENTER acabamos com o vim aberto e não é o que queremos, já que queremos voltar ao terminal depois que nosso script terminar de rodar.

Podemos então adicionar :qall! no final do nosso script, isso vai fazer com que o vim feche automaticamente.

Nosso novo script fica assim:

echo "Hello World"
:qall!

E podemos rodar assim:

$ vim --cmd "source ./hello-world.vim"
Hello World

Nesse momento temos nosso primeiro script em vimL podendo ser rodado na linha de comando.

Rodando uma instância de vim sem nenhuma configuração personalizada

Apesar de termos conseguido rodar nosso script, temos ainda um problema. Essa instância de vim que estamos usando para interpretar nosso script está carregando configurações customizadas escolhidas pelo usuário. Isso pode ser muito ruim pois não temos controle sobre quais são essas configurações e elas podem influenciar na execução do nosso script.

Para rodar um vim sem nenhuma configuração, podemos usar a opção -u <file> que diz ao vim para usar o arquivo <file> como sendo o .vimrc. Assim nós conseguimos substituir toda e qualquer configuração feita pelo usuário.

Então nosso script pode ser rodado assim:

$ vim -u hello-world.vim
Hello World

Usando vim no shebang do nosso script

Uma forma de podermos chamar nosso script diretamente é colocar o vim no shebang do nosso script, assim:

#! vim -u
echo "Hello World"
:qall!

e a partir de agora podemos rodar nosso script diretamente:

$ ./hello-world.vim
Hello World

Exemplo de script um pouco mais complexo e novos problems que isso traz

Vamos escrever um script um pouco mais complexo. Vamos retornar a soma de todos os parametros passados na linha de comando.

Esse é o código:

#! vim -u

let s:sum = 0

for n in argv()
 let s:sum += n
endfor

echo s:sum

:qall!

Colocamos em soma.vim. E rodamos com:

$ ./soma.vim 10 20 30 40                        
4 files to edit
100

O vim sendo um editor de texto espera que seus argumentos sejam arquivos a serem editados e por isso coloca esse output junto com o output do nosso script:

4 files to edit

Infelizmente isso não é configurável, ou seja, sempre que você passar mais de 1 argumento para o seu script, essa frase vai aparecer. Mas já já veremos uma forma de driblar isso.

Quando o script está em uma linha de comando com pipe (|)

Um outro problema é quando rodamos nosso script em um linha de comando mais complexa, com pipe (|). Veja:

$ ./soma.vim 10 20 30 40 | cat                  
Vim: Warning: Output is not to a terminal
100
4 files to edit

Felizmente esse warning é configurável através da opção --not-a-term. Então basta adicionar essa opção em nosso shebang, certo? Na verdade não. Uma das regras é que o comando que está no shebang só pode receber um parametro. Veja o que acontece quando tentamos passar mais de um parametro:

#! vim --not-a-ter -u
echo "Hello World"
:qall!

Quando rodamos, vemos:

$ ./hello-world.vim
VIM - Vi IMproved 8.1 (2018 May 18, compiled Nov 11 2018 17:01:22)
Unknown option argument: "--not-a-ter -u"
More info with: "vim -h"

veja como o vim considera todos os parametros passados como sendo um só: "--not-a-ter -u". Aqui coloquei de propósito um parametro errado pois caso contrário nenhum erro é mostrado e o vim apenas abre o arquivo que estamos tentando executar.

Usando um segundo script como interpretador

Apesar do comando no shebang poder receber apenas um parametro é permitido colocar ali o caminho de um outro script. Então podemos fazer uma espécie de “wrapper” que vai montar a linha de comando de chamada do vim pra nós. Esse pode ser nosso wrapper:

#! /bin/bash

vim --not-a-term -u "$@" | sed '/files\ to\ edit$/d'

A partir de agora podemos usar esse script como sendo o “interpretador” do nosso script vimL. Mas ainda temos um problema, pois parece que tudo que o código vimL imprime vai para stderr. Então temos que adicionar 2>&1 em nosso runner, assim:

#! /bin/bash

vim --not-a-term -u "$@" 2>&1 | sed '/files\ to\ edit$/d'

Podemos então gravar esse código em /usr/local/bin/viml e usá-lo no shebang de qualquer script escrito em vimL.

#! /usr/bin/env viml

let s:sum = 0

echo 'Somando números: ' . join(argv(), ' + ')
for n in argv()
 let s:sum += n
endfor

echo 'Resultado: ' . s:sum

:qall!

Assim podemos rodar nosso script diretamente:

./run.vim 3 4 4 5 5 6                      
Somando números: 3 + 4 + 4 + 5 + 5 + 6
Resultado: 27

E podemos também usar nosso script em uma linha de comando com pipe:

./run.vim 3 4 4 5 5 6 | grep Res
Resultado: 27

E assim temos uma forma bem transparente para podermos escrever scripts de uso geral usando vimL como linguagem de programação e usando o próprio vim como interpretador desses scripts.