O problema
for f in $(find .)
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 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 -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 algumas xarg
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 de find
seria usar os zsh
suportes IFS=$'\0'
e:
IFS=$'\0'
for f in $(find . -print0)
(substitua -print0
com -exec printf '%s\0' {} +
para find
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
:
find . -exec something with {} \;
Ou se something
pode 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
, stdin
seja o pipe ou /dev/null
.
Um motivo que você pode querer usar é a -P
opção do GNU xargs
para processamento paralelo. O stdin
problema também pode ser contornado com o GNU, xargs
com a -a
opçã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 something
cada uma recebendo 20 argumentos de arquivo.
Com zsh
ou 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.4
e acima também podem armazenar arquivos retornados por find -print0
uma matriz com:
readarray -td '' files < <(find . -print0)
O zsh
equivalente (que tem a vantagem de preservar find
o status de saída):
files=(${(0)"$(find . -print0)"})
Com zsh
, você pode traduzir a maioria das find
expressões para uma combinação de globbing recursivo com qualificadores glob. Por exemplo, repetir find . -name '*.txt' -type f -mtime -1
seria:
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).
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 que bash
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 usando openat()
os O_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 vez cmd
executado, por exemplo, quando cmd ./foo/bar
ou cmd ./foo/bar ./foo/bar/baz
quando o cmd
uso for feito ./foo/bar
, os atributos de bar
podem não mais atender aos critérios correspondentes a find
, 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 {} +
onde find
espera ter arquivos suficientes para chamar cmd
).
Algumas find
implementações têm um -execdir
predicado (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 -- 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 comandos rm
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 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 cmd
aumentará 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 cmd
fizer nesse caminho falhará com um ENAMETOOLONG
erro.
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 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
seria cô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 e cє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 GNU find
).
find . -name '*.txt'
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, em
find . -name '????' -exec dash -c '
name=${1##*/}; echo "${#name}"' sh {} \;
find
encontrará com êxito os arquivos cujo nome consiste em 4 caracteres codificados em UTF-8, mas dash
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.
find . -exec yash -c 'echo "$1"' sh {} \;
em um local UTF-8 falhará em nosso ISO-8859-1 côté.txt
anteriormente, por exemplo.
Aqueles como bash
ou zsh
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) ou mksh -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:
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 $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.