Quero escrever um /bin/sh
script de shell que lide com qualquer arquivo que corresponda a um curinga. É fácil lidar com 1 ou mais arquivos correspondentes. No entanto, estou achando estranho lidar com o caso de 0 arquivos correspondentes.
A construção óbvia é:
#!/bin/sh
for f in *.ext; do
handle "$f"
done
onde *.ext
poderia ser uma ou mais expressões que o shell compara aos caminhos de arquivo e handle
é um comando do shell que é executado corretamente se for fornecido um caminho para um arquivo existente, mas falha se for fornecido um caminho que não mapeia para um arquivo. (No caso que provoca esta pergunta, são *.flac
e ffmpeg
, mas acho que isso não importa.)
Se houver arquivos correspondentes foo.ext
, bar.ext
então este script executa
handle "foo.ext"
handle "bar.ext"
como esperado. No entanto, se houvernãoquaisquer arquivos correspondentes, este script exibe uma mensagem de erro como,
handle: *.ext: No such file or directory
Acho que entendo por que isso acontece: ofestapágina de manual(que presumo ser válido /bin/sh
também) diz que a "lista de palavras a seguir in
é expandida, gerando uma lista de itens.… Se a expansão dos itens a seguir resultar em uma lista vazia, nenhum comando será executado e o status de retorno será 0." Aparentemente, quando *.ext
"está expandido", a lista de resultados inclui *.ext
, em vez de uma lista vazia. Mas isso não explica como impedir que isso aconteça.
(Atualização: há umsh (1)
página de manual do Projeto Heirloomque descreve o Bourne Shell sh
, não o posterior Bourne Again Shell bash
. SobGeração de nome de arquivo, diz claramente: "Se nenhum nome de arquivo que corresponda ao padrão for encontrado, a palavra permanecerá inalterada." Explica por que sh
deixa um *.ext
padrão na lista.)
Qual é uma maneira compacta e idiomática de escrever esse loop de modo que ele seja executado zero vezes sem erros se não houver arquivos correspondentes, mas seja executado uma vez para cada arquivo correspondente? Melhor ainda, o que funcionará com vários padrões como:
for f in *.ext1 special.ext1 *.ext2; do ...
Prefiro usar sintaxe compatível com /bin/sh
, para portabilidade. Acontece que estou usando o Mac OS X 10.11.6, mas espero uma sintaxe que funcione em qualquer sistema operacional semelhante ao Unix.
Eu descobri algumas maneiras desajeitadas e não idiomáticas de escrever esse loop. Se eu não obtiver boas respostas imediatamente, contribuirei com elas como respostas, para registro.
Responder1
A maneira portátil mais simples de fazer isso é pular o loop se a expansão não produzir algo que realmente exista:
for f in *.ext1 *.ext2; do
[ -e "$f" ] || continue
handle "$f"
done
Explicação: suponha que existam vários arquivos .ext2, mas nenhum arquivo .ext1. Nesse caso, os curingas serão expandidos para *.ext1
filea.ext2
fileb.ext2
filec.ext2
. Então [ -e "*.ext1" ]
falha e é executado continue
, o que pula o resto da iteração do loop. Então [ -e "filea.ext2" ]
, etc., é bem-sucedido e essas iterações do loop são executadas normalmente.
Aliás, você pode modificar isso para, por exemplo, [ -f "$f" ] || continue
pular qualquer coisa que não seja um arquivo simples (se quiser pular diretórios, etc.).
Claro, se você estiver executando no bash, você pode simplesmente executar shopt -s nullglob
primeiro para não deixar padrões incomparáveis na lista. E depois shopt -u nullglob
, para evitar efeitos colaterais inesperados (por exemplo, grep pattern *.nonexistent
tentarei ler o stdin se nullglob
estiver definido).
Responder2
Mude para o Bourne Again Shell ( /bin/bash
) do Bourne Shell ( /bin/sh
) e uma solução simples se tornará possível.
Obash(1)
página de manualmenciona onullglobopção:
Se definido, o bash permite padrões que não correspondem a nenhum arquivo (vejaExpansão do nome do caminhoacima) para expandir para uma string nula, em vez de eles próprios.
OExpansão do nome do caminhoseção diz:
Após a divisão das palavras,…bash verifica cada palavra em busca dos caracteres*,?, e[. Se um desses caracteres aparecer, a palavra será considerada um padrão e substituída por uma lista ordenada alfabeticamente de nomes de arquivos que correspondam ao padrão. Se nenhum nome de arquivo correspondente for encontrado e a opção shellnullglobnão está habilitado, a palavra permanece inalterada. Se onullglobopção for definida e nenhuma correspondência for encontrada, a palavra será removida.…
Então, definindo onullglobopção fornece o comportamento desejado para padrões. Se nenhum arquivo corresponder ao padrão, o loop não será executado.
#!/bin/bash
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
Mas onullglobopção pode interferir no comportamento desejado para outros comandos. Então, você provavelmente achará sensato salvar a configuração existente denullglobe restaure-o posteriormente. Felizmente, o shopt -p
comando interno emite uma saída em um formato que pode ser reutilizado como entrada. Mas ele emite saída para todas as opções, então use grep
para escolhernullobjcontexto. Veja o log abaixo (onde $
indica o prompt de comando do bash):
$ shopt -p | grep nullglob
shopt -u nullglob
$ shopt -s nullglob
$ shopt -p | grep nullglob
shopt -s nullglob
Portanto, a sintaxe final do bash para lidar com zero ou mais arquivos que correspondam a um padrão curinga é semelhante a esta:
#!/bin/bash
SAVED_NULLGLOB=$(shopt -p | grep nullglob)
shopt -s nullglob
for f in *.ext; do
handle "$f"
done
eval "$SAVED_NULLGLOB"
Além disso, a pergunta mencionou vários padrões, como:
for f in *.ext1 special.ext1 *.ext2; do ...
OnullglobA opção não afeta uma palavra na lista que não seja um "padrão", portanto special.ext1
ainda será passada para o loop. A única solução para isso é@Gordon Davissonexpressão continue
, [ -e "$f" ] || continue
.
Agradecimento: ambos@Ignacio Vázquez-Abramse@Gordon Davissonaludido bash(1)
e seunullglobopção. Obrigado. Como eles não contribuíram imediatamente com uma resposta com um exemplo completo, eu mesmo estou fazendo isso.