3 pontos por GN⁺ 2023-11-24 | 1 comentários | Compartilhar no WhatsApp
  • Em sistemas da família Unix, pode existir um executável chamado /bin/[, com nome de um único caractere simbólico, e a sintaxe que parece uma expressão condicional do shell na verdade se apoia na execução de comandos e em códigos de saída
  • test avalia expressões e retorna 0 quando o resultado é verdadeiro e 1 quando é falso; quando invocado como [ também verifica se o último argumento é ]
  • Muitos shells também fornecem test e [ como comandos internos, então as mensagens de erro e o comportamento podem diferir entre o /bin/test externo e a implementação embutida no shell
  • A extensão do Bash [[ não é um comando externo, mas uma construção interna, então aplica regras diferentes de [; por exemplo, como no exemplo, long* não sofre expansão de glob e é comparado como string literal
  • Em scripts portáveis, o certo é usar [, e se o script for exclusivo de Bash, costuma ser melhor usar [[ de forma consistente, mas é preciso escolher sabendo das diferenças nas regras de expansão entre os dois

O que são /bin/[ e /bin/test

  • Em sistemas Unix, pode existir um executável chamado /bin/[ com nome de um único caractere simbólico
    • O comando de exemplo ls /bin/? mostra /bin/[
  • /bin/[ e /bin/test podem apontar para o mesmo binário
    • No exemplo, os dois caminhos aparecem como arquivos com o mesmo inode e o mesmo tamanho
    • Porém, isso não precisa ser um hard link em todos os sistemas
  • test é um programa do shell para avaliar expressões
    • comparação de strings
    • comparação de números
    • verificação de condições de arquivos
  • Se a avaliação for verdadeira, ele retorna o código de saída 0; se for falsa, retorna 1

Por que [ funciona como um comando

  • test a = b não parece muito uma expressão condicional, mas a mesma lógica em [ a = b ] fica em um formato mais familiar
  • [ parece uma sintaxe própria, mas na prática é uma invocação de comando
    • if [ a = b ]; then ... fi executa o comando [ e verifica seu código de saída
    • Quando chamado como [, o programa verifica se o último argumento é o colchete de fechamento ]
  • A instrução if não interpreta a condição diretamente; ela decide com base no código de saída do comando recebido
    • test a = a; echo $? resulta em 0
    • test a = b; echo $? resulta em 1
    • [ a = a ]; echo $? resulta em 0
    • [ a = b ]; echo $? resulta em 1
  • No mesmo contexto, true e false também podem ser vistos como binários auxiliares que retornam códigos de saída

Diferença entre binários externos e comandos internos do shell

  • Como test e [ são muito usados em scripts de shell, a maioria dos shells também os implementa como comandos internos
  • Mesmo com a mesma entrada, a saída do binário externo e a do comando interno do shell podem ser diferentes
    • /bin/test a b produz test: a: unexpected operator
    • test a b produz dash: 2: test: a: unexpected operator
  • Essa diferença pode ocorrer não só com test e [, mas também com comandos aparentemente simples como echo
  • Como a implementação interna varia entre shells, o comportamento do script pode mudar dependendo do shell usado para executá-lo

As regras próprias aplicadas pela extensão [[ do Bash

  • [[ é uma extensão do Bash e pode substituir o uso de [
  • A maior diferença é que [[ é sempre uma construção interna
    • Ao contrário de [, que pode ser executado como binário externo, em [[ o Bash pode mudar as regras da linguagem dentro da expressão
  • No exemplo com glob, [ e [[ se comportam de forma diferente
    • Depois de touch long-name, [ long* = long-name ] && echo match imprime match
    • Aos argumentos do comando [ se aplicam as regras normais de expansão do shell, então long* é expandido para long-name no diretório
    • [[ long* = long-name ]] && echo match não imprime nada
    • [[ trata long* como string literal e a compara diretamente com long-name, então a comparação falha
  • Em scripts exclusivos de Bash, [[ também permite usar recursos como correspondência por expressão regular com =~

O que escolher em scripts

  • Em scripts de shell portáveis, o mais adequado é usar [
  • Também é possível usar test, mas não é a escolha mais comum
  • Se o script for exclusivo de Bash, é melhor usar [[ de forma consistente
  • O próprio shell também tem operadores de expressão como !, && e ||
    • Esses operadores funcionam com base no status de saída dos comandos
    • grep ^hello$ ... && grep ^bye$ ... faz com que o código de saída total seja 0 se ambos os comandos tiverem sucesso
    • Se o primeiro grep falhar, o comando depois de && também não poderá fazer o conjunto ter sucesso, então o código de saída total será 1
  • Por isso, é possível combinar expressões test/[ com os operadores lógicos do shell dentro de uma mesma condição
    • Exemplo: [ a = b ] || grep -q ^hello$ /usr/share/dict/words
  • O POSIX não exige que /bin/[ e /bin/test sejam hard links
    • No NetBSD, eles eram hard links
    • O macOS Catalina fornece cópias separadas do mesmo binário
    • O Debian testing fornece binários diferentes
    • A especificação POSIX não exige que os dois arquivos sejam links

1 comentários

 
GN⁺ 2023-11-24
Opiniões do Hacker News
  • Sou o autor do texto original. Obrigado por compartilhar, e fico feliz que tenha chegado à página inicial. O título provavelmente deveria ter (2020), e "test" se refere de fato ao comando, então é melhor não capitalizar
    Também escrevi um texto relacionado em 2021, que aborda até o operador [[ do bash, então acho que pode ser uma leitura interessante neste contexto: https://jmmv.dev/2021/08/useless-use-of-gnu.html

    • [[ não é, estritamente falando, um comando embutido, mas fundamentalmente algo mais próximo de um elemento de sintaxe. Talvez internamente use um comando embutido quase inacessível, mas o interessante é que ]] também é uma palavra reservada, embora não possa aparecer em uma posição em que uma palavra reservada tenha significado
      Em alguns shells que não são bash, a palavra-chave function é necessária para declarar certos tipos de função. O $(shell) do make pode apresentar uma diferença de desempenho mensurável ao compilar muitos alvos. Ainda assim, quando ele não faz nada, é um custo desnecessário; normalmente, se a intenção é forçar a regeneração, o correto é usar include. A GNU ignorar o POSIX é totalmente razoável, já que o POSIX não é muito útil para resolver a maioria dos problemas reais
    • Muitas das extensões GNU tratadas em https://jmmv.dev/2021/08/useless-use-of-gnu.html são muito úteis no uso interativo. Procurar no diretório atual sem um . explícito também é útil, e acrescentar opções ao comando recém-digitado é realmente conveniente. Sempre me irrito quando vejo comandos que não dão suporte a isso
      Em scripts, geralmente faz sentido se limitar ao POSIX sh. No mínimo, é preciso saber se você está usando sintaxe específica do Bash
    • O texto reclama que usar extensões GNU como --ignore-case e set -o pipefail reduz a portabilidade dos scripts. Isso, em si, está correto
      Mas ele não explica por que usuários de Linux deveriam se preocupar muito com portabilidade. OpenBSD e FreeBSD seguem vivos, mas têm tão poucos usuários que não parecem ser algo com que valha se preocupar especialmente. Por uma questão de justiça, dá para dizer que esses sistemas operacionais também deveriam ser considerados, mas onde esse critério deveria parar? Também deveríamos considerar algo obscuro como vxWorks? O caso do BusyBox e do Alpine é mais interessante, mas as mudanças são tão grandes que quase sempre é necessário fazer um porte separado de qualquer jeito. Há algum outro motivo convincente para se preocupar com ecossistemas que não são GNU?
    • Algo como if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; fi não é simplesmente um uso cotidiano de shell bem comum?
    • Como observação secundária, parece que o software do blog estragou o título. Por exemplo, ele aparece como make $(shell …) expansion, mas deveria ser make $(shell ...) expansion
      Como no corpo do texto está escrito corretamente com três pontos, e não com um único caractere de reticências, o próprio mldr também não está correto. Parece que dois bugs independentes acabaram afetando isso ao mesmo tempo
  • Levando o último ponto um passo além, dá até para eliminar o próprio bloco if. if [ a = b ]; then echo "Oops!"; else echo "Expected; phew!"; fi vira [ a = b ] && echo "Oops!" || echo "Expected; phew!"
    Não sei com que frequência se deve fazer isso, mas às vezes é útil para imprimir saída de depuração condicionalmente no erro padrão, como em [ "$debug" ] && echo "what's going on" >&2. O fato de o bloco if verificar comandos comuns também permite coisas como if grep -q 'debug' /var/log/nginx/access.log; then echo "Debug request found!"; fi. O que ainda não investiguei é se devo escrever [ $(expr 1 + 1) -eq 2 ] && [ $(expr 2 + 2) -eq 3 ], ou usar o AND lógico embutido de test, como em [ $(expr 1 + 1) -eq 2 -a $(expr 2 + 2) -eq 4 ]. Se desempenho não for um problema, ambos parecem válidos por motivos parecidos

    • [ a = b ] && echo "Oops!" || echo "Expected; phew!" não deve ser tomado como regra geral. O bash provavelmente interpreta essa linha como ([ a = b ] && echo "Oops!") || echo "Expected; phew!"
      Então, se a sequência de comandos depois de && falhar, o código depois de || será executado de qualquer forma. Por exemplo, se >/dev/full echo "strings match" falhar com erro de escrita, "strings don't match" será impresso mesmo que as strings fossem iguais. Isso difere do significado de um bloco if
    • É melhor evitar esse tipo de abreviação. Se você estiver usando o set -e, como deveria, if [ a = b ]; then echo "Oops!"; fi funciona como esperado, mas [ a = b ] && echo "Oops!" termina com erro quando a expressão a não é igual a b
    • Segundo o POSIX, as primitivas binárias -a, -o e os operadores (, ) estão marcados como obsoletos. Para detalhes, veja "Application Usage" em https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t...
    • Quer você use -a ou dois testes com &&, se puder usar a avaliação aritmética do bash, não há necessidade de sair para executar expr: [ $((1+1)) -eq 2 ]
  • Há alguns anos parei de usar [. test reforça o fato de que isso não é sintaxe, mas apenas um comando como qualquer outro. E man test é muito mais agradável do que vasculhar o man bash

    • Isso não bate muito. O GNU Coreutils tem não só a página de manual man test, mas também man [
      No Bash, há help test como uma cola rápida. O comando [ é muito antigo e já estava no Version 7 Unix em 1979
  • Concordo. [ e [[, que é específico do bash, realmente criam muita confusão sobre o que está acontecendo de fato, então era difícil ter certeza
    Mas o fato de [[ ser garantidamente um comando embutido certamente tinha um propósito na época em que a performance de scripts de shell importava, e isso nem faz tanto tempo assim

    • Depois de ler este texto, acho que vou simplesmente usar test. Não escrevo scripts bash com frequência, então sempre tropeço nas instruções if, especialmente nas regras de espaços. Depois de ver o motivo, ficou óbvio demais, e usar test deixa mais claro que você está apenas passando argumentos
  • A maior armadilha em [ e test é o comportamento com um único argumento. Por exemplo, para verificar se uma variável não está vazia, você poderia escrever [ -n $FOO ]
    Mas, se FOO não estiver definida, ela não se expande para uma string vazia, e sim para nada, ficando igual a [ -n ]. O POSIX exige que a forma de [ com um único argumento tenha sucesso se esse argumento, aqui "-n", não estiver vazio. Então ele informa incorretamente que $FOO não está vazia. Variáveis precisam sempre ser colocadas entre aspas

    • A última frase deveria ir para o começo. Coloque variáveis entre aspas. A especificação do comando embutido test em si não tem uma armadilha; a armadilha está no próprio shell
      O comportamento mencionado faz sentido. [ "$FOO" ] é uma forma que sempre testa se há conteúdo não vazio, seja qual for esse conteúdo, mesmo que seja "-n"
    • É só rodar ShellCheck nos scripts
    • Se $FOO contiver espaços, ela se expande para vários argumentos. Simplesmente sempre coloque variáveis entre aspas
    • [ x"$FOO" != x"" ]
    • Nesse caso, eu usaria [ -n "${FOO?}" ] para fazer o script parar imediatamente se $FOO for nula ou não estiver definida
  • chubot escreveu documentos interessantes que exploram as partes mais sutis de test/[/[[. Outros posts desse blog também explicam de forma bem interessante as esquisitices do shell
    ¹ https://www.oilshell.org/blog/2017/08/31.html
    ² https://www.oilshell.org/blog/2016/11/18.html

  • Eu não fazia ideia de que [ era um programa, e acho meio engraçado ele verificar se o último argumento é o colchete de fechamento
    Ainda assim, isso explica por que é preciso haver espaços dos dois lados dos colchetes

  • [[ é específico do bash. Se você sabe que só vai usar bash, pode usar. O texto cobre bem os detalhes

    • zsh também tem :)
      https://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
    • Dá para usar sempre [[
      Ele também existe no zsh e no ksh, e tenho quase certeza de que na verdade começou no ksh em 1988 ou antes
    • Ou seja, test e [ são especificados pelo POSIX e normalmente existem como binários reais. Mas um comando embutido do shell pode encobri-los
      [[ não é especificado pelo POSIX e normalmente existe apenas como comando embutido do shell
    • A menos que você esteja explicitamente usando um shell totalmente diferente, como Fish, não entendo por que não usar Bash
      Mirar apenas no menor denominador comum entre shells parece uma preocupação completamente antiga
  • Não entendi muito bem por que o último if é confuso. Se for porque, quando se aprende shell script pela primeira vez, normalmente se assume que [ é parte da linguagem de scripts do bash, e não apenas outro programa, aí eu entendo. Caso contrário, seria bom explicar por que isso é surpreendente

    • Mesmo sem saber que [ é um binário, não vejo muito por que seria confuso. Parece bash bem comum
  • Tenho opiniões fortes sobre shell, mas elas não combinam muito com as da maioria do mundo
    Acho que nunca se deveria usar [, apenas test. [ faz parecer que o mecanismo é parte da sintaxe da linguagem, mas na verdade é só mais um "programa". Aqui, "programa" inclui comandos embutidos e funções. if/||/&& olham para o status de saída, e programas, depois da expansão, não conseguem ver o status de saída de outra coisa, exceto olhando para a variável mágica $?, que é apenas uma string. case olha para strings, mas não opera com base no status de saída nem define um status de saída como parte do funcionamento de case ... esac. Um "programa" define o status de saída. Além disso, [/test deveriam ser usados apenas para avaliar estruturas do sistema de arquivos, como em test -f /dev/null. Para avaliação de strings, acho que se deve usar case. Naturalmente, fico incomodado ao ver a maioria dos scripts, e as pessoas acham estranhos os scripts que escrevo

    • A piada se voltou contra mim. Esse era exatamente o ponto do texto
      Ao usar shell, prefiro o formato em que, depois de if, o programa fica em uma linha separada e então vem then. É para enfatizar que o que vem depois de if "olha para o status de saída do último comando antes de then". Por exemplo, numa estrutura como if; ls /tmp/goober; test -d /tmp/goober; echo "I'll always execute the then clause because echo will always return a 0 return code"; then ... fi, como o if acaba olhando apenas para o status de saída de echo, a cláusula then sempre é executada
    • Encontrei um companheiro que trilha o mesmo caminho. Oito anos de scripts com test são minha prova de concordância. É um caminho solitário... culpo o guia de estilo de shell do Google
      Desenvolvi o hábito de preferir test escrevendo scripts que precisavam rodar tanto em sh quanto em bash, mas continuei porque faz mais sentido semanticamente do que tratar o caractere [ como um comando. Também é estranho que ] não seja um binário separado, mas um argumento de [. Entendo a razão técnica, mas parece um hack
    • Lendo os comentários aqui, parece que há dezenas de pessoas que de algum jeito usam só test. Dezenas!
  • Aprendi a usar [[ apenas quando quero fazer correspondência com regex. Ex.: if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fi
    Fora isso, uso simplesmente "test" ou "[". Já estou em 85.000 linhas de bash. Não estou dizendo que bash seja ótimo, mas ainda atende às minhas necessidades para muitas coisas

    • Se você quiser uma correspondência de padrões relativamente simples de forma compatível com POSIX, expr consegue fazer correspondência de expressões regulares básicas e também retornar grupos de captura

1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...

  • Fiquei apanhando por um tempo por causa do fato de que se usa a expressão regular diretamente e sem colocá-la entre aspas. Como sou do tipo que coloca aspas em tudo quase religiosamente, demorei para descobrir por que uma regex boba e simples não estava dando match.
  • Esse exemplo não tem lá muito sentido. Basta usar [ "$foo" = bar ] && echo Yes.
    Para correspondência de substring, [ e o glob * geralmente são suficientes. Algo como [ "$bar" = extra* ] && echo '$bar começou com extra'. O dialeto de regex do Bash é primitivo, então quase nunca vale o esforço de usá-lo. Para coisas complexas, o certo é usar outras ferramentas como grep, awk ou perl. Se você ficar obcecado em fazer tudo em bash, inclusive tarefas complexas que precisam de maior reutilização, modularidade e tipos nativos, o retorno diminui rapidamente.