Comentários
- Não concordo que esta seja uma duplicata. A resposta aceita responde como fazer um loop sobre nomes de arquivos com espaços; isso não tem nada a ver com " por que está repetindo find ' s saída de má prática ". Eu encontrei esta questão (não a outra) porque eu preciso fazer um loop sobre nomes de arquivos com espaços, como em: for file in $ LIST_OF_FILES; do … onde $ LIST_OF_FILES não é a saída de find; é ' é apenas uma lista de nomes de arquivos (separados por novas linhas).
- @CarloWood – os nomes dos arquivos podem incluir novas linhas, então sua pergunta é única: repetindo uma lista de nomes de arquivos que podem conter espaços, mas não novas linhas. Acho que você ' terá que usar a técnica IFS para indicar que a interrupção ocorre em ' \ n '
- @ Diagonwoah, nunca percebi que nomes de arquivo podem conter novas linhas. Eu uso principalmente (apenas) linux / UNIX e até mesmo os espaços são raros; Certamente, nunca em toda a minha vida vi novas linhas sendo usadas: p. Eles também podem proibir isso imho.
- @CarloWood – nomes de arquivos terminam em nulo (' \ 0 ' , o mesmo que ' '). Qualquer outra coisa é aceitável.
- @CarloWood Você deve se lembrar que as pessoas votam primeiro e leem em segundo …
Resposta
Resposta curta (mais próxima da sua resposta, mas aceita espaços)
OIFS="$IFS" IFS=$"\n" for file in `find . -type f -name "*.csv"` do echo "file = $file" diff "$file" "/some/other/path/$file" read line done IFS="$OIFS"
Melhor resposta (também lida com curingas e novas linhas em nomes de arquivo)
find . -type f -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
Melhor resposta (com base em Gilles ” resposta )
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Ou ainda melhor, para evitar executar um sh
por arquivo:
find . -type f -name "*.csv" -exec sh -c " for file do echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty done " exec-sh {} +
Resposta longa
Você tem três problemas:
- Por padrão, o shell divide a saída de um comando em espaços, tabulações e novas linhas
- Nomes de arquivos podem conter caracteres curinga que seria expandido
- E se houvesse um diretório cujo nome termine em
*.csv
?
1. Dividindo apenas em novas linhas
Para descobrir o que definir file
, o shell deve pegar a saída de find
e interpretá-lo de alguma forma, caso contrário, file
seria apenas o resultado completo de find
.
O shell lê a variável IFS
, que é definida como <space><tab><newline>
por padrão.
Em seguida, examina cada caractere na saída de find
. Assim que vê qualquer caractere que “está em IFS
, ele pensa que marca o final do nome do arquivo, então define file
para quaisquer caracteres que viu até agora e executa o loop. Em seguida, começa de onde parou para obter o próximo nome de arquivo e executa o próximo loop, etc., até atingir o final da saída.
Portanto, está efetivamente fazendo isso:
for file in "zquery" "-" "abc" ...
Para dizer para dividir a entrada apenas em novas linhas, você precisa fazer
IFS=$"\n"
antes do seu for ... find
comando.
Isso define IFS
como um nova linha única, por isso só se divide em novas linhas, e não em espaços e tabulações também.
Se você estiver usando sh
ou dash
em vez de ksh93
, bash
ou zsh
, você precisa escrever IFS=$"\n"
assim:
IFS=" "
Isso provavelmente é o suficiente para fazer seu script funcionar, mas se você “estiver interessado em lidar com alguns outros casos secundários corretamente, continue lendo …
2. Expandindo $file
sem curingas
Dentro do loop, onde você faz
diff $file /some/other/path/$file
o shell tenta expandir $file
(de novo!).
Ele pode conter espaços, mas como já definimos IFS
acima, isso não será um problema aqui.
Mas também pode conter caracteres curinga, como *
ou ?
, o que levaria a um comportamento imprevisível. (Obrigado a Gilles por apontar isso.)
Para dizer ao shell para não expandir os caracteres curinga, coloque a variável entre aspas duplas, por exemplo,
diff "$file" "/some/other/path/$file"
O mesmo problema também pode nos afetar
for file in `find . -name "*.csv"`
Por exemplo, se você tivesse esses três arquivos
file1.csv file2.csv *.csv
(muito improvável, mas ainda possível)
Seria como se você tivesse executado
for file in file1.csv file2.csv *.csv
que será expandido para
for file in file1.csv file2.csv *.csv file1.csv file2.csv
causando file1.csv
e file2.csv
para ser processado duas vezes.
Em vez disso, temos que fazer
find . -name "*.csv" -print | while IFS= read -r file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty done
read
lê as linhas da entrada padrão, divide a linha em palavras de acordo com IFS
e as armazena nos nomes de variáveis que você especificar.
Aqui, estamos contando não dividir a linha em palavras e armazenar a linha em $file
.
Observe também que mudou para read line </dev/tty
.
Isso ocorre porque dentro do loop, a entrada padrão vem de find
por meio do pipeline.
Se apenas fizéssemos read
, isso consumiria parte ou todo o nome de um arquivo e alguns arquivos seriam ignorados .
/dev/tty
é o terminal a partir do qual o usuário está executando o script. Observe que isso causará um erro se o script for executado via cron, mas presumo que isso não seja importante neste caso.
Então, e se um nome de arquivo contiver novas linhas?
Podemos lidar com isso alterando -print
para -print0
e usando read -d ""
no final de um pipeline:
find . -name "*.csv" -print0 | while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read char </dev/tty done
Isso faz com que find
coloque um byte nulo no final de cada nome de arquivo. Bytes nulos são os únicos caracteres não permitidos em nomes de arquivo, então isso deve lidar com todos os nomes de arquivo possíveis, não importa o quão estranho seja.
Para obter o nome do arquivo do outro lado, usamos IFS= read -r -d ""
.
Onde usamos read
acima, usamos o delimitador de linha padrão de nova linha, mas agora, find
está usando null como delimitador de linha. Em bash
, você não pode “passar um caractere NUL em um argumento para um comando (mesmo os internos), mas bash
entende -d ""
como significando delimitado por NUL . Portanto, usamos -d ""
para fazer read
use o mesmo delimitador de linha que find
. Observe que -d $"\0"
, aliás, também funciona, porque bash
não suportando bytes NUL, trata-o como uma string vazia.
Para ser correto, também adicionamos -r
, que diz que não manipula barras invertidas em nomes de arquivos especialmente. Por exemplo, sem -r
, \<newline>
são removidos e \n
é convertido em n
.
Uma maneira mais portátil de escrever isso que não requer bash
ou zsh
ou lembrando-se de todas as regras acima sobre bytes nulos (novamente, graças a Gilles):
find . -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read char </dev/tty " exec-sh {} ";"
* 3. Ignorando diretórios cujo nomes terminados em .csv
find . -name "*.csv"
também corresponderão a diretórios chamados something.csv
.
Para evitar isso, adicione -type f
ao comando find
.
find . -type f -name "*.csv" -exec sh -c " file="$0" echo "$file" diff "$file" "/some/other/path/$file" read line </dev/tty " exec-sh {} ";"
Como glenn jackman aponta, em ambos os exemplos, os comandos a serem executados para cada arquivo são sendo executado em um subshell, então se você alterar quaisquer variáveis dentro do loop, elas serão esquecidas.
Se você precisar definir variáveis e mantê-las ainda definidas no final do loop, você pode reescrevê-lo para usar a substituição de processo como esta:
i=0 while IFS= read -r -d "" file; do echo "file = $file" diff "$file" "/some/other/path/$file" read line </dev/tty i=$((i+1)) done < <(find . -type f -name "*.csv" -print0) echo "$i files processed"
Observe que se você tentar copiar e colar isso na linha de comando , read line
consumirá o echo "$i files processed"
, de modo que o comando não será executado.
Para evitar isso, você poderia remover read line </dev/tty
e enviar o resultado para um pager como less
.
NOTAS
Eu removi os pontos e vírgulas (;
) dentro do ciclo. Você pode colocá-los de volta se quiser, mas eles não são necessários.
Atualmente, $(command)
é mais comum do que `command`
. Principalmente porque é mais fácil escrever $(command1 $(command2))
do que `command1 \`command2\``
.
read char
realmente não lê um caractere.Ele lê uma linha inteira, então eu mudei para read line
.
Comentários
- colocando
while
em um pipeline pode criar problemas com o subshell criado (variáveis no bloco de loop não visíveis após a conclusão do comando, por exemplo). Com o bash, eu usaria redirecionamento de entrada e substituição de processo:while read -r -d $'\0' file; do ...; done < <(find ... -print0)
- Claro, ou usando um heredoc:
while read; do; done <<EOF "$(find)" EOF
. No entanto, não é tão fácil de ler. - @glenn jackman: Tentei adicionar mais explicações agora. Acabei de torná-lo melhor ou pior?
- Você não ' t precisa
IFS, -print0, while
eread
se você manipularfind
totalmente, como mostrado abaixo em minha solução. - Sua primeira solução lidará com qualquer caractere, exceto nova linha se você também desativar o globbing com
set -f
.
Resposta
Este script falhará se qualquer nome de arquivo contiver espaços ou caracteres globais de shell \[?*
. O comando find
gera um nome de arquivo por linha. Em seguida, a substituição do comando `find …`
é avaliada pelo shell da seguinte maneira:
- Execute o comando
find
, pegue sua saída. - Divida a saída
find
em palavras separadas. Qualquer caractere de espaço em branco é um separador de palavras. - Para cada palavra, se for um padrão globbing, expanda-o para a lista de arquivos correspondentes.
Por exemplo, suponha que haja três arquivos no diretório atual, chamados `foo* bar.csv
, foo 1.txt
e foo 2.txt
.
- O comando
find
retorna./foo* bar.csv
. - O shell divide esta string no espaço, produzindo duas palavras:
./foo*
ebar.csv
. - Desde
./foo*
contém um metacaractere globbing, ele é expandido para a lista de arquivos correspondentes:./foo 1.txt
e./foo 2.txt
. - Portanto, o loop
for
é executado sucessivamente com./foo 1.txt
,./foo 2.txt
ebar.csv
.
Você pode evitar a maioria dos problemas neste estágio diminuindo a divisão e a conversão de palavras saindo de globbing. Para diminuir a divisão de palavras, defina a variável IFS
para um único caractere de nova linha; desta forma, a saída de find
só será dividida em novas linhas e os espaços permanecerão. Para desligar o globbing, execute set -f
. Então, esta parte do código funcionará, desde que nenhum nome de arquivo contenha um caractere de nova linha.
IFS=" " set -f for file in $(find . -name "*.csv"); do …
(Isso não é parte do seu problema, mas eu recomendamos o uso de $(…)
em vez de `…`
. Eles têm o mesmo significado, mas a versão de crase tem regras estranhas.)
Há outro problema abaixo: diff $file /some/other/path/$file
deve ser
diff "$file" "/some/other/path/$file"
Caso contrário, o valor de $file
é dividido em palavras e as palavras são tratadas como padrões globais, como com o comando substitutio acima. Se você deve se lembrar de algo sobre a programação shell, lembre-se disto: sempre use aspas duplas em torno de expansões de variáveis ($foo
) e substituições de comando ( $(bar)
) , a menos que você saiba que deseja dividir. (Acima, sabíamos que queríamos dividir a find
saída em linhas.)
Uma maneira confiável de chamar find
está dizendo a ele para executar um comando para cada arquivo que encontrar:
find . -name "*.csv" -exec sh -c " echo "$0" diff "$0" "/some/other/path/$0" " {} ";"
Neste caso, outra abordagem é comparar os dois diretórios, embora seja necessário exclua explicitamente todos os arquivos “enfadonhos”.
diff -r -x "*.txt" -x "*.ods" -x "*.pdf" … . /some/other/path
Comentários
- I ' d se esqueceu dos curingas como outro motivo para citar corretamente. Obrigado! 🙂
- em vez de
find -exec sh -c 'cmd 1; cmd 2' ";"
, você deve usarfind -exec cmd 1 {} ";" -exec cmd 2 {} ";"
, porque o shell precisa mascarar os parâmetros, mas localizar não ' t. No caso especial aqui, echo " $ 0 " não ' não precisa ser um parte do script, basta anexar -print após';'
. Você não ' não incluiu uma pergunta para prosseguir, mas mesmo isso pode ser feito por find, conforme mostrado abaixo em minha argumentação. 😉 - @userunknown: O uso de
{}
como substring de um parâmetro emfind -exec
não é portátil, é ' por que o shell é necessário.Eu não ' não entendo o que você quer dizer com “o shell precisa mascarar os parâmetros”; se ' é sobre citações, minha solução está citada corretamente. Você ' está certo que aecho
parte pode ser executada por-print
em vez disso.-okdir
é uma extensão GNU find recente, ' não está disponível em todos os lugares. Não ' não incluí a espera para continuar porque considero que a IU extremamente pobre e o solicitante podem facilmente colocarread
no snippet de shell se ele quer. - Citar é uma forma de mascarar, não ' é isso? Eu não ' não entendo sua observação sobre o que é portátil e o que não é. Seu exemplo (2º de baixo) usa -exec para invocar
sh
e usa{}
– então, onde está meu exemplo (ao lado de -okdir) menos portátil?find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print
- “Mascarar” não é ' uma terminologia comum na literatura shell, então você ' Terei de explicar o que você quer dizer se quiser ser compreendido. Meu exemplo usa
{}
apenas uma vez e em um argumento separado; outros casos (usados duas vezes ou como substring) não são portáteis. “Portátil” significa que ' funcionará em todos os sistemas unix; uma boa diretriz é a POSIX / especificação Unix única .
Resposta
Estou surpreso por não ver readarray
mencionado. Isso torna muito fácil quando usado em combinação com <<<
operador:
$ touch oneword "two words" $ readarray -t files <<<"$(ls)" $ for file in "${files[@]}"; do echo "|$file|"; done |oneword| |two words|
Usar a construção <<<"$expansion"
também permite que você divida variáveis contendo novas linhas em matrizes, como :
$ string=$(dmesg) $ readarray -t lines <<<"$string" $ echo "${lines[0]}" [ 0.000000] Initializing cgroup subsys cpuset
readarray
está no Bash há anos, então esta provavelmente deve ser a maneira canônica de fazer isso no Bash.
Resposta
Afaik find tem tudo que você precisa.
find . -okdir diff {} /some/other/path/{} ";"
O find se preocupa em chamar os programas de forma segura. -okdir irá perguntar antes do diff (você tem certeza que sim / não).
Nenhum shell envolvido, nenhum globbing, jokers, pi, pa, po.
Como nota: se você combinar find com for / while / do / xargs, na maioria dos casos, y você está fazendo errado. 🙂
Comentários
- Obrigado pela resposta. Por que você está fazendo errado se combinar find com for / while / do / xargs?
- Find já itera sobre um subconjunto de arquivos. A maioria das pessoas que aparecem com perguntas podem usar apenas uma das ações (-ok (dir) -exec (dir), -delete) em combinação com "; " ou + (posteriormente para chamada paralela). A principal razão para fazer isso é que você não ' não precisa mexer nos parâmetros do arquivo, mascarando-os para o shell. Não é tão importante: você não precisa ' t novos processos o tempo todo, menos memória, mais velocidade. programa mais curto.
- Não aqui para esmagar seu espírito, mas compare:
time find -type f -exec cat "{}" \;
comtime find -type f -print0 | xargs -0 -I stuff cat stuff
. A versãoxargs
foi mais rápida em 11 segundos ao processar 10.000 arquivos vazios. Tenha cuidado ao afirmar que, na maioria dos casos, combinarfind
com outros utilitários é errado.-print0
e-0
existem para lidar com espaços nos nomes dos arquivos usando um byte zero como separador de item em vez de um espaço. - @JonathanKomar: Seu comando find / exec levou 11,7 s em meu sistema com 10.000 arquivos, o xargs versão 9.7 s,
time find -type f -exec cat {} +
como sugerido em meu comentário anterior levou 0,1 s. Observe a diferença sutil entre " e está errado " e " você ' está fazendo errado ", especialmente quando decorado com um sorriso. Você, por exemplo, fez errado? 😉 BTW, espaços no nome do arquivo não são problema para o comando acima e find em geral. Programador cult da carga? E, a propósito, combinar encontrar com outras ferramentas é bom, apenas xargs é na maioria das vezes supérfluo. - @userunknown Expliquei como meu código lida com espaços para a posteridade (educação de futuros visualizadores), e foi não implicando que seu código não. O
+
para chamadas paralelas é muito rápido, como você mencionou. Eu não diria programador de culto à carga, porque essa capacidade de usarxargs
dessa forma é útil em várias ocasiões. Eu concordo mais com a filosofia Unix: faça uma coisa e faça-a bem (use programas separadamente ou em combinação para fazer um trabalho).find
está caminhando sobre uma linha tênue aí.
Resposta
Percorrer todos os arquivos ( qualquer caractere especial incluído) com o localização completamente segura (consulte o link para documentação):
exec 9< <( find "$absolute_dir_path" -type f -print0 ) while IFS= read -r -d "" -u 9 do file_path="$(readlink -fn -- "$REPLY"; echo x)" file_path="${file_path%x}" echo "START${file_path}END" done
Comentários
- Obrigado por mencionar
-d ''
. Não ' não percebi que$'\0'
era o mesmo que''
, mas parece que estar. Boa solução, também. - Gosto da dissociação de find e while, thanks.
Resposta
Estou surpreso que ninguém tenha mencionado a solução zsh
óbvia aqui ainda:
for file (**/*.csv(ND.)) { do-something-with $file }
((D)
para incluir também arquivos ocultos, (N)
para evitar o erro se não houver correspondência, (.)
para restringir a arquivos regulares .)
bash4.3
e acima agora oferece suporte parcial também:
shopt -s globstar nullglob dotglob for file in **/*.csv; do [ -f "$file" ] || continue [ -L "$file" ] && continue do-something-with "$file" done
Resposta
Nomes de arquivos com espaços parecem vários nomes na linha de comando se ” não está entre aspas. Se o seu arquivo se chamar “Hello World.txt”, a linha diff se expande para:
diff Hello World.txt /some/other/path/Hello World.txt
que se parece com quatro nomes de arquivo. Basta inserir citações em torno dos argumentos:
diff "$file" "/some/other/path/$file"
Comentários
- Isso ajuda, mas não ' para resolver meu problema. Ainda vejo casos em que o arquivo está sendo dividido em vários tokens.
- Essa resposta é enganosa. O problema é o comando
for file in `find . -name "*.csv"`
. Se houver um arquivo chamadoHello World.csv
,file
será definido como./Hello
e depois paraWorld.csv
. Citar$file
ganhou ' t ajuda.
Resposta
Aspas duplas são suas.
diff "$file" "/some/other/path/$file"
Caso contrário, o conteúdo da variável será dividido por palavras.
Comentários
- Isso é enganoso. O problema é o comando
for file in `find . -name "*.csv"`
. Se houver um arquivo chamadoHello World.csv
,file
será definido como./Hello
e, em seguida,World.csv
. Citando$file
ganhou ' t ajuda.
Resposta
Com bash4, você também pode usar a função embutida mapfile para definir uma matriz contendo cada linha e iterar nesta matriz.
$ tree . ├── a │ ├── a 1 │ └── a 2 ├── b │ ├── b 1 │ └── b 2 └── c ├── c 1 └── c 2 3 directories, 6 files $ mapfile -t files < <(find -type f) $ for file in "${files[@]}"; do > echo "file: $file" > done file: ./a/a 2 file: ./a/a 1 file: ./b/b 2 file: ./b/b 1 file: ./c/c 2 file: ./c/c 1
Resposta
Os espaços nos valores podem ser evitados com a construção simples de loop
for CHECK_STR in `ls -l /root/somedir` do echo "CHECKSTR $CHECK_STR" done
ls -l root / algumedir c mantém meu arquivo com espaços
Saída acima do meu arquivo com espaços
para evitar esta saída, solução simples (observe as aspas duplas)
for CHECK_STR in "`ls -l /root/somedir`" do echo "CHECKSTR $CHECK_STR" done
enviar meu arquivo com espaços
experimentado no bash
Comentários
- “Looping through files ”- é o que diz a pergunta. Sua solução irá gerar a saída inteira
ls -l
de uma vez . É efetivamente equivalente aecho "CHECKSTR `ls -l /root/somedir`"
.