Elmord's Magic Valley

Computers, languages, and computer languages. Às vezes em Português, sometimes in English.

Blueprints for a shell, parte 4: Ramblings on syntax

2015-03-17 01:10 -0300. Tags: comp, prog, pldesign, shell, lash, em-portugues

Este post é parte de uma série sobre o lash, um shell que eu estou ideando.

Hoje discutiremos algumas questões sintáticas do shell. Depois disso eu provavelmente vou dar uma pausa na série e tentar implementar um protótipo do lash, mesmo com algumas questões ainda em aberto. Em particular, falta falar sobre estruturas de controle (mas o básico (if, while, each) não tem muito o que discutir) e módulos (que vão ficar para o futuro).

O meu objetivo ao escolher a sintaxe do shell é achar um ponto de equilíbrio entre minimalismo sintático total (e.g., S-expressions1) e ter sintaxe especial para tudo (e.g., bash). No geral, o guiding principle é expor a maior parte das funcionalidades do shell por meio de funções, e usar sintaxe especial apenas quando seria inconveniente escrever uma chamada de função, especialmente para features freqüentemente usadas em modo interativo (e.g., redirects e pipelines). Este post é uma survey dos elementos sintáticos do (ba)sh e como eles serão representados em lash.

Comandos simples

A sintaxe básica de um comando em (ba)sh é, em BNF fuleiro:

command ::= {var=value}* {word | redirect}*

A semântica é: se há words no comando, a primeira word é o nome do comando a ser executado, e as demais são os argumentos. O comando é executado em um ambiente acrescido das variáveis de ambiente especificadas, e com os redirects em efeito. Se não há words, as variáveis especificadas (do shell, não de ambiente) recebem os valores atribuídos, e os redirects... bom, aparentemente não fazem nada, mas isso depende da variante de sh, porque o comportamento aparentemente é indefinido no padrão POSIX. A ordem de avaliação das coisas também é um pouco peculiar:

bash# a=$(date >&2) uname $(pwd >&2) 2>/dev/null
/tmp
Mon Mar 16 21:27:09 BRT 2015
Linux

dash# a=$(date >&2) uname $(pwd >&2) 2>/dev/null
/tmp
Linux

Vale notar que os redirects e as words podem aparecer intercalados na linha de comando (inclusive minha BNF está errada, porque redirects podem aparecer intercalados com as atribuições também); a ordem em que eles aparecem relativos aos outros elementos sintáticos parece ser irrelevante.

Em lash, depois de muita hesitação, eu decidi atirar pela janela as atribuições prefixadas; o comando env do Unix já serve para rodar comandos em um ambiente modificado (env FOO=bar comando). Eu pensei em obrigar os redirects a aparecerem no final, mas me dei conta de que pode ser útil escrever um redirect intercalado em comandos que recebem blocos. e.g.:

each_line </etc/passwd {|line|
    echo "bla bla $line"
}

Ainda não sei até que ponto isso pode ser útil, mas por enquanto fica aí. Fica a questão da ordem de avaliação. A remoção das variáveis prefixadas são uma coisa a menos na equação. Quanto ao momento em que os redirects tomam efeito, há algumas possibilidades:

  1. Antes de tudo, afetando inclusive chamadas a comandos com $(...), $[...] e companhia. Tem o detalhe de que o redirect em si também pode envolver avaliação (ls >$[generate-a-file-name]). Nesse caso o redirect evidentemente só pode ter efeito depois do comando.
  2. Depois da avaliação de tudo e imediatamente antes de executar o comando propriamente dito. Aparentemente é isso que o bash faz.
  3. O redirect afeta a avaliação de tudo o que aparece depois dele na linha de comando, i.e., 2>/dev/null foo $(bar) afeta a execução de bar, mas foo $(bar) 2>/dev/null não.

Por ora o plano é fazer como o bash, primariamente porque sim.

Fica ainda a questão da atribuição, já mencionada anteriormente: usar um comando para atribuição (set x = 42), ou tratar o = especialmente no parser? Eu não gosto muito de casos especiais, mas talvez a atribuição mereça tratamento especial. Eu nem sei se atribuição (por oposição a definição de uma nova variável) é particularmente freqüente em um script para justificar um caso especial.

Quoting

O bash possui uma porção de coisas quote-like:

O plano para o lash é:

Outra utilidade de strings com delimitador (semi-)arbitrário é que elas supririam a funcionalidade dos "here-documents" do bash, os quais veremos adiante.

Here-documents

Here-documents permitem embutir um trecho de texto, delimitado por uma string à escolha, a ser enviado para a entrada padrão (ou outro file descriptor) do comando a ser executado:

cat <<FIM >foo.txt
The quick brown fox
jumps over the lazy dog.
FIM

Por padrão, o shell realiza substituições no conteúdo do here-document. Se o delimitador for citado/escapado, o conteúdo é interpretado literalmente. Além disso, se o delimitador é precedido de -, espaços e tabs no começo de cada linha são descartados.

Em alguma versão o bash introduziu também "here-strings", que permitem usar uma string simples ao invés de um documento multi-linha como entrada:

sed 's/foo/bar/' <<<"$content"

Se o lash adotasse um mecanismo para strings com delimitadores (semi-)arbitrários, como a contra-aspa descrita anteriormente, seria possível unificar esses dois casos. Strings com delimitador arbitrário podem ser usadas também para inicializar variáveis, por exemplo, coisa que não é possível com here-documents em bash.

Parameter substitution

O bash possui uma dúzia de coisas da forma ${varsomething}, que permitem fazer alguma transformação sobre o valor de uma variável. Além de a sintaxe ser abstrusa, a string a ser manipulada tem que estar armazanada em uma variável (não pode ser o resultado de outra substituição, por exemplo; para aplicar múltiplas substituições é necessário armazenar os resultados parciais em uma variável). O plano em lash é substituir todas as substituições (heh) por funções.

Existe um pequeno problema envolvido: o bash distingue entre ${var//$match/$replacement} e ${var//"$match"/$replacement}. No primeiro caso, *, ? e similares dentro de $match têm seus significados de globbing, enquanto no segundo eles são interpretados literalmente. Esse problema afeta outras coisas que trabalham com patterns. No comentário linkado (que trata da função glob, que retorna uma lista dos arquivos que casam com um padrão), a solução que eu encontrei foi usar uma format string para separar as partes que devem ser interpretadas como pattern das partes que devem ser interpretadas literalmente (assim como printf em C separa a string de controle de strings incluídas com %s e que são usadas literalmente), mas no caso de substituições não sei se seria muito conveniente – talvez agrupando o pattern e seus argumentos em um array:

# Equivalente a ${string//"$match"*/"$replacement"} em bash.

subst $string ("%s*" $match) $replacement

Kinda weird, mas eu consigo sobreviver. Na verdade, acho que o melhor seria tratar o pattern como literal por padrão, senão certo que alguém vai escrever $[subst $var $match $replacement] sem nem pensar se $match contém asteriscos ou não, e aí vai ser outra daquelas situações em que um script funciona 99% do tempo, até que um dia alguém resolve usar uma string com * e o script tem um comportamento inesperado. A sintaxe de subst poderia ser:

Qual a sua opinião?

Outra situação que usa patterns e sofre do mesmo problema é o case, que a princípio há de ser um comando comum sem sintaxe especial (case STRING (PATTERN-1 BLOCO-1 ... PATTERN-N BLOCO-N)2). Idealmente a sintaxe adotada para as substituições deverá ser utilizada para o case também.

And, or, not

Em (ba)sh, comando1 && comando2 executa comando1 e, se este retornar 0 (i.e., verdadeiro), executa comando2. O exit status do comando como um todo é o exit status do último comando que for executado. Analogamente, comando1 || comando2 executa comando1 e, se este retornar não-zero (i.e., falso), executa comando2. Em ambos os casos, comando é um "comando completo", que pode envolver pipelines. Há dois casos de uso principais desses operadores:

Portanto, eles permanecem.

! nega o exit status do comando (troca de não-zero para 0 e de 0 para 1). Ele também se aplica a um "comando completo", negando uma pipeline inteira (o exit status de uma pipeline é o exit status do último comando), e essa seria a única razão que eu vejo para tratá-lo como sintaxe especial e não apenas um comando chamado !. Não sei se justifica; além de ser uma situação bem rara, nada impede de simplesmente escrever o ! antes do último comando da pipeline. Além disso, talvez fosse o caso de escrever ! {comando1 | comando2} anyway, por clareza. While we are at it, podíamos renomear o comando para not, para deixar mais claro que se trata de um comando comum e não sintaxe especial, mas aí já não sei.

Process substitution

Em bash, <(comando) cria um pipe (um par de file descriptors em que tudo que entra numa ponta sai na outra), executa comando com a saída padrão redirecionada para o lado entrante do pipe, e a expressão é substituída por um nome de arquivo que corresponde ao lado de saída do pipe. Por exemplo, é possível escrever:

diff <(sort file1) <(sort file2)

que executa sort file1 e sort file2 e chama algo como diff /dev/fd/63 /dev/fd/62. Analogamente, >(comando) executa comando com a entrada padrão vinda da ponta de saída do pipe, e a expressão é substituída por um nome de arquivo correspondente à ponta de entrada.

Embora essa sintaxe seja bastante conveniente para usar na linha de comando (e na verdade acho que o exemplo com o diff é o único que eu já usei na linha de comando na vida), não sei se eu quero mantê-la em lash. Não só pelo princípio de evitar sintaxe extra gratuita, mas também porque ela parece um redirecionamento, mas é uma word. Se eu quisesse redirecionar um file descriptor para o resultado do process substitution (o que é útil primariamente para fazer um pipeline com um file descriptor que não seja a stdout, e.g., redirecionar a stderr para um comando), eu teria que escrever algo como (o espaço é necessário):

ls 2> >(comando)

o que não é exatamente óbvio. Talvez uma função desse conta do recado, algo como:

diff $[popen -r {sort file1}] $[popen -r {sort file2}]

Ok, a cara disso é terrível3. Talvez se a popen ganhar outro nome, e o comando aceitar um nome de comando e argumentos diretamente ao invés de obrigatoriamente um bloco:

diff $[readfrom {sort file1}] $[readfrom {sort file2}]
diff $[pipefrom {sort file1}] $[pipefrom {sort file2}]
diff $[pipefrom sort file1] $[pipefrom sort file2]

Não sei.

Outro problema com a sintaxe do bash é que o comando parece um array, e talvez um array fizesse sentido como alvo do redirect (redirecionaria para todos os nomes de arquivo no array). Por outro lado, o caso do array poderia ser representado pelo array "spliced", qualquer que seja a sintaxe escolhida para ele (e.g., >$@(file1 file2)), ou simplesmente permitindo múltiplos redirects do mesmo file descriptor (>file1 >file2; o zsh permite isso, acho). Não sei.

Humanitas precisa dormir

Por hoje ficamos por aqui. Como sempre, tudo o que eu digo que "é" de tal jeito é só o plano atual, tudo está sujeito a discussão, comentários e sugestões são sempre bem-vindos, live free or die, do what you want 'cause a pirate is free, etc. Como esse é, a princípio, o último post da série for a while, sinta-se a vontade para comentar aqui sobre tópicos não abordados até agora na série.

_____

1 Em tempos de outrora eu pensei em usar S-expressions para toda a sintaxe (inclusive redirecionamentos e pipelines), mas permitir omitir os parênteses em torno de comandos que aparecem sozinhos em uma linha. O resultado não me foi exatamente satisfatório. Além disso, turns out que um shell totalmente baseado em S-expressions já foi feito (o qual por sinal provavelmente é uma boa fonte de inspiração).

2 Os patterns e blocos vão em um array primariamente para permitir que eles ocupem múltiplas linhas sem ter que pôr um \ no final de cada linha:

case $file (
    "*.mp3" { ... }
    "*.ogg" { ... }
    "*" { ... }
)

3 Revisando o post, eu olhei para isso e pareceu a sintaxe mais natural do mundo, mas a essa altura minha percepção já está meio alterada pelo sono.

Comentários / Comments (8)

Marcus Aurelius, 2015-03-17 12:14:46 -0300 #

`...`: sintaxe antiga

Hmpf. Essa juventude e suas sintaxes modernosas. No meu tempo era assim que a gente fazia e pronto! (Quando o $() foi adicionado? Vou pesquisar para ver se realmente vivi na época do `...` ou se apenas lia documentação desatualizada, hahaha)

aspas endinheiradas: valeu a leitura só por essa expressão.

> $[subst $var $match $replacement] sem nem pensar
> se $match contém asteriscos ou não

Vale a pena fazer um shell novo só por causa disso. Essa história de substituir a variável e depois processar outras coisas (globbing, word-splitting, sei lá) é uma das coisas mais incômodas, digo, idiotas mesmo do shell. É um potencial qualquer-coisa-injection em cada uso de $variável e exige contorcionismos cheios de aspas como: "$VAR", "$@", "x$VAR" == "x$VAR", etc.

Já perdi tempo demais estudando as diferenças entre $@, $*, "$@", "$*". E não lembro mais direito, só sei que apenas um deles faz o que eu normalmente quero. E deve ser o menos intuitivo.

Sobre $file*, me vieram tantas ideias conflitantes e reescrevi este comentário tantas vezes que já estou ficando confuso. Não ajuda nem um pouco o fato de eu ter lido teus posts apenas 1 vez cada (quase) sem reler pontos importantes...

A última idéia que surgiu são os "literais" de expansion, usando as aspas extensíveis de um jeito diferente:

* é um asterisco literal
`g[*] é um literal de glob, que será expandido. O delimitador (colchetes, parênteses, chaves, barras, etc.) pode ser flexível, desde que a abertura matche (haha) com o fechamento. Não precisa de backquote no final. Poderia fazer um caso especial para quando for só 1 caractere: `g*

my file = 'hello_world.c'
$file é 'hello_world.c'
$file* é 'hello_world.c*' (com asterisco)
$file`g* é 'hello_world.c', 'hello_world.c~', 'hello_world.cobol', etc.

my pattern = '*.txt'
$pattern expandiria para asterisco ponto tê xis tê.
`g[$pattern] expandiria para todos os arquivos tê-xis-tês.

O atual

ls *.txt{,~}

ficaria

ls `g*.txt`e{,~}

O "e" de "expansion" e já imagino literal de regexp: `m/foo(.*)bar/ pegando o "m" do Perl (se bem que a interpretação de regexp normalmente fica por conta do programa, como grep e sed, mas sei lá)

Claro, já vi que joguei pela janela a reconhecibilidade, compatibilidade, e quase tudo da digitibilidade (apesar de ter tentado consertar deixando flexível o uso de delimitadores e usando só 1 letra para definir o tipo de literal)


Marcus Aurelius, 2015-03-17 12:21:39 -0300 #

Bah, na refatoração esqueci de dizer um ponto principal:

Seria ótimo se $file* expandisse o $file (se tivesse asteriscos dentro, eles seriam interpretados literalmente) e fizesse globbing do * como no shell.

Mas quanto mais pensava, mais complicado ficava de explicar como funcionaria essa expansão no estilo "do what I mean", e foi daí que surgiu o literal de glob...


Marcus Aurelius, 2015-03-17 12:38:06 -0300 #

OK, em 1999 `...` já era chamado de "old-style". Não sou tão velho assim. Eu devia estar usando tcsh no ano 2000.


Vítor De Araújo, 2015-03-17 13:56:52 -0300 #

Tecnicamente o `...` nunca foi oficialmente declarado "deprecated", acho, mas faz tantos anos que eu não uso que eu já declarei como deprecated por conta. :P

E eu estava enganado quanto à possibilidade de nesting com contra-aspas: é possível usando \`, mas tem alguns problemas: http://pubs.opengroup.org/onlinepubs/009695399/xrat/xcu_chap02.html#tag_02_02_06_03

O que a gente quer é sempre "$@" (gera um argumento para cada parâmetro, não word-splita o conteúdo se contiver espaços, não expande nada, não faz globbing, etc). É muito raro eu ter que usar alguma das outras possibilidades. "$*" gera um argumento só com tudo, sem word-splitar/expandir o conteúdo. Sem aspas é a mesma coisa mas word-splita/expande, e nunca é o que ninguém quer. :P

Interessante a proposta de "flipar" o comportamento do globbing e marcar onde se *quer* que haja globbing, ao invés de fazer globbing e suprimir por default. Não sou exatamente fã da sintaxe, mas dá pra pensar sobre o assunto. :P

Mas acho que pelo menos o '*' tem que ser expandido por default, por conta do uso interativo. '?' eu já não sei; eu uso muito raramente, e normalmente quando uso um '?' na linha de comando eu quero um '?' literal (wget http://example.org/foo?bar=42). Mas o plano original era manter os globbings do sh (*, ?, [...]) intactos...

De qualquer forma, glob characters dentro de variáveis *não* serão expandidos, i.e., o exemplo do $file* faz o que tu queria (conteúdo de $file literal, * faz globbing). O problema surge quando se tem _funções_ que recebem patterns por default, porque depois de avaliados os argumentos, a função não tem como saber o que veio de uma variável e o que não veio, ou o que tinha aspas e o que não tinha, e além disso pode ser que eu _queira_ em alguns casos tratar o que veio da variável como pattern. O bash contorna esse problema porque as "variable substitutions" são sintaxe especial, então o shell tem como "ver" o que tinha aspas na linha de comando e o que não tinha. Uma solução meio weird seria ter um "literal de pattern", e.g.:

pat`"$file"*`

que gera um "pattern object" que carrega a informação de quais coisas são literais e quais não são. Inclusive o resultado da expansão podia ser simplesmente o array

("%s*" "$file")

o que unifica essa solução com a citada no post.

Uma vantagem da tua aspa extensível (com o "prefixo" depois e não antes do `) é que dá pra embutir uma citação extensível no meio de outra string, porque o ` marca o início da citação, enquanto na minha versão a citação a princípio tem que ser uma word by itself no comando.


Vítor De Araújo, 2015-03-17 14:05:23 -0300 #

Bá, vou ter que ler essa parte do POSIX de "rationales" do shell inteira. :P


Marcus Aurelius, 2015-03-17 15:55:03 -0300 #

> o exemplo do $file* faz o que tu queria
> (conteúdo de $file literal, * faz globbing)

Ótimo, vai ficar "supimpa"!

---8<---[ cut here ]---

> O problema surge quando se tem _funções_ que recebem patterns

Sim, sim, foi exatamente nesse momento que apaguei tudo que tinha escrito, hahaha.

---8<---[ cut here ]---

> dá pra embutir uma citação extensível

Mudei a posição do ` por causa disso mesmo, pra não precisar fazer ${file}glob`*`.

