A seguinte função Bash deu resultados inconsistentes:
# $1 Path to ZIP archive.
# Exits with 0 status iff it contains a “.mp3” or “.flac” file.
mp3_or_flac_in_zip() {
local archive=${1:?No archive given.}
(
set -o pipefail
unzip -l "$archive" | grep -iqE '.\.(flac|mp3)$'
)
}
Quando executado n vezes seguidas no mesmo ZIP contendo música, ele relatou aleatoriamente que não havia música nele (cerca de 1–5% das vezes, mas variou muito entre os ZIPs).
Mudar para uma variável intermediária em vez de um pipe (com &&
instead of set -o pipefail
para ainda ter certeza de unzip
que estava funcionando bem) corrigiu as inconsistências:
# $1 Path to ZIP archive.
# Exits with 0 status iff it contains a “.mp3” or “.flac” file.
mp3_or_flac_in_zip() {
local archive=${1:?No archive given.}
local listing
listing=$(unzip -l "$archive") &&
grep -iqE '.\.(flac|mp3)$' <<< "$listing"
}
Qual poderia ser o problema aí? E há outros contextos em que pipes não são uma ideia tão boa?
Basicamente parece outro caso de https://stackoverflow.com/questions/19120263/why-exit-code-141-with-grep-q , que descobri ao verificar o status de retorno que estava recebendo (141).
Em algumas execuções,
unzip
parece ter “tempo suficiente” para terminar seu trabalho, enquanto em outras execuções, ele é morto porgrep
porquegrep
encontra rapidamente uma correspondência . Comopipefail
estava ligado, isso associa um status de erro a todo ofoo | bar
conjunto de comandos. Conclui-se que é provavelmente possível encontrar problemas semelhantes com praticamente qualquerfoo | grep -q
combinado compipefail
.As discrepâncias em termos de probabilidade de falha entre os ZIPs podem ser devidas ao número de arquivos nesses ZIPs (listas mais curtas vs. mais longas) e à posição do primeiro arquivo correspondente ao grep nessas listas.
A
foo_output=$(foo)
abordagem garante quefoo
sempre haja a oportunidade de terminar seu trabalho sem ser eliminado , mas é claro que isso pode levar a desempenhos mais baixos (grep -q
eliminar o comando de entrada após a primeira partida é intencional e economiza tempo e recursos).Compromisso provavelmente fraco
Ainda use um pipe, mas sem
pipefail
, e capturestderr
para verificar se está vazio, e assuma que a ausência de erros ou avisos significa queunzip
correu bem. O principal problema que vejo aí é que pode levar a falhas devido a avisos básicos associados a peculiaridades não fatais de ZIPs específicos, mas isso é, até certo ponto, também o caso com as outras abordagens da questão, eu acho:(Isso me faz pensar se há maneiras melhores de verificar a presença de tipos específicos de arquivos em um ZIP. Isso parece altamente provável.)
Editar: Abordagem mais realista
Conforme apontado por discussões naquela página , a utilidade de
pipefail
nesse contexto é duvidosa na melhor das hipóteses, pois de qualquer forma uma falhaunzip
não produzirá (no stdout) nada que satisfaça ogrep
. Além disso, se algo desagradaunzip
, o motivo provavelmente aparecerá via stderr. E para tornar as coisas ainda mais interessantes, problemas não fatais (que tendem a produzir status diferentes de zero de acordo comman unzip
) também não nos impedirão de encontrar arquivos, dessa forma.Considerações finais
Na minha humilde opinião, esse é mais um argumento para dizer que não devemos alternar inúmeras flags Bash o tempo todo sem estar muito cientes das consequências potenciais, mesmo que alguns “modelos de script” multifuncionais anunciem
set -o pipefail
eset -e
(entre outras coisas) como formas de evitar bugs magicamente. Para cada problema que eles evitam, eles geralmente criam o dobro da quantidade de problemas potenciais se você não ficar de olho (e as coisas pioram ainda mais em um ambiente empresarial onde você não pode forçar todos a passar um dia lendoman bash
). Eu ainda ocasionalmente caio em advertências como essa depois de anos de script. No caso presente, pensei que usar um( … )
subshell para manter opipefail
efeito o mais local possível me manteria seguro, apenas para tropeçar em um problema dentro do subshell, bem debaixo do meu nariz.Você pode fazer:
Para
grep
ler toda a entrada (encontrar todas as correspondências e relatá-las, que descartamos, mesmo que só nos importemos se houve pelo menos uma) e evitarbsdtar
ser morto por um SIGPIPE apósgrep
as saídas após a primeira correspondência.Substituí
unzip
pela interface CLI do libarchive , pois não é possível processar caminhos de arquivo arbitrários, pois ele interpreta curingas¹. Isso também significa que podemos processar outros tipos de arquivo.bsdtar
unzip
Movi a
archive
atribuição de variáveis para dentro do subshell, então não precisamos do não padrãolocal
, e${var?error}
só sai desse subshell, ou seja, da função, e não do script inteiro.E eu substituí
.
por[^/]
para evitar corresponder apath/to/.flac
como presumo que era sua intenção com isso.
.Em qualquer caso, se você quiser verificar se
unzip
/bsdtar
egrep
foram bem-sucedidos, porque, por exemplo, você quer ser capaz de detectar um arquivo corrompido mesmo que alguns.mp3
arquivos possam ser encontrados, então você precisa deixarunzip
/bsdtar
ser executado até o final, então você precisa consumir toda a sua saída.Além de mudar de
grep -q
paragrep > /dev/null
, como uma abordagem mais genérica, você poderia fazer:Uma alternativa para obter
bsdtar
/unzip
para imprimir a lista completa de membros do arquivo e processá-la comgrep
, você pode usarbsdcpio
para listar apenas os membros que deseja e apenas verificar se há alguma saída:Ou:
Agora, um problema com essa abordagem é que ela retornará true mesmo se todos os arquivos que têm um nome terminando em
.mp3
ou.flac
forem do tipo directory . Absdtar|grep
abordagem teria excluído aqueles como nabsdtar
listagem, arquivos do tipo directory aparecem com um/
anexado ao seu nome. Há um problema semelhante, independentemente da abordagem com symlinks.Observe que, como alternativa a um subshell, desde o bash 4.4, você pode usar
local -
como no shell Almquish ou como oset -o localoptions
ofzsh
para que as alterações nas configurações de opções (apenas aquelas definidas porset
, não aquelas definidas porshopt
) sejam locais para a função:O mesmo que o do zsh:
Ou ksh93 (de onde a
pipefail
opção veio inicialmente):Onde as mudanças nas opções são sempre locais, desde que você use o estilo Korn de definição de função. Observe também que no ksh93, essas mudanças de opções (e esse é o caso da
$archive
variável local também) não se propagam para outras funções chamadas dentro, então sua função pode invocar com segurança outras funções que esperam que apipefail
opção esteja desligada sem fazer issoset +o pipefail
elas mesmas.¹ Mesmo em sistemas do tipo Unix, onde
*
,?
são caracteres tão válidos quanto quaisquer outros em um caminho de arquivo e a geração do nome de arquivo deve ser feita pelo shell,unzip
também tenta ler o arquivo com.zip
ou.ZIP
anexado se não puder abrir o arquivo sem e, ao contrário,bsdtar
não oferece suporte a arquivos não pesquisáveis; é mais um programa do MSDOS.zipinfo
, que geralmente vem comunzip
, com sua-1
opção também teria sido uma alternativa melhor, embora sofra dos mesmos problemas queunzip
.Se for importante que o comando do lado esquerdo termine, você pode executar
grep
sem-q
e apenas redirecionar a saída para/dev/null
. O status de saída deve ser o mesmo (mas é claro que grep faz algum trabalho desnecessário aqui).Se não importa se o lado esquerdo termina ou é encerrado pelo fechamento do pipe, você pode apenas verificar especificamente o status de saída correspondente ao SIGPIPE e tratá-lo como zero.
No Bash, você também pode verificar a
$PIPESTATUS
matriz para verificar qual comando falha e com qual status.