Por que repetir as práticas inadequadas de saída do find?

170

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 findque 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?

don_crissti
fonte
12
Eu acho que isso é algo como "Nunca analise ls output!" - você certamente pode fazer qualquer um de uma só vez, mas eles são mais um hack rápido do que a qualidade da produção. Ou, mais geralmente, definitivamente nunca será dogmático.
precisa
Isso deve ser transformado em uma resposta canônica
Zaid
6
Porque o ponto de descoberta é repetir o que encontra.
OrangeDog
2
Um ponto auxiliar - você pode enviar a saída para um arquivo e processá-la posteriormente no script. Dessa forma, a lista de arquivos está disponível para revisão, se você precisar depurar o script.
user117529

Respostas:

87

O problema

for f in $(find .)

combina duas coisas incompatíveis.

findimprime 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 in zsh)) e executa globbing em cada palavra resultante (exceto in zsh) (e até pare de expandir os derivados ksh93 ou pdksh!).

Mesmo se você fizer isso:

IFS='
' # split on newline only
set -o noglob # disable glob (also disables brace expansion in pdksh
              # but not ksh93)
for f in $(find .) # invoke split+glob

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 -printsimplesmente 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 findtotalmente 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 cmdhá problemas semelhantes (há espaços em branco, nova linha, aspas simples, aspas duplas e barra invertida (e com algumas xargimplementaçõ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 forloop na saída de findseria usar os zshsuportes IFS=$'\0'e:

IFS=$'\0'
for f in $(find . -print0)

(substitua -print0com -exec printf '%s\0' {} +para findimplementaçõ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:

find . -exec something with {} \;

Ou se somethingpode levar mais de um argumento:

find . -exec something with {} +

Se você precisar que a lista de arquivos seja manipulada por um shell:

find . -exec sh -c '
  for file do
    something < "$file"
  done' find-sh {} +

(cuidado, pode iniciar mais de um sh).

Em alguns sistemas, você pode usar:

find . -print0 | xargs -r0 something with

embora isso tenha pouca vantagem sobre a sintaxe padrão e os meios something, stdinseja o pipe ou /dev/null.

Um motivo que você pode querer usar é a -Popção do GNU xargspara processamento paralelo. O stdinproblema também pode ser contornado com o GNU, xargscom a -aopção com shells que suportam a substituição do processo:

xargs -r0n 20 -P 4 -a <(find . -print0) something

por exemplo, para executar até 4 chamadas simultâneas de somethingcada uma recebendo 20 argumentos de arquivo.

Com zshou bash, outra maneira de fazer um loop sobre a saída de find -print0é com:

while IFS= read -rd '' file <&3; do
  something "$file" 3<&-
done 3< <(find . -print0)

read -d '' lê registros delimitados NUL em vez de registros delimitados por nova linha.

bash-4.4e acima também podem armazenar arquivos retornados por find -print0uma matriz com:

readarray -td '' files < <(find . -print0)

O zshequivalente (que tem a vantagem de preservar findo status de saída):

files=(${(0)"$(find . -print0)"})

Com zsh, você pode traduzir a maioria das findexpressões para uma combinação de globbing recursivo com qualificadores glob. Por exemplo, repetir find . -name '*.txt' -type f -mtime -1seria:

for file (./**/*.txt(ND.m-1)) cmd $file

Ou

for file (**/*.txt(ND.m-1)) cmd -- $file

(cuidado com a necessidade de --como **/*, os caminhos dos arquivos não estão começando ./, portanto, podem começar com, -por exemplo).

ksh93e, basheventualmente, 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 que bashantes 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/ zshencontrar 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( findPelo menos GNU ) faz isso abrindo os diretórios usando openat()os O_NOFOLLOWsinalizadores corretos (onde houver suporte) e mantendo um descritor de arquivo aberto para cada diretório, zsh/ bash/ kshnã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 findque desça o diretório corretamente, com -exec cmd {} \;e ainda mais com -exec cmd {} +, uma vez cmdexecutado, por exemplo, quando cmd ./foo/barou cmd ./foo/bar ./foo/bar/bazquando o cmduso for feito ./foo/bar, os atributos de barpodem não mais atender aos critérios correspondentes a find, mas ainda pior, ./foopodem ter sido substituído por um link simbólico para outro lugar (e a janela da corrida é aumentada com -exec {} +onde findespera ter arquivos suficientes para chamar cmd).

Algumas findimplementações têm um -execdirpredicado (ainda não padronizado) para aliviar o segundo problema.

Com:

find . -execdir cmd -- {} \;

find chdir()s no diretório pai do arquivo antes de executar cmd. Em vez de chamar cmd -- ./foo/bar, ele chama cmd -- ./bar( cmd -- barcom algumas implementações, daí a --), para ./fooevitar o problema de ser alterado para um link simbólico. Isso torna o uso de comandos rmmais 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 GNU find, é 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:

find . -exec cmd {} \;

o tamanho do caminho indicado cmdaumentará com a profundidade do diretório em que o arquivo está. Se esse tamanho for maior que PATH_MAX(algo como 4k no Linux), qualquer chamada do sistema que cmdfizer nesse caminho falhará com um ENAMETOOLONGerro.

Com -execdir, apenas o nome do arquivo (possivelmente prefixado ./) é passado para cmd. Os nomes dos arquivos na maioria dos sistemas de arquivos têm um limite muito menor ( NAME_MAX) do que PATH_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 finde, 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 74seria côté.txtpara um aplicativo que interpreta esse nome de arquivo em um código de idioma em que o conjunto de caracteres é ISO-8859-1 e cєtщ.txtem 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/ -pathpredicados (e mais, como -inameou -regexcom algumas implementações).

O que isso significa é que, por exemplo, com várias findimplementações (incluindo GNU find).

find . -name '*.txt'

não encontrou nosso 63 f4 74 e9 2e 74 78 74arquivo 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:

  1. 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, em

    find . -name '????' -exec dash -c '
      name=${1##*/}; echo "${#name}"' sh {} \;
    

    findencontrará com êxito os arquivos cujo nome consiste em 4 caracteres codificados em UTF-8, mas dashreportará comprimentos que variam entre 4 e 24.

  2. 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.

    find . -exec yash -c 'echo "$1"' sh {} \;
    

    em um local UTF-8 falhará em nosso ISO-8859-1 côté.txtanteriormente, por exemplo.

  3. Aqueles como bashou zshonde 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) )

  4. Aqueles como o shdo FreeBSD (11 no mínimo) ou mksh -o utf8-modeque suportam multi-bytes, mas apenas para UTF-8.

Notas

1 Para completar, poderíamos mencionar uma maneira hacky de zshfazer loop sobre arquivos usando globbing recursivo sem armazenar a lista inteira na memória:

process() {
  something with $REPLY
  false
}
: **/*(ND.m-1+process)

+cmdé um qualificador global que chama cmd(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 $REPLYou retornar vários arquivos em uma $replymatriz). Aqui fazemos o processamento nessa função e retornamos false para que o arquivo não seja selecionado.

Stéphane Chazelas
fonte
Se zsh eo bash estão disponíveis, você pode ser melhor fora apenas usando englobamento e shell construções em vez de tentar se contorcer findpara se comportar de forma segura. O globbing é seguro por padrão, enquanto a localização é insegura por padrão.
Kevin
@ Kevin, veja editar.
Stéphane Chazelas 11/11
182

Por que o loop over findé uma má prática?

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 finde 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 findinclui um -print0primário que usará um byte nulo para delimitar os nomes de arquivos impressos, o GNU find pode ser usado com segurança com o GNU xargse seu -0sinalizador (e -rsinalizador) para lidar com a saída de find:

find ... -print0 | xargs -r0 ...

No entanto, não há um bom motivo para usar este formulário, porque:

  1. Ele adiciona uma dependência do GNU findutils, que não precisa estar lá, e
  2. findfoi desenvolvido para executar comandos nos arquivos encontrados.

Além disso, o GNU xargsrequer -0e -r, enquanto o FreeBSD xargsrequer apenas -0(e não tem -ropção), e alguns xargsnão suportam -0. Portanto, é melhor manter os recursos do POSIX find(consulte a próxima seção) e pular xargs.

Quanto ao ponto 2 find- a capacidade de executar comandos nos arquivos encontrados - acho que Mike Loukides disse o melhor:

findO negócio da empresa é avaliar expressões - não localizar arquivos. Sim, findcertamente localiza arquivos; mas isso é realmente apenas um efeito colateral.

- Ferramentas elétricas Unix


Usos especificados POSIX de find

Qual é a maneira correta de executar um ou mais comandos para cada um dos findresultados?

Para executar um único comando para cada arquivo encontrado, use:

find dirname ... -exec somecommand {} \;

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:

find dirname ... -exec somecommand {} \; -exec someothercommand {} \;

Para executar um único comando em vários arquivos de uma vez:

find dirname ... -exec somecommand {} +

find em combinação com sh

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 -cconstrução. Você deve saber algumas coisas sobre isso:

  • Nunca incorpore {}diretamente no shcó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 parafind :

    Se um nome_da_utilização ou sequência de argumentos contiver os dois caracteres "{}", mas não apenas os dois caracteres "{}", será definido pela implementação se find substituirá esses dois caracteres ou usará a sequência sem alteração.

    ... Se houver mais de um argumento contendo os dois caracteres "{}", o comportamento não será especificado.

  • Os argumentos após a sequência de comandos do shell passada para a -copçã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 $0valor "fictício" , como find-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 $0significaria que o primeiro arquivo passado seria definido $0e, portanto, não incluído "$@".


Para executar um único comando shell por arquivo, use:

find dirname ... -exec sh -c 'somecommandwith "$1"' find-sh {} \;

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:

find dirname ... -exec sh -c 'for f do somecommandwith "$f"; done' find-sh {} +

(Observe que for f doé equivalente for f in "$@"; doe lida com cada um dos parâmetros posicionais por sua vez - em outras palavras, ele usa cada um dos arquivos encontrados por find, independentemente de quaisquer caracteres especiais em seus nomes.)


Outros exemplos de finduso correto :

(Nota: fique à vontade para estender esta lista.)

Curinga
fonte
5
Há um caso em que não conheço uma alternativa à findsaí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 -u3e 3<existe caso algo dentro do loop tente ler stdin.
Gordon Davisson
1
@GordonDavisson, talvez, mas o que você precisa definir essas variáveis para ? Eu diria que o que quer que seja, deve ser tratado dentro da find ... -execchamada. Ou apenas use um shell glob, se ele manipular seu caso de uso.
Curinga
1
Muitas vezes, quero imprimir um resumo após o processamento dos arquivos ("2 convertidos, 3 ignorados, os seguintes arquivos tiveram erros: ...") e essas contagens / listas precisam ser acumuladas nas variáveis ​​do shell. Além disso, há situações em que desejo criar uma matriz de nomes de arquivos para que eu possa fazer coisas mais complexas do que iterar na ordem (nesse caso, é filelist=(); while ... do filelist+=("$file"); done ...).
Gordon Davisson
3
Sua resposta está correta. No entanto, eu não gosto do dogma. Embora eu conheça melhor, há muitos casos de uso (especialmente interativos) em que é seguro e mais fácil digitar loop over findoutput ou pior ainda ls. 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.
Rudimeier
16
@rudimeier, o argumento sobre dogma vs. melhores práticas já foi feito até a morte . Não interessado. Se você usá-lo de forma interativa e funcionar, tudo bem, é bom para você - mas não vou promover isso. A porcentagem de autores de scripts que se preocupam em aprender o que é um código robusto e, em seguida, fazem apenas isso ao escrever scripts de produção, em vez de apenas fazer o que costumam fazer de maneira interativa, é extremamente mínima. A manipulação é promover as melhores práticas o tempo todo. As pessoas precisam aprender que existe uma maneira correta de fazer as coisas.
Curinga
10

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

for file in `find . -type f -name ...`; do smth with ${file}; done

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 finde analisada forantes mesmo de executar sua primeira smth.

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 findseja executado paralelamente ao seu smth, e apenas mantenha o nome do arquivo atual na RAM enquanto ele o entrega smth.

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 um smthprocesso por arquivo. Se smthsó pode funcionar em um arquivo, é assim que deve ser.

Se possível, a solução ideal seria find -print0 | smth: smthpoder processar nomes de arquivos em seu STDIN. Então, você terá apenas um smthprocesso, 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 se smthfor 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, xargsdivide os argumentos em várias execuções de smthquando os limites do sistema são atingidos (por padrão, no intervalo de 128 KB ou qualquer limite imposto pelo execsistema) e tem opções para influenciar quantas os arquivos são dados a uma chamada de smth, portanto, é encontrado um equilíbrio entre o número de smthprocessos e o atraso inicial.

EDIT: removeu as noções de "melhor" - é difícil dizer se algo melhor surgirá. ;)

AnoE
fonte
find ... -exec smth {} +é a solução.
Curinga
find -print0 | xargs smthnão funciona, mas find -print0 | xargs -0 smth(nota -0) ou find | xargs smthse os nomes de arquivos não têm aspas em espaço em branco ou a barra invertida executa um smthcom o mesmo número de nomes de arquivos disponíveis e se encaixa em uma lista de argumentos ; se você exceder maxargs, ele será executado smthquantas 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.
Dave_thompson_085
4

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'.

$ ls -l
-rw-rw-r-- 1 ec2-user ec2-user 0 Nov  7 18:24 foo bar
$ for file in `find . -type f` ; do echo filename $file ; done
filename ./foo
filename bar
$

Funciona bem se -exec usado em vez disso

$ find . -type f -exec echo filename {} \;
filename ./foo bar
$ find . -type f -exec stat {} \;
  File: ‘./foo bar’
  Size: 0               Blocks: 0          IO Block: 4096   regular empty file
Device: ca01h/51713d    Inode: 9109        Links: 1
Access: (0664/-rw-rw-r--)  Uid: (  500/ec2-user)   Gid: (  500/ec2-user)
Access: 2016-11-07 18:24:42.027554752 +0000
Modify: 2016-11-07 18:24:42.027554752 +0000
Change: 2016-11-07 18:24:42.027554752 +0000
 Birth: -
$
Steve
fonte
Especialmente no caso de findcomo existe uma opção para executar um comando em cada arquivo, é facilmente a melhor opção.
Centimane 7/11
1
Considere também -exec ... {} \;versus-exec ... {} +
thrig
1
se você usar for file in "$(find . -type f)" e echo "${file}", em seguida, ele funciona mesmo com espaços em branco, outros caracteres especiais i adivinhar causa mais problemas embora
mazs
9
@ amazs - não, citar não faz o que você pensa. Em um diretório com vários arquivos, tente o for file in "$(find . -type f)";do printf '%s %s\n' name: "${file}";doneque deve (de acordo com você) imprimir cada nome de arquivo em uma linha separada precedida por name:. Não faz.
111316 don_crissti
2

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:

shopt -s nullglob globstar
for i in **
do
    echo «"$i"»
done

O mesmo em peixes:

for i in **
    echo «$i»
end

Se você precisar dos recursos find, certifique-se de dividir apenas em NUL (como o find -print0 | xargs -r0idioma).

Os peixes podem iterar a saída delimitada por NUL. Portanto, este não é realmente ruim:

find -print0 | while read -z i
    echo «$i»
end

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.

user2394284
fonte
@don_crissti Precisamente. Não geralmente funcionam. Eu estava tentando ser sarcástico dizendo que "funciona" (com aspas).
user2394284
Observe que o globbing recursivo se originou no zshinício dos anos 90 (embora você precise **/*lá). fishcomo 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.
Stéphane Chazelas
1

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

Jan Kyu Peblik
fonte