Como encontro quais arquivos estão faltando em uma lista?

9

Eu tenho uma lista de arquivos que quero verificar se eles existem no meu sistema de arquivos. Eu pensei em fazer isso usando findcomo em:

for f in $(cat file_list); do
find . -name $f > /dev/null || print $f
done

(usando zsh), mas isso não funciona, pois findparece sair 0se ele encontra ou não o arquivo. Eu acho que eu poderia passar por algum outro teste que testa para ver se findproduz alguma saída (bruto, mas eficaz seria substituí-lo > /dev/nullpor |grep ''), mas isso parece usar um troll para pegar uma cabra (outras nacionalidades podem dizer algo sobre marretas e nozes )

Existe uma maneira de coagir finda me dar um valor de saída útil? Ou pelo menos para obter uma lista daqueles arquivos que não foram encontrados? (Eu posso imaginar o último sendo talvez mais fácil por alguma escolha astuta de conectivos lógicos, mas pareço sempre ficar atolado quando tento descobrir isso.)

Antecedentes / Motivação: Eu tenho um backup "principal" e quero verificar se existem alguns arquivos na minha máquina local no meu backup principal antes de excluí-los (para criar um pouco de espaço). Então, fiz uma lista dos arquivos, ssheditei-os na máquina principal e fiquei sem saber por que descobrir a melhor maneira de encontrar os arquivos ausentes.

Andrew Stacey
fonte
Atualizei minha solução para usar o muito mais rápido locate.
usuário desconhecido
O @userunknown locatenão está mostrando o estado atual do sistema de arquivos, pode ser um dia ou até uma semana. Isso é adequado como base para testar backups.
Volker Siegel

Respostas:

5

findconsidera não encontrar nada como um caso especial de sucesso (nenhum erro ocorreu). Uma maneira geral de testar se os arquivos correspondem a alguns findcritérios é testar se a saída de findestá vazia. Para uma melhor eficiência quando houver arquivos correspondentes, use -quitno GNU find para fazê-lo sair na primeira partida ou head( head -c 1se disponível, caso contrário, o head -n 1que é padrão) em outros sistemas para fazê-lo morrer de um cano quebrado ao invés de produzir uma saída longa.

while IFS= read -r name; do
  [ -n "$(find . -name "$name" -print | head -n 1)" ] || printf '%s\n' "$name"
done <file_list

No bash ≥4 ou zsh, você não precisa do findcomando externo para uma correspondência simples de nome: você pode usar **/$name. Versão do Bash:

shopt -s nullglob
while IFS= read -r name; do
  set -- **/"$name"
  [ $# -ge 1 ] || printf '%s\n' "$name"
done <file_list

Versão Zsh em um princípio semelhante:

while IFS= read -r name; do
  set -- **/"$name"(N)
  [ $# -ge 1 ] || print -- "$name"
done <file_list

Ou aqui está uma maneira mais curta, porém mais enigmática, de testar a existência de um arquivo que corresponde a um padrão. O qualificador glob Ndeixa a saída vazia se não houver correspondência, [1]retém apenas a primeira correspondência ee:REPLY=true: altera cada correspondência para expandir para em 1vez do nome do arquivo correspondente. Então, **/"$name"(Ne:REPLY=true:[1]) falseexpande para true falsese houver uma correspondência ou apenas falsese não houver uma correspondência.

while IFS= read -r name; do
  **/"$name"(Ne:REPLY=true:[1]) false || print -- "$name"
done <file_list

Seria mais eficiente combinar todos os seus nomes em uma única pesquisa. Se o número de padrões não for muito grande para o limite de comprimento do sistema em uma linha de comando, você poderá juntar todos os nomes com-o , fazer uma única findchamada e pós-processar a saída. Se nenhum dos nomes contiver metacaracteres de shell (para que os nomes também sejam findpadrões), eis uma maneira de pós-processar com o awk (não testado):

set -o noglob; IFS='
'
set -- $(<file_list sed -e '2,$s/^/-o\
/')
set +o noglob; unset IFS
find . \( "$@" \) -print | awk -F/ '
    BEGIN {while (getline <"file_list") {found[$0]=0}}
    wanted[$0]==0 {found[$0]=1}
    END {for (f in found) {if (found[f]==0) {print f}}}
'

Outra abordagem seria usar o Perl e File::Find, o que facilita a execução do código Perl para todos os arquivos em um diretório.

perl -MFile::Find -l -e '
    %missing = map {chomp; $_, 1} <STDIN>;
    find(sub {delete $missing{$_}}, ".");
    print foreach sort keys %missing'

Uma abordagem alternativa é gerar uma lista de nomes de arquivos de ambos os lados e trabalhar em uma comparação de texto. Versão Zsh:

comm -23 <(<file_list sort) <(print -rl -- **/*(:t) | sort)
Gilles 'SO- parar de ser mau'
fonte
Estou aceitando este por duas razões. Eu gosto da zshsolução com a **sintaxe. É uma solução muito simples e, embora possa não ser a mais eficiente em termos de máquina , é provavelmente a mais eficiente em termos de eu realmente me lembrar dela! Além disso, a primeira solução aqui responde à pergunta real , pois ela se findtransforma em algo em que o código de saída distingue "eu consegui uma correspondência" de "eu não consegui uma correspondência".
Andrew Stacey
9

Você pode usar statpara determinar se existe um arquivo no sistema de arquivos.

Você deve usar as funções internas do shell para testar se existem arquivos.

while read f; do
   test -f "$f" || echo $f
done < file_list

O "teste" é opcional e o script realmente funciona sem ele, mas eu o deixei lá para facilitar a leitura.

Edit: Se você realmente não tem opção, a não ser trabalhar para uma lista de nomes de arquivos sem caminhos, sugiro que você crie uma lista de arquivos uma vez com o find, em seguida, itere-o com grep para descobrir quais arquivos estão lá.

find -type f /dst > $TMPFILE
while read f; do
    grep -q "/$f$" $TIMPFILE || echo $f
done < file_list

Observe que:

  • a lista de arquivos inclui apenas arquivos, não diretórios,
  • a barra no padrão de correspondência grep é para compararmos nomes de arquivos completos e não parciais,
  • e o último '$' no padrão de pesquisa é corresponder ao final da linha para que você não obtenha correspondências de diretório, apenas patches completos de nome de arquivo.
Caleb
fonte
stat precisa da localização exata, não é? Estou usando o find porque tenho apenas uma lista de nomes de arquivos e eles podem estar em vários diretórios. Desculpe se isso não estava claro.
Andrew Stacey
Hummm. Você não disse que tinha nomes de arquivos sem caminhos! Talvez você possa resolver esse problema? Seria muito mais eficiente do que a execução de encontrar várias vezes no mesmo conjunto de dados.
24511 Caleb
Obrigado pela edição e lamentamos novamente por não ser específico. O nome / caminho do arquivo não é algo que eu vou corrigir - os arquivos podem estar em lugares diferentes nos dois sistemas, então eu quero uma solução robusta o suficiente para contornar isso. O computador deve funcionar de acordo com minhas especificações, e não o contrário! Sério, isso não é algo que faço com frequência - eu estava procurando por alguns arquivos antigos para excluir para ganhar espaço e só queria uma maneira "rápida e suja" de garantir que eles estivessem nos meus backups.
Andrew Stacey
Antes de tudo, você não precisaria do caminho completo, apenas um caminho relativo para qualquer estrutura de diretório que estivesse fazendo backup. Permita-me sugerir que, se o caminho não for o mesmo, há uma boa chance de que o arquivo não seja o mesmo e você possa obter falsos positivos em seu teste. Parece que sua solução pode estar mais suja do que rápida; Não gostaria de vê-lo queimado pensando que tinha algo que não tinha. Além disso, se os arquivos são valiosos o suficiente para fazer backup em primeiro lugar, você não deve excluir as primárias, caso contrário, você precisa fazer backup de seus backups!
Caleb
Ak! Deixei de fora um monte de detalhes para tentar focar a questão e você os preenche com um monte de suposições que - devo dizer - são perfeitamente razoáveis, mas por acaso estão completamente erradas! Basta dizer que eu sei que, se o arquivo está lá e está em um diretório com um tipo específico de nome, sei que é o arquivo original e é seguro excluir a cópia na minha máquina.
Andrew Stacey
1

Uma primeira abordagem simplista poderia ser:

a) classifique sua lista de arquivos:

sort file.lst > sorted.lst 
for f in $(< sortd.lst) ; do find -name $f -printf "%f\n"; done > found.lst
diff sorted.lst found.lst

para encontrar missões ou

comm sorted.lst found.lst

encontrar correspondências

  • Armadilhas:
    • Novas linhas nos nomes de arquivos são muito difíceis de lidar
    • espaços em branco e coisas semelhantes nos nomes de arquivos também não são bons. Mas como você tem controle sobre os arquivos na lista de arquivos, talvez essa solução já seja suficiente, no entanto ...
  • Desvantagens:

    • Quando o find encontra um arquivo, ele continua sendo executado para encontrar outro e outro. Seria bom pular outras pesquisas.
    • O find pode procurar vários arquivos ao mesmo tempo, com alguma preparação:

      localize -name a.file -ou -name -b.file -ou -name c.file ...

Pode localizar ser uma opção? Novamente, uma lista pré-classificada de arquivos assumiu:

 for f in $(< sorted.tmp) ; do locate --regexp "/"$f"$" > /dev/null || echo missing $f ; done

Uma pesquisa por foo.bar não corresponderá ao arquivo foo.ba ou oo.bar com a construção --regexp-(não deve ser confundida com regex sem p).

Você pode especificar um banco de dados específico para localização e precisará atualizá-lo antes de pesquisar, se precisar dos resultados mais recentes.

Usuário desconhecido
fonte
1

Eu acho que isso pode ser útil também.

Esta é uma solução de uma linha, caso você opte pela "lista" de arquivos reais que deseja sincronizar com outra pasta:

function FUNCsync() { local fileCheck="$synchronizeTo/$1"; if [[ ! -f "$fileCheck" ]];then echo "$fileCheck";fi; };export -f FUNCsync;find "$synchronizeFrom/" -maxdepth 1 -type f -not -iname "*~" -exec bash -c 'FUNCsync "{}"' \; |sort

para ajudar a ler:

function FUNCsync() {
  local fileCheck="$synchronizeTo/$1";
  if [[ ! -f "$fileCheck" ]];then 
    echo "$fileCheck";
  fi; 
};export -f FUNCsync;
find "$synchronizeFrom/" -maxdepth 1 -type f -not -iname "*~" -exec bash -c 'FUNCsync "{}"' \; |sort

este exemplo exclui arquivos "* ~" de backup e limita ao tipo de arquivo regular "-type f"

Poder do Aquário
fonte
0
FIND_EXP=". -type f \( "
while read f; do
   FIND_EXP="${FIND_EXP} -iname $f -or"
done < file_list
FIND_EXP="${var%-or}"
FIND_EXP="${FIND_EXP} \)"
find ${FIND_EXP}

Talvez?

Hello71
fonte
0

Por que não simplesmente comparar o comprimento da lista de consultas com o comprimento da lista de resultados?

while read p; do
  find . -name $p 2>/dev/null
done < file_list.txt | wc -l
wc -l file_list.txt
Holger Brandl
fonte