sh sintaxe para lidar com zero arquivos correspondentes a um caractere curinga, bem como mais?

0

Eu quero escrever um script shell /bin/sh que manipulará todos os arquivos correspondentes a um caractere curinga. É fácil lidar com 1 ou mais arquivos correspondentes. No entanto, estou achando estranho lidar com o caso de 0 arquivos correspondentes.

O constructo óbvio é:

#!/bin/sh
for f in *.ext; do
  handle "$f"
done

onde *.ext pode ser uma ou mais expressões que o shell compara a caminhos de arquivo, e handle é um comando shell que é executado corretamente se for fornecido um caminho para um arquivo existente, mas falhar se fornecer um caminho que não mapear para um arquivo. (No caso que provoca essa questão, eles são *.flac e ffmpeg , mas acho que isso não importa.)

Se houver arquivos correspondentes foo.ext , bar.ext , esse script será executado

handle "foo.ext"
handle "bar.ext"

como esperado. No entanto, se não houver arquivos correspondentes, esse script apresentará uma mensagem de erro como

handle: *.ext: No such file or directory

Acho que entendo por que isso acontece: a página do manual bash (que Eu assumo que é válido para /bin/sh também) diz que a "lista de palavras após in é expandida, gerando uma lista de itens ... Se a expansão dos itens seguintes resultar em uma lista vazia, nenhum comando será executado e o status de retorno é 0. " Aparentemente, quando *.ext "é 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á uma página sh (1) man do Projeto Heirloom que descreve o Bourne Shell sh , não o posterior Bourne Again Shell bash Em Geração de nome de arquivo , diz claramente, "Se nenhum nome de arquivo for encontrado que corresponda ao padrão, a palavra será mantida inalterada." Isso explica por que sh deixa um padrão *.ext na lista.)

O que é uma maneira idiomática e compacta de escrever esse loop para que ele seja executado zero vezes sem erros se não houver arquivos correspondentes, mas será 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 ...

Eu prefiro usar a sintaxe compatível com /bin/sh , para portabilidade. Por acaso estou usando o Mac OS X 10.11.6, mas estou esperando pela sintaxe que funcione em qualquer sistema operacional parecido com o Unix.

Eu criei algumas maneiras desajeitadas e não idiomáticas para escrever esse loop. Se eu não obtiver imediatamente boas respostas, contribuirei com essas respostas, para o registro.

    
por Jim DeLaHunt 03.04.2018 / 01:48

2 respostas

2

A forma mais simples e portátil de fazer isso é pular o loop para 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 haja vários arquivos .ext2, mas nenhum arquivo .ext1. Nesse caso, os curingas serão expandidos para *.ext1 filea.ext2 fileb.ext2 filec.ext2 . Portanto, [ -e "*.ext1" ] falha e executa continue , que pula o restante da iteração do loop. Então, [ -e "filea.ext2" ] etc terá sucesso, e essas iterações do loop serão executadas normalmente.

BTW, você pode modificar isso para, por exemplo, [ -f "$f" ] || continue para pular qualquer coisa que não seja um arquivo simples (se você quiser pular diretórios, etc).

É claro que, se você estiver executando o bash, poderá 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 tentará ler de stdin se nullglob estiver definido).

    
por 03.04.2018 / 07:16
0

Mude para o Bourne Again Shell ( /bin/bash ) do Bourne Shell ( /bin/sh ), e uma solução simples se torna possível.

A página bash(1) man menciona a opção nullglob :

If set, bash allows patterns which match no files (see Pathname Expansion above) to expand to a null string, rather than themselves.

A seção Expansão do nome do caminho diz:

After word splitting, …bash scans each word for the characters *, ?, and [. If one of these characters appears, then the word is regarded as a pattern, and replaced with an alphabetically sorted list of file names matching the pattern. If no matching file names are found, and the shell option nullglob is not enabled, the word is left unchanged. If the nullglob option is set, and no matches are found, the word is removed.…

Portanto, a configuração da opção nullglob fornece o comportamento desejado para os 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 a opção nullglob pode interferir no comportamento desejado de outros comandos. Assim, você provavelmente achará melhor salvar a configuração existente de nullglob e restaurá-la depois. Felizmente, o comando shopt -p builtin emite a saída em um formato que pode ser reutilizado como entrada. Mas emite saída para todas as opções, então use grep para escolher a configuração nullobj . 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

Assim, a sintaxe final do bash para lidar com zero ou mais arquivos correspondentes a um padrão curinga é semelhante a:

#!/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 questão mencionou vários padrões, como:

for f in *.ext1 special.ext1 *.ext2; do ...

A opção nullglob não afeta uma palavra na lista que não seja "padrão", por isso special.ext1 ainda será passado para o loop. A única solução para isso é @Gordon Davisson continue expression, [ -e "$f" ] || continue .

Reconhecimento: ambos @Ignacio Vazquez-Abrams e @ Gordon Davisson aludiu à opção bash(1) e seu nullglob . Obrigado. Uma vez que eles não contribuíram imediatamente com uma resposta com um exemplo totalmente trabalhado, eu estou fazendo isso sozinho.

    
por 06.04.2018 / 01:53