O código tá pronto, now run

now logo

Situação comum durante o desenvolvimento de software é entrar no Dilema Makefile/Shell Script. Afinal, existe sempre um conjunto de operações básicas que os desenvolvedores querem efetuar no projeto, seja rodar o projeto localmente, os testes unitários, compilar algo, análise estática, popular o banco com dados mockados, rodar migrations, pingar alguns serviços ou mesmo engatilhar um deploy. E a melhor opção para entregar isso geralmente é fazer uso do bom e velho make, que permite usar “subcomandos” (os targets do Makefile) e chamar comandos, tudo de maneira muito simples.

Repositório: https://github.com/now-run/now .

Entretanto, a partir de certo ponto isso mostra-se simples demais e logo pensa-se em criar alguns scripts shell para lidar com as tarefas que podem ser mais complexas. É questão de um for e um grep e já torna-se óbvio que escrever tal lógica dentro do próprio Makefile seria meio estranho.

E então cria-se um diretório bin/ na raiz do projeto para armazenar o diversos scripts — que nunca são apenas um ou dois, eles sempre acabam se multiplicando! Afinal, dois scripts que fazem tarefas similares sobre os mesmos dados (no caso, os arquivos do projeto, mais comandos e até processos) certamente compartilham algum tipo de configuração, certo? E aí surge o bin/settings.sh que todos os scripts devem “sourcear”. E por aí vai.

Agora alguns targets do Makefile chamam scripts de dentro de bin/ e o desenvolvedor começa a se perguntar se talvez não seria o caso de ter tudo como scripts shell — os que resistem a essa pergunta estão de parabéns. Mas implementar a funcionalidade aparentemente simples do make num script shell envolve switch/case, shift e mais alguns macetes para trabalhar com argumentos (aspas para todo lado!), o que torna os scripts auxiliares ainda mais complexos e ainda perde-se o autocomplete do shell.

Scripts auxiliares deveriam ter o nível de complexidade mais baixo possível, menor que dos testes unitários. Mas é muito fácil acabar com algo complexo ao nível do próprio algoritmo principal!

E existem, de fato, algumas ferramentas que prometem aliviar esse problema, mas eu, particularmente, nunca encontrei algo que fosse um substituto viável do famigerado make+bash.

Não que essas opções sejam horríveis. Mas sempre tem um gotcha que complica as coisas...

Now

Acontece que, eventualmente, quase por acaso, vindo de um turbilhão de ideias e outras motivações, acabou que, veja você, comecei a trabalhar numa ferramenta que chamei de now. A ideia do nome é semântica, mesmo, é você, desenvolvedor, clonar um repositório, adentrar o diretório raiz e poder dizer “agora rode”, ou now run. Ou “agora teste”, now test.

Para isso, algumas caixas precisam estar marcadas:

Ademais, percebo um problema recorrente em ferramentas que buscam atender esses cenários que é o “tentar ser mas não sendo”. Há coisas em que um bom shell é absolutamente a melhor solução e acho importante reconhecer isso. Ao invés de competir com o shell, por que não integrar-se muito bem com ele?

Inclusive esse é o outro dilema, o Dilema Shell Script versus Python (ou Ruby ou Perl ou qual seja a sua linguagem “de script” favorita). Não demora muito para chegar o momento em que sente-se que já se levou os shell scripts longe demais e que iria bem, além de sumir com a maioria dos problemas escondidos que o shell costuma manter (pense naquele nome de arquivo que tem um espaço no final), ter estruturas de dados fáceis de usar.

Só que ler e escrever e buscar coisas em arquivos e diretórios é tão mais fácil no shell...

Só que lidar com estruturas de dados, listas, dicionários, conjuntos, etc. é tão mais fácil no Python...

E algum transeunte empolgadinho dirá “é, mas tem esse e aquele pacotes Python que permitem lidar com essas coisas de maneira muito mais fácil”, ao que eu respondo:

  1. Não.
  2. Não perca de vista o cenário em que estamos.

Você realmente quer um requirements.txt específico para poder realizar operações no seu projeto? Você vai exigir que todos tenham a versão certa do Python ou, o que é ainda pior, exigir que todos tenham pyenv instalado e habilitado para poder simplesmente “buildar” as imagens Docker do seu projeto???

Não, né?

Logo, boa integração com shell e programas do sistema é essencial.

O Documento Now

Uma coisa interessante de Now é que não se escreve “programas”, mas “documentos”. A sintaxe de um documento Now é razoavelmente simples:

[Meu Primeiro Documento]

E aqui vai algum texto que explique o que esse documento deveria fazer ou representar.

O texto acima é um documento Now válido. Obviamente, ele também não “faz” nada além de carregar um título e um texto breve. Mas já é o suficiente para mostrar uma parte da sintaxe.

Sintaxe

Um documento Now é composto por seções. Cada seção tem:

  1. um título, como “Meu Primeiro Documento”;
  2. um cabeçalho, que no exemplo acima está vazio e
  3. um corpo, como o breve texto que vimos.

A divisão entre cabeçalho e corpo é como a divisão de headers e body de uma requisição HTTP: uma linha em branco, vazia, separa ambos.

[título]
cabeçalho

corpo

O título pode representar tanto um título mesmo, quando começa com letra maiúscula, quanto uma chave, quando começa com letra minúscula.

A definição de um procedimento nos ajuda a visualizar como uma seção mais “orientada a código” se parece:

[procedures/hello]
parameters {
    name {
        type string
        default "World"
    }
}

print "Hello, $name!"

Nessa seção temos um título ou chave, “procedures/hello”. O que isso significa é que o documento possui um chave “procedures”, que é um dicionário (dicionários em Now são sempre indexados por strings), que por sua vez contém uma chave “hello”, que é definida de acordo com o que vimos no exemplo.

Esta seção tem um cabeçalho. Cabeçalhos podem ser dicionários, como no exemplo, ou listas. Um dicionário-de-seção é composto por duplas chave-valor separadas por 1 espaço em branco, como em

chave valor

(A sintaxe poderia ser extremamente compatível com TOML e eu cheguei a tentar isso, mas a enorme quantidade de = no meio do documento é um negócio feio demais...)

Já o corpo do procedimento compõe um SubPrograma. Aí já não estamos lidando somente com estruturas de dados, mas com código mesmo. Now inclui uma linguagem razoavelmente simples que busca favorecer composição linear — ou seja: temos pipes!

Programando

Now oferece uma gama de ferramentas para que seu documento faça alguma coisa. São elas:

Commands

São os comandos que serão invocados pela linha de comando. Quando você chama now run, o comando chamado é run, que seria definido assim:

[commands/run]
parameters {
    # parâmetros esperados aqui
}

# code

Uma característica dos comandos é que eles podem ser chamados via linha de comando. São entrypoints e se a ideia é criar procedimentos reaproveitáveis para serem chamados por outras partes do código, deve-se criar... procedimentos.

Procedures

Um procedimento é como uma “função”. O nome não é “função” porque um dos objetivos de Now é que você pense mais num diálogo do que numa fórmula matemática.

[procedures/md5hash]
description "Return the md5 hash of a given string."
parameters {
    string {
        type string
    }
}

obj $string | md5 | return

System Commands

Now não tenta ser um shell. Para chamar comandos do sistema é necessário “envelopá-los” numa declaração específica. Veja um exemplo:

[system_commands/find_pattern_in_file]
description "Find occurrences of a given regexp in a given file"
parameters {
    regexp {
        type string
    }
    file {
        type string
    }
}
command {
    - grep
    - $regexp
    - $file
}
which "grep"

A ideia da chave which é poder saber ahead of time se algum comando está faltando. Descobrir isso no meio do runtime é algo extremamente frustrante em scripts shell. Já a ideia de encapsular é justamente poder isolar esse conhecimento muito específico que é a magia negra envolvida em cada comando shell em um nome mais amigável e com parâmetros mais claros.

É mais burocrático? Certamente. Mas no longo prazo acredito que é algo que se mostrará útil.

Shells e Scripts

Escrever um script bash dentro de um documento Now é muito simples. Seções representando scripts seguem em seus títulos o padrão shells/$nome_do_shell/scripts/$nome_do_script.

Se o shell em si for bash ou zsh, não é necessário declarar o shell. Do contrário, um novo shell define-se de maneira muito parecida com um “system_command”:

[shells/python]
description "Python 3 shell"
command {
    - python3
    - "-c"
    - $script_body
}

As variáveis disponíveis para a definição do shell são:

“shell_name” pode parecer confuso, a princípio, mas lembre-se que você pode definir N shells com nomes distintos chamando o mesmo comando, então pode ser o caso em que diferenciar um do outro seja uma necessidade.

O script em si é bastante simples:

[shells/bash/scripts/adjust_volume]
description "Change main audio channel volume by a given number of points."
parameters {
    points {
        type integer
    }
}

amixer -c 0 sset 'Master',0 $points;

Uma parte muito legal desse sistema de scripts é que você não precisa se preocupar em como as variáveis serão “inseridas” no script, porque elas não serão inseridas no script (a não ser que você adicione expand_variables true no cabeçalho!). Aquela string que representa o script é absoluta, total e completamente a mesma string que você vê, e o que Now faz é exportar os valores dos argumentos (como points) como variáveis de ambiente, tornando o manuseio desses valores muito menos complicado.

Ou seja: quando Now chamar bash, no caso acima, já o fará exportando points, então quem faz a expansão daquele valor ($points) é o próprio bash!

Event handlers

O exemplo acima é interessante porque esse comando, amixer, espera valores no formato 1+, 2+, 3-, etc. O valor é sufixado pelo sinal! E isso pode tornar o uso do nosso system_command extremamente propenso a erros.

A primeira alternativa que viria à mente do desenvolvedor seria encapsular a chamada ao system_command em um procedure, mas Now permite ajustar-se os parâmetros em um passo separado, um evento chamado on.call.

[shells/bash/scripts/adjust_volume/on.call]

obj $points : lt 0 : then {
    # if $points is lesser than zero:
    set p ($points * -1)
    set points "${p}-"
} {
    # or else:
    set points "${points}+"
}

Usando um procedure como exemplo (só porque o corpo é escrito na mesma linguagem), você pode pensar nesses event handlers dessa forma:

[procedure/p]
parameters {
    x {
        type int
    }
}

`on.call` é chamado aqui!

O corpo do procedimento

`on.return` é chamado aqui

Se ocorrer um erro, `on.error` é chamado.

Os event handlers, exceto on.error, compartilham do mesmo escopo que o procedimento ou comando. E funcionam da mesma forma tanto para comandos, procedimentos, system_commands e scripts.

Dessa forma, o que shells/bash/scripts/adjust_volume/on.call faz é ajeitar o valor de points para que o script em si possa funcionar da maneira esperada (5 -> 5+ e -5 -> 5-).

Templates

Esse tópico merece um artigo à parte, mas segue um breve exemplo:

[Now Document Example]

Show how the document syntax works.

[templates/html_base]
description "Basic HTML structure."

<html>
<head>
    <title>$page_title</title>
</head>
<body>
    % body %
    BODY
    % ---- %
</body>
</html>

[templates/htmls]
description "Display a list of files in HTML format."
extends html_base

<div class="files-list">
% body %
    <h2>Files in $directory</h2>
    <ul>
    % file %
        <li>$file</li>
    % --- %
    </ul>
% --- %
</div>

[commands/run]
description "Show how templates work."

template htmls (page_title = "Directory Listings Example") | as tpl

list "/home" "/opt" | foreach directory {
    obj $tpl : emit "body"
    path $directory : glob "*" | foreach file {
        obj $tpl : emit "file"
    }
}
obj $tpl : render | as rendered_template | print

Resumo

Na data em que esse artigo foi escrito (2023-05-14), Now nem está contando número de versão. Mas a linguagem em si já está bastante estável e as principais funcionalidades também. A ideia é ir aumentando os casos de uso para encontrar corner cases que precisem de ajustes, mas sem adicionar nada novo.

Além disso, é necessário documentar os comandos. Para conhecê-los, hoje é necessário ler o código fonte ou os exemplos que estão no repositório.

Mas, de qualquer forma, já é possível compilar localmente (basta ter o gdc instalado) e ir testando, se quiser.

:–)