---8<---[ cut here ]---

O literal de pattern ficou muito bom para encapsular os detalhes sórdidos num lugar só e evitar o %s coisa-e-tal. Dá pra usar tranqẅilamente um pat`"$file"*` apesar de ter tantos tracinhos próximos uns dos outros — digitei um ẅ sem querer, mas ficou tão legal que não corrigi.

---8<---[ cut here ]---

Existe rationale dessas coisas? hehehe
Achei que o rationale fosse "porque não deu tempo de pensar melhor". Isso geralmente explica a maioria das coisas, inclusive JavaScript.

---8<---[ cut here ]---

Protótipo da implementação? Vou estar torcendo :-D


Vítor De Araújo, 2015-03-17 17:24:01 -0300 #

Os rationales das diferentes seções do POSIX é que são lindos:

> The following entries represent [...], or rationale
> for common variables in use by shells that have been excluded:
> [...]
>
> RANDOM
> This pseudo-random number generator was not seen as being
> useful to interactive users.

vs.

> This section contains rationale for some of the deliberations
> that led to this set of utilities, and why certain utilities were
> excluded.
> [...]
>
> su
> This utility is not useful from shell scripts or typical
> application programs.

I don't even. :P


Aldros Rux, 2015-04-02 15:18:41 -0300 #

Botswana for the gloruy! Younkow wtha ai say go to hell assinaeid jonzie


Deixe um comentário / Leave a comment

Main menu

Recent posts

Recent comments

Tags

em-portugues (213) comp (148) prog (71) in-english (62) life (49) unix (38) pldesign (37) lang (32) random (28) about (28) mind (26) lisp (25) fenius (22) mundane (22) web (20) ramble (18) img (13) rant (12) hel (12) scheme (10) privacy (10) freedom (8) esperanto (7) music (7) lash (7) bash (7) academia (7) copyright (7) home (6) mestrado (6) shell (6) android (5) conlang (5) misc (5) emacs (5) latex (4) editor (4) etymology (4) php (4) worldly (4) book (4) politics (4) network (3) c (3) tour-de-scheme (3) security (3) kbd (3) film (3) wrong (3) cook (2) treta (2) poem (2) physics (2) x11 (2) audio (2) comic (2) lows (2) llvm (2) wm (2) philosophy (2) perl (1) wayland (1) ai (1) german (1) en-esperanto (1) golang (1) translation (1) kindle (1) pointless (1) old-chinese (1)

Elsewhere

Quod vide


Copyright © 2010-2024 Vítor De Araújo
O conteúdo deste blog, a menos que de outra forma especificado, pode ser utilizado segundo os termos da licença Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International.

Powered by Blognir.