Esta questão é inspirada em
Por que o uso de um loop de shell para processar o texto é considerado uma má prática?
Eu vejo essas construções
for file in `find . -type f -name ...`; do smth with ${file}; done
e
for dir in $(find . -type d -name ...); do smth with ${dir}; done
sendo usado aqui quase diariamente, mesmo que algumas pessoas comentem sobre essas postagens, explicando por que esse tipo de coisa deve ser evitado ...
Vendo o número dessas postagens (e o fato de que algumas vezes esses comentários são simplesmente ignorados) Eu pensei que também poderia fazer uma pergunta:
Por find
que as práticas inadequadas de saída do loop over e qual é a maneira correta de executar um ou mais comandos para cada nome / caminho de arquivo retornado find
?
Respostas:
O problema
combina duas coisas incompatíveis.
find
imprime uma lista de caminhos de arquivo delimitados por caracteres de nova linha. Enquanto o operador split + glob que é chamado quando você o deixa sem$(find .)
aspas nesse contexto de lista o divide nos caracteres de$IFS
(por padrão, inclui nova linha, mas também espaço e tabulação (e NUL inzsh
)) e executa globbing em cada palavra resultante (exceto inzsh
) (e até pare de expandir os derivados ksh93 ou pdksh!).Mesmo se você fizer isso:
Isso ainda está errado, pois o caractere de nova linha é tão válido quanto qualquer outro no caminho do arquivo. A saída de
find -print
simplesmente não é pós-processável de maneira confiável (exceto usando algum truque complicado, como mostrado aqui ).Isso também significa que o shell precisa armazenar a saída
find
totalmente e depois dividi-la + globá-la (o que implica armazenar essa saída uma segunda vez na memória) antes de começar a percorrer os arquivos.Observe que
find . | xargs cmd
há problemas semelhantes (há espaços em branco, nova linha, aspas simples, aspas duplas e barra invertida (e com algumasxarg
implementações de bytes que não fazem parte de caracteres válidos) são um problema)Alternativas mais corretas
A única maneira de usar um
for
loop na saída defind
seria usar oszsh
suportesIFS=$'\0'
e:(substitua
-print0
com-exec printf '%s\0' {} +
parafind
implementações que não suportam o não-padrão (mas bastante comum hoje em dia)-print0
).Aqui, a maneira correta e portátil é usar
-exec
:Ou se
something
pode levar mais de um argumento:Se você precisar que a lista de arquivos seja manipulada por um shell:
(cuidado, pode iniciar mais de um
sh
).Em alguns sistemas, você pode usar:
embora isso tenha pouca vantagem sobre a sintaxe padrão e os meios
something
,stdin
seja o pipe ou/dev/null
.Um motivo que você pode querer usar é a
-P
opção do GNUxargs
para processamento paralelo. Ostdin
problema também pode ser contornado com o GNU,xargs
com a-a
opção com shells que suportam a substituição do processo:por exemplo, para executar até 4 chamadas simultâneas de
something
cada uma recebendo 20 argumentos de arquivo.Com
zsh
oubash
, outra maneira de fazer um loop sobre a saída defind -print0
é com:read -d ''
lê registros delimitados NUL em vez de registros delimitados por nova linha.bash-4.4
e acima também podem armazenar arquivos retornados porfind -print0
uma matriz com:O
zsh
equivalente (que tem a vantagem de preservarfind
o status de saída):Com
zsh
, você pode traduzir a maioria dasfind
expressões para uma combinação de globbing recursivo com qualificadores glob. Por exemplo, repetirfind . -name '*.txt' -type f -mtime -1
seria:Ou
(cuidado com a necessidade de
--
como**/*
, os caminhos dos arquivos não estão começando./
, portanto, podem começar com,-
por exemplo).ksh93
e,bash
eventualmente, adicionou suporte para**/
(embora não haja mais formas avançadas de globbing recursivo), mas ainda não os qualificadores da glob, que fazem uso**
muito limitado por lá. Lembre-se também de quebash
antes do 4.3 segue links simbólicos ao descer a árvore de diretórios.Como no looping
$(find .)
, isso também significa armazenar toda a lista de arquivos na memória 1 . Isso pode ser desejável, embora em alguns casos, quando você não quer suas ações sobre os arquivos para ter uma influência sobre a descoberta de arquivos (como quando você adicionar mais arquivos que podem acabar-up sendo encontraram-se).Outras considerações de confiabilidade / segurança
Condições da corrida
Agora, se estamos falando de confiabilidade, temos que mencionar as condições da corrida entre o horário
find
/zsh
encontrar um arquivo e verificar se ele atende aos critérios e o tempo em que está sendo usado ( corrida TOCTOU ).Mesmo ao descer uma árvore de diretórios, é preciso ter o cuidado de não seguir os links simbólicos e fazer isso sem a corrida TOCTOU.
find
(find
Pelo menos GNU ) faz isso abrindo os diretórios usandoopenat()
osO_NOFOLLOW
sinalizadores corretos (onde houver suporte) e mantendo um descritor de arquivo aberto para cada diretório,zsh
/bash
/ksh
não faça isso. Portanto, diante de um invasor ser capaz de substituir um diretório por um link simbólico no momento certo, você pode acabar descendo para o diretório errado.Mesmo
find
que desça o diretório corretamente, com-exec cmd {} \;
e ainda mais com-exec cmd {} +
, uma vezcmd
executado, por exemplo, quandocmd ./foo/bar
oucmd ./foo/bar ./foo/bar/baz
quando ocmd
uso for feito./foo/bar
, os atributos debar
podem não mais atender aos critérios correspondentes afind
, mas ainda pior,./foo
podem ter sido substituído por um link simbólico para outro lugar (e a janela da corrida é aumentada com-exec {} +
ondefind
espera ter arquivos suficientes para chamarcmd
).Algumas
find
implementações têm um-execdir
predicado (ainda não padronizado) para aliviar o segundo problema.Com:
find
chdir()
s no diretório pai do arquivo antes de executarcmd
. Em vez de chamarcmd -- ./foo/bar
, ele chamacmd -- ./bar
(cmd -- bar
com algumas implementações, daí a--
), para./foo
evitar o problema de ser alterado para um link simbólico. Isso torna o uso de comandosrm
mais seguro (ainda pode remover um arquivo diferente, mas não um arquivo em um diretório diferente), mas não comandos que podem modificar os arquivos, a menos que tenham sido projetados para não seguir links simbólicos.-execdir cmd -- {} +
às vezes também funciona, mas com várias implementações, incluindo algumas versões do GNUfind
, é equivalente a-execdir cmd -- {} \;
.-execdir
também tem o benefício de solucionar alguns dos problemas associados a árvores de diretório muito profundas.No:
o tamanho do caminho indicado
cmd
aumentará com a profundidade do diretório em que o arquivo está. Se esse tamanho for maior quePATH_MAX
(algo como 4k no Linux), qualquer chamada do sistema quecmd
fizer nesse caminho falhará com umENAMETOOLONG
erro.Com
-execdir
, apenas o nome do arquivo (possivelmente prefixado./
) é passado paracmd
. Os nomes dos arquivos na maioria dos sistemas de arquivos têm um limite muito menor (NAME_MAX
) do quePATH_MAX
, portanto,ENAMETOOLONG
é menos provável que o erro seja encontrado.Bytes vs caracteres
Além disso, muitas vezes esquecido ao considerar a segurança
find
e, geralmente, o manuseio de nomes de arquivos em geral, é o fato de que na maioria dos sistemas semelhantes ao Unix, os nomes de arquivos são sequências de bytes (qualquer valor de byte, mas 0 em um caminho de arquivo e na maioria dos sistemas ( Os baseados em ASCII, ignoraremos os raros baseados em EBCDIC por enquanto) (0x2f é o delimitador de caminho).Cabe aos aplicativos decidir se desejam considerar esses bytes como texto. E geralmente, mas geralmente a conversão de bytes para caracteres é feita com base na localidade do usuário, com base no ambiente.
O que isso significa é que um determinado nome de arquivo pode ter uma representação de texto diferente, dependendo da localidade. Por exemplo, a sequência de bytes
63 f4 74 e9 2e 74 78 74
seriacôté.txt
para um aplicativo que interpreta esse nome de arquivo em um código de idioma em que o conjunto de caracteres é ISO-8859-1 ecєtщ.txt
em um código de idioma em que o conjunto de caracteres é IS0-8859-5.Pior. Em um local onde o conjunto de caracteres é UTF-8 (a norma atualmente), 63 f4 74 e9 2e 74 78 74 simplesmente não podiam ser mapeados para caracteres!
find
é um desses aplicativos que considera nomes de arquivos como texto para seus-name
/-path
predicados (e mais, como-iname
ou-regex
com algumas implementações).O que isso significa é que, por exemplo, com várias
find
implementações (incluindo GNUfind
).não encontrou nosso
63 f4 74 e9 2e 74 78 74
arquivo acima quando chamado em um código de idioma UTF-8, pois*
(que corresponde a 0 ou mais caracteres , não bytes) não poderia corresponder a esses não caracteres.LC_ALL=C find...
resolveria o problema, pois o código de idioma C implica um byte por caractere e (geralmente) garante que todos os valores de byte sejam mapeados para um caractere (embora possivelmente indefinidos para alguns valores de byte).Agora, quando se trata de fazer um loop sobre esses nomes de arquivo a partir de um shell, esse byte vs caractere também pode se tornar um problema. Normalmente, vemos 4 tipos principais de conchas nesse sentido:
Os que ainda não têm conhecimento de vários bytes
dash
. Para eles, um byte é mapeado para um personagem. Por exemplo, em UTF-8,côté
tem 4 caracteres, mas 6 bytes. Em um local onde UTF-8 é o conjunto de caracteres, emfind
encontrará com êxito os arquivos cujo nome consiste em 4 caracteres codificados em UTF-8, masdash
reportará comprimentos que variam entre 4 e 24.yash
: o oposto. Ele lida apenas com personagens . Toda a entrada necessária é traduzida internamente para caracteres. Ele cria o shell mais consistente, mas também significa que ele não pode lidar com seqüências de bytes arbitrárias (aquelas que não se traduzem em caracteres válidos). Mesmo no código C, ele não pode lidar com valores de bytes acima de 0x7f.em um local UTF-8 falhará em nosso ISO-8859-1
côté.txt
anteriormente, por exemplo.Aqueles como
bash
ouzsh
onde o suporte multi-byte foi adicionado progressivamente. Aqueles voltarão a considerar bytes que não podem ser mapeados para caracteres como se fossem caracteres. Eles ainda têm alguns bugs aqui e ali, especialmente com conjuntos de caracteres de bytes múltiplos menos comuns, como GBK ou BIG5-HKSCS (aqueles que são bastante desagradáveis, pois muitos de seus caracteres de bytes múltiplos contêm bytes no intervalo de 0 a 127 (como os caracteres ASCII) )Aqueles como o
sh
do FreeBSD (11 no mínimo) oumksh -o utf8-mode
que suportam multi-bytes, mas apenas para UTF-8.Notas
1 Para completar, poderíamos mencionar uma maneira hacky de
zsh
fazer loop sobre arquivos usando globbing recursivo sem armazenar a lista inteira na memória:+cmd
é um qualificador global que chamacmd
(normalmente uma função) com o caminho do arquivo atual$REPLY
. A função retorna true ou false para decidir se o arquivo deve ser selecionado (e também pode modificar$REPLY
ou retornar vários arquivos em uma$reply
matriz). Aqui fazemos o processamento nessa função e retornamos false para que o arquivo não seja selecionado.fonte
find
para se comportar de forma segura. O globbing é seguro por padrão, enquanto a localização é insegura por padrão.A resposta simples é:
Porque os nomes de arquivos podem conter qualquer caractere.
Portanto, não há caracteres imprimíveis que você possa usar com confiabilidade para delimitar nomes de arquivos.
Novas linhas são frequentemente usadas (incorretamente) para delimitar nomes de arquivos, porque é incomum incluir caracteres de nova linha nos nomes de arquivos.
No entanto, se você criar seu software com base em suposições arbitrárias, na melhor das hipóteses, simplesmente não consegue lidar com casos incomuns e, na pior das hipóteses, se abre para explorações maliciosas que liberam o controle do seu sistema. Portanto, é uma questão de robustez e segurança.
Se você puder escrever software de duas maneiras diferentes, e uma delas manipular corretamente casos extremos (entradas incomuns), mas a outra for mais fácil de ler, você poderá argumentar que há uma troca. (Eu não gostaria. Prefiro o código correto.)
No entanto, se a versão correta e robusta do código também for fácil de ler, não haverá desculpa para escrever código que falhe em casos extremos. Este é o caso
find
e a necessidade de executar um comando em cada arquivo encontrado.Vamos ser mais específicos: em um sistema UNIX ou Linux, os nomes de arquivos podem conter qualquer caractere, exceto um
/
(que é usado como um separador de componentes de caminho) e não podem conter um byte nulo.Um byte nulo é, portanto, a única maneira correta de delimitar nomes de arquivos.
Como o GNU
find
inclui um-print0
primário que usará um byte nulo para delimitar os nomes de arquivos impressos, o GNUfind
pode ser usado com segurança com o GNUxargs
e seu-0
sinalizador (e-r
sinalizador) para lidar com a saída defind
:No entanto, não há um bom motivo para usar este formulário, porque:
find
foi desenvolvido para executar comandos nos arquivos encontrados.Além disso, o GNU
xargs
requer-0
e-r
, enquanto o FreeBSDxargs
requer apenas-0
(e não tem-r
opção), e algunsxargs
não suportam-0
. Portanto, é melhor manter os recursos do POSIXfind
(consulte a próxima seção) e pularxargs
.Quanto ao ponto 2
find
- a capacidade de executar comandos nos arquivos encontrados - acho que Mike Loukides disse o melhor:Usos especificados POSIX de
find
Para executar um único comando para cada arquivo encontrado, use:
Para executar vários comandos em sequência para cada arquivo encontrado, onde o segundo comando só deve ser executado se o primeiro comando for bem-sucedido, use:
Para executar um único comando em vários arquivos de uma vez:
find
em combinação comsh
Se você precisar usar os recursos de shell no comando, como redirecionar a saída ou remover uma extensão do nome do arquivo ou algo semelhante, poderá usar a
sh -c
construção. Você deve saber algumas coisas sobre isso:Nunca incorpore
{}
diretamente nosh
código. Isso permite a execução arbitrária de códigos a partir de nomes de arquivos criados com códigos maliciosos. Além disso, nem mesmo é especificado pelo POSIX que funcionará. (Veja o próximo ponto.)Não use
{}
várias vezes ou use-o como parte de um argumento mais longo. Isso não é portátil. Por exemplo, não faça isso:find ... -exec cp {} somedir/{}.bak \;
Para citar as especificações POSIX para
find
:Os argumentos após a sequência de comandos do shell passada para a
-c
opção são definidos nos parâmetros posicionais do shell, começando com$0
. Não começando com$1
.Por esse motivo, é bom incluir um
$0
valor "fictício" , comofind-sh
, que será usado para relatórios de erros de dentro do shell gerado. Além disso, isso permite o uso de construções, como"$@"
ao passar vários arquivos para o shell, enquanto a omissão de um valor para$0
significaria que o primeiro arquivo passado seria definido$0
e, portanto, não incluído"$@"
.Para executar um único comando shell por arquivo, use:
No entanto, geralmente ele oferece melhor desempenho para manipular os arquivos em um loop de shell, para que você não crie um shell para cada arquivo encontrado:
(Observe que
for f do
é equivalentefor f in "$@"; do
e lida com cada um dos parâmetros posicionais por sua vez - em outras palavras, ele usa cada um dos arquivos encontrados porfind
, independentemente de quaisquer caracteres especiais em seus nomes.)Outros exemplos de
find
uso correto :(Nota: fique à vontade para estender esta lista.)
fonte
find
saída da análise - em que você precisa executar comandos no shell atual (por exemplo, porque deseja definir variáveis) para cada arquivo. Nesse caso,while IFS= read -r -u3 -d '' file; do ... done 3< <(find ... -print0)
é o melhor idioma que conheço. Notas:<( )
não é portátil - use bash ou zsh. Além disso, o-u3
e3<
existe caso algo dentro do loop tente ler stdin.find ... -exec
chamada. Ou apenas use um shell glob, se ele manipular seu caso de uso.filelist=(); while ... do filelist+=("$file"); done ...
).find
output ou pior aindals
. Estou fazendo isso diariamente sem problemas. Eu sei sobre as opções -print0, --null, -z ou -0 de todos os tipos de ferramentas. Mas não perderia tempo para usá-los no prompt do shell interativo, a menos que realmente fosse necessário. Isso também pode ser observado em sua resposta.Essa resposta é para conjuntos de resultados muito grandes e diz respeito principalmente ao desempenho, por exemplo, ao obter uma lista de arquivos em uma rede lenta. Para pequenas quantidades de arquivos (digamos alguns 100 ou talvez 1000 em um disco local), a maior parte disso é discutível.
Paralelismo e uso de memória
Além das outras respostas dadas, relacionadas a problemas de separação e outras, existe outro problema com
A peça dentro dos backticks deve ser avaliada totalmente primeiro, antes de ser dividida nas quebras de linha. Isso significa que, se você receber uma quantidade enorme de arquivos, ele poderá engasgar com os limites de tamanho existentes nos vários componentes; você pode ficar sem memória se não houver limites; e, em qualquer caso, você deve esperar até que toda a lista seja impressa
find
e analisadafor
antes mesmo de executar sua primeirasmth
.A maneira unix preferida é trabalhar com pipes, que são inerentemente executados em paralelo e que também não precisam de buffers arbitrariamente grandes em geral. Isso significa: você prefere que ele
find
seja executado paralelamente ao seusmth
, e apenas mantenha o nome do arquivo atual na RAM enquanto ele o entregasmth
.Uma solução pelo menos parcialmente aceitável para isso é a mencionada acima
find -exec smth
. Isso elimina a necessidade de manter todos os nomes de arquivos na memória e funciona bem em paralelo. Infelizmente, também inicia umsmth
processo por arquivo. Sesmth
só pode funcionar em um arquivo, é assim que deve ser.Se possível, a solução ideal seria
find -print0 | smth
:smth
poder processar nomes de arquivos em seu STDIN. Então, você terá apenas umsmth
processo, não importa quantos arquivos existam, e precisará armazenar em buffer apenas uma pequena quantidade de bytes (qualquer que seja o buffer intrínseco de pipe) entre os dois processos. Obviamente, isso não é realista sesmth
for um comando Unix / POSIX padrão, mas pode ser uma abordagem se você estiver escrevendo sozinho.Se isso não for possível,
find -print0 | xargs -0 smth
é provavelmente uma das melhores soluções. Como @ dave_thompson_085 mencionado nos comentários,xargs
divide os argumentos em várias execuções desmth
quando os limites do sistema são atingidos (por padrão, no intervalo de 128 KB ou qualquer limite imposto peloexec
sistema) e tem opções para influenciar quantas os arquivos são dados a uma chamada desmth
, portanto, é encontrado um equilíbrio entre o número desmth
processos e o atraso inicial.EDIT: removeu as noções de "melhor" - é difícil dizer se algo melhor surgirá. ;)
fonte
find ... -exec smth {} +
é a solução.find -print0 | xargs smth
não funciona, masfind -print0 | xargs -0 smth
(nota-0
) oufind | xargs smth
se os nomes de arquivos não têm aspas em espaço em branco ou a barra invertida executa umsmth
com o mesmo número de nomes de arquivos disponíveis e se encaixa em uma lista de argumentos ; se você exceder maxargs, ele será executadosmth
quantas vezes forem necessárias para lidar com todos os argumentos fornecidos (sem limite). Você pode definir 'pedaços' menores (portanto, um paralelismo um tanto anterior) com-L/--max-lines -n/--max-args -s/--max-chars
.Uma razão é que o espaço em branco lança uma chave de boca em andamento, fazendo com que o arquivo 'foo bar' seja avaliado como 'foo' e 'bar'.
Funciona bem se -exec usado em vez disso
fonte
find
como existe uma opção para executar um comando em cada arquivo, é facilmente a melhor opção.-exec ... {} \;
versus-exec ... {} +
for file in "$(find . -type f)"
eecho "${file}"
, em seguida, ele funciona mesmo com espaços em branco, outros caracteres especiais i adivinhar causa mais problemas emborafor file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";done
que deve (de acordo com você) imprimir cada nome de arquivo em uma linha separada precedida porname:
. Não faz.Como a saída de qualquer comando é uma única sequência, mas seu loop precisa de uma matriz de sequências para repetir. A razão pela qual "funciona" é que conchas traem a parte em branco do espaço para você.
Em segundo lugar, a menos que você precise de um recurso específico
find
, lembre-se de que seu shell provavelmente já pode expandir um padrão glob recursivo por si só e, crucialmente, que ele será expandido para uma matriz adequada.Exemplo de festança:
O mesmo em peixes:
Se você precisar dos recursos
find
, certifique-se de dividir apenas em NUL (como ofind -print0 | xargs -r0
idioma).Os peixes podem iterar a saída delimitada por NUL. Portanto, este não é realmente ruim:
Como uma última pegadinha, em muitos shells (não é claro, é claro), o loop sobre a saída do comando tornará o corpo do loop um subshell (o que significa que você não pode definir uma variável de qualquer maneira que seja visível após o encerramento do loop), o que é nunca o que você quer.
fonte
zsh
início dos anos 90 (embora você precise**/*
lá).fish
como implementações anteriores do recurso equivalente do bash, segue links simbólicos ao descer a árvore de diretórios. Consulte O resultado de ls *, ls ** e ls *** para obter as diferenças entre as implementações.Fazer um loop sobre a saída da descoberta não é uma prática ruim - o que é uma prática ruim (nesta e em todas as situações) é assumir que sua entrada é um formato específico, em vez de saber (testar e confirmar) que é um formato específico.
tldr / cbf:
find | parallel stuff
fonte