test, [, e [[ (2020)
(jmmv.dev)- 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 testavalia 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
teste[como comandos internos, então as mensagens de erro e o comportamento podem diferir entre o/bin/testexterno 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/[
- O comando de exemplo
/bin/[e/bin/testpodem 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 = bnã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 comandoif [ a = b ]; then ... fiexecuta 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
ifnão interpreta a condição diretamente; ela decide com base no código de saída do comando recebidotest a = a; echo $?resulta em0test a = b; echo $?resulta em1[ a = a ]; echo $?resulta em0[ a = b ]; echo $?resulta em1
- No mesmo contexto,
trueefalsetambé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
teste[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 bproduztest: a: unexpected operatortest a bproduzdash: 2: test: a: unexpected operator
- Essa diferença pode ocorrer não só com
teste[, mas também com comandos aparentemente simples comoecho - 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
- Ao contrário de
- No exemplo com glob,
[e[[se comportam de forma diferente- Depois de
touch long-name,[ long* = long-name ] && echo matchimprimematch - Aos argumentos do comando
[se aplicam as regras normais de expansão do shell, entãolong*é expandido paralong-nameno diretório [[ long* = long-name ]] && echo matchnão imprime nada[[tratalong*como string literal e a compara diretamente comlong-name, então a comparação falha
- Depois de
- 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 seja0se ambos os comandos tiverem sucesso- Se o primeiro
grepfalhar, 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
- Exemplo:
- O POSIX não exige que
/bin/[e/bin/testsejam 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
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 significadoEm alguns shells que não são bash, a palavra-chave
functioné necessária para declarar certos tipos de função. O$(shell)domakepode 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 é usarinclude. A GNU ignorar o POSIX é totalmente razoável, já que o POSIX não é muito útil para resolver a maioria dos problemas reais.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 issoEm scripts, geralmente faz sentido se limitar ao POSIX
sh. No mínimo, é preciso saber se você está usando sintaxe específica do Bash--ignore-caseeset -o pipefailreduz a portabilidade dos scripts. Isso, em si, está corretoMas 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?if [ a = b ] || grep -q ^hello$ /usr/share/dict/words; then echo "test failed and grep succeeded"; finão é simplesmente um uso cotidiano de shell bem comum?make $(shell …) expansion, mas deveria sermake $(shell ...) expansionComo no corpo do texto está escrito corretamente com três pontos, e não com um único caractere de reticências, o próprio
mldrtambém não está correto. Parece que dois bugs independentes acabaram afetando isso ao mesmo tempoLevando 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!"; fivira[ 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 blocoifverificar comandos comuns também permite coisas comoif 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 detest, 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 blocoifset -e, como deveria,if [ a = b ]; then echo "Oops!"; fifunciona como esperado, mas[ a = b ] && echo "Oops!"termina com erro quando a expressãoanão é igual ab-a,-oe os operadores(,)estão marcados como obsoletos. Para detalhes, veja "Application Usage" em https://pubs.opengroup.org/onlinepubs/9699919799/utilities/t...-aou dois testes com&&, se puder usar a avaliação aritmética do bash, não há necessidade de sair para executarexpr:[ $((1+1)) -eq 2 ]Há alguns anos parei de usar
[.testreforça o fato de que isso não é sintaxe, mas apenas um comando como qualquer outro. Eman testé muito mais agradável do que vasculhar oman bashman test, mas tambémman [No Bash, há
help testcomo uma cola rápida. O comando[é muito antigo e já estava no Version 7 Unix em 1979Concordo.
[e[[, que é específico do bash, realmente criam muita confusão sobre o que está acontecendo de fato, então era difícil ter certezaMas 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 assimtest. Não escrevo scripts bash com frequência, então sempre tropeço nas instruçõesif, especialmente nas regras de espaços. Depois de ver o motivo, ficou óbvio demais, e usartestdeixa mais claro que você está apenas passando argumentosA maior armadilha em
[etesté 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
FOOnã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$FOOnão está vazia. Variáveis precisam sempre ser colocadas entre aspastestem si não tem uma armadilha; a armadilha está no próprio shellO 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"$FOOcontiver espaços, ela se expande para vários argumentos. Simplesmente sempre coloque variáveis entre aspas[ x"$FOO" != x"" ][ -n "${FOO?}" ]para fazer o script parar imediatamente se$FOOfor nula ou não estiver definidachubot 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 fechamentoAinda 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 detalheshttps://zsh.sourceforge.io/Doc/Release/Conditional-Expressio...
[[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
teste[são especificados pelo POSIX e normalmente existem como binários reais. Mas um comando embutido do shell pode encobri-losJá
[[não é especificado pelo POSIX e normalmente existe apenas como comando embutido do shellMirar 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[é um binário, não vejo muito por que seria confuso. Parece bash bem comumTenho opiniões fortes sobre shell, mas elas não combinam muito com as da maioria do mundo
Acho que nunca se deveria usar
[, apenastest.[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.caseolha para strings, mas não opera com base no status de saída nem define um status de saída como parte do funcionamento decase ... esac. Um "programa" define o status de saída. Além disso,[/testdeveriam ser usados apenas para avaliar estruturas do sistema de arquivos, como emtest -f /dev/null. Para avaliação de strings, acho que se deve usarcase. Naturalmente, fico incomodado ao ver a maioria dos scripts, e as pessoas acham estranhos os scripts que escrevoAo usar shell, prefiro o formato em que, depois de
if, o programa fica em uma linha separada e então vemthen. É para enfatizar que o que vem depois deif"olha para o status de saída do último comando antes de then". Por exemplo, numa estrutura comoif; 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 oifacaba olhando apenas para o status de saída deecho, a cláusula then sempre é executadatestsão minha prova de concordância. É um caminho solitário... culpo o guia de estilo de shell do GoogleDesenvolvi o hábito de preferir
testescrevendo scripts que precisavam rodar tanto emshquanto embash, 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 hacktest. Dezenas!Aprendi a usar
[[apenas quando quero fazer correspondência com regex. Ex.:if [[ "${foo}" =~ ^bar$ ]]; then echo Yes; fiFora 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 coisasexprconsegue fazer correspondência de expressões regulares básicas e também retornar grupos de captura1: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/e...
[ "$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 comogrep,awkouperl. 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.