Fazendo loop em arquivos com espaços nos nomes? [duplicar]

Esta pergunta já tem respostas aqui :

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:

  1. Por padrão, o shell divide a saída de um comando em espaços, tabulações e novas linhas
  2. Nomes de arquivos podem conter caracteres curinga que seria expandido
  3. 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 e read se você manipular find 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:

  1. Execute o comando find, pegue sua saída.
  2. Divida a saída find em palavras separadas. Qualquer caractere de espaço em branco é um separador de palavras.
  3. 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.

  1. O comando find retorna ./foo* bar.csv.
  2. O shell divide esta string no espaço, produzindo duas palavras: ./foo* e bar.csv.
  3. Desde ./foo* contém um metacaractere globbing, ele é expandido para a lista de arquivos correspondentes: ./foo 1.txt e ./foo 2.txt.
  4. Portanto, o loop for é executado sucessivamente com ./foo 1.txt, ./foo 2.txt e bar.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 usar find -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 em find -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 a echo 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 colocar read 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 "{}" \; com time find -type f -print0 | xargs -0 -I stuff cat stuff. A versão xargs foi mais rápida em 11 segundos ao processar 10.000 arquivos vazios. Tenha cuidado ao afirmar que, na maioria dos casos, combinar find 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 usar xargs 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 chamado Hello World.csv, file será definido como ./Hello e depois para World.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 chamado Hello 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 a echo "CHECKSTR `ls -l /root/somedir`".

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *