Bash - Verifique o diretório de arquivos na lista de nomes de arquivos parciais

8

Eu tenho um servidor que recebe um arquivo por cliente a cada dia em um diretório. Os nomes dos arquivos são construídos da seguinte forma:

uuid_datestring_other-data

Por exemplo:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid é um formato padrão uuid.
  • datestring é a saída de date +%Y%m%d .
  • other-data é variável em tamanho, mas nunca conterá um sublinhado.

Eu tenho um arquivo com o formato:

#
d6f60016-0011-49c4-8fca-e2b3496ad5a7    client1
d5873483-5b98-4895-ab09-9891d80a13da    client2
be0ed6a6-e73a-4f33-b755-47226ff22401    another_client
...

Eu preciso verificar se todos os uuid listados no arquivo têm um arquivo correspondente no diretório, usando o bash.

Cheguei até aqui, mas sinto que estou vindo da direção errada usando uma instrução if e que preciso percorrer os arquivos no diretório de origem.

As variáveis source_directory e uuid_list foram atribuídas anteriormente no script:

# Check the entries in the file list

while read -r uuid name; do
# Ignore comment lines
   [[ $uuid = \#* ]] && continue
   if [[ -f "${source_directory}/${uuid}*" ]]
   then
      echo "File for ${name} has arrived"
   else
      echo "PANIC! - No File for ${name}"
   fi
done < "${uuid_list}"

Como devo verificar se os arquivos da minha lista existem no diretório? Eu gostaria de usar a funcionalidade bash o mais longe possível, mas não sou contra o uso de comandos, se necessário.

    
por Arronical 04.02.2016 / 15:40

5 respostas

5

Percorra os arquivos, crie uma matriz associativa sobre os uuids contidos em seus nomes (usei a expansão de parâmetro para extrair o uuid). O, leia a lista, verifique o array associativo para cada uuid e relate se o arquivo foi gravado ou não.

#!/bin/bash
uuid_list=...

declare -A file_for
for file in *_*_* ; do
    uuid=${file%%_*}
    file_for[$uuid]=1
done

while read -r uuid name ; do
    [[ $uuid = \#* ]] && continue
    if [[ ${file_for[$uuid]} ]] ; then
        echo "File for $name has arrived."
    else
        echo "File for $name missing!"
    fi
done < "$uuid_list"
    
por choroba 04.02.2016 / 15:56
5

Aqui está uma abordagem mais "básica" e concisa:

#!/bin/bash

## Read the UUIDs into the array 'uuids'. Using awk
## lets us both skip comments and only keep the UUID
mapfile -t uuids < <(awk '!/^\s*#/{print $1}' uuids.txt)

## Iterate over each UUID
for uuid in ${uuids[@]}; do
        ## Set the special array $_ (the positional parameters: $1, $2 etc)
        ## to the glob matching the UUID. This will be all file/directory
        ## names that start with this UUID.
        set -- "${source_directory}"/"${uuid}"*
        ## If no files matched the glob, no file named $1 will exist
        [[ -e "$1" ]] && echo "YES : $1" || echo  "PANIC $uuid" 
done

Observe que, embora o acima seja bonito e funcione bem para alguns arquivos, sua velocidade depende do número de UUIDs e será muito lenta se você precisar processar muitos. Se for esse o caso, use a solução @ choroba ou, para algo realmente rápido, evite o shell e chame perl :

#!/bin/bash

source_directory="."
perl -lne 'BEGIN{
            opendir(D,"'"$source_directory"'"); 
            foreach(readdir(D)){ /((.+?)_.*)/; $f{$2}=$1; }
           } 
           s/\s.*//; $f{$_} ? print "YES: $f{$_}" : print "PANIC: $_"' uuids.txt

Apenas para ilustrar as diferenças de tempo, testei minha abordagem bash, a choroba e meu perl em um arquivo com 20000 UUIDs, dos quais 18001 tinham um nome de arquivo correspondente. Observe que cada teste foi executado redirecionando a saída do script para /dev/null .

  1. Meu bash (~ 3,5 min)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
    
  2. Choroba (bash, ~ 0,7 seg)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
    
  3. Meu perl (~ 0,1 seg):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
    
por terdon 04.02.2016 / 17:39
3

Isso é puro Bash (ou seja, sem comandos externos), e é a abordagem mais coincidente que eu posso imaginar.

Mas desempenho-sábio não é realmente muito melhor do que o que você tem atualmente.

Ele irá ler cada linha de path/to/file ; para cada linha, ele armazenará o primeiro campo em $uuid e imprimirá uma mensagem se um arquivo correspondente ao padrão path/to/directory/$uuid* for não encontrado:

#! /bin/bash
[ -z "$2" ] && printf 'Not enough arguments.\n' && exit

while read uuid; do
    [ ! -f "$2/$uuid"* ] && printf '%s missing in %s\n' "$uuid" "$2"
done <"$1"

Chame com path/to/script path/to/file path/to/directory .

Exemplo de saída usando o arquivo de entrada de amostra na pergunta em uma hierarquia de diretório de teste que contém o arquivo de amostra na pergunta:

% tree
.
├── path
│   └── to
│       ├── directory
│       │   └── d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
│       └── file
└── script.sh

3 directories, 3 files
% ./script.sh path/to/file path/to/directory
d5873483-5b98-4895-ab09-9891d80a13da* missing in path/to/directory
be0ed6a6-e73a-4f33-b755-47226ff22401* missing in path/to/directory
    
por kos 04.02.2016 / 16:13
3
unset IFS
set -f
set +f -- $(<uuid_file)
while  [ "${1+:}" ]
do     : < "$source_directory/$1"*  &&
       printf 'File for %s has arrived.\n' "$2"
       shift 2
done

A ideia aqui não é se preocupar em relatar erros que o shell informará para você. Se você tentar < abrir um arquivo que não existe, seu shell irá reclamar. Na verdade, será prefixado $0 do seu script e o número da linha na qual o erro ocorreu na saída do erro quando isso ocorrer ... Esta é uma boa informação que é fornecida por padrão já - então não se incomode.

Você também não precisa pegar o arquivo linha por linha, assim - pode ser muito lento. Isso expande a coisa toda em um único tiro para uma matriz delimitada de espaços em branco de argumentos e lida com dois de cada vez. Se os seus dados forem consistentes com o seu exemplo, então $1 será sempre seu uuid e $2 será seu $name . Se bash puder abrir uma correspondência para seu uuid - e somente um tal correspondência existe - então printf acontece. Caso contrário, isso não acontece e o shell grava diagnósticos para saber o motivo.

    
por mikeserv 05.02.2016 / 14:47
1

A maneira como eu me aproximo disso é obter os uuids do arquivo primeiro, depois usar find

awk '{print $1}' listfile.txt  | while read fileName;do find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null;done

Para leitura,

awk '{print $1}' listfile.txt  | \
    while read fileName;do \
    find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null;
    done

Exemplo com uma lista de arquivos em /etc/ , procurando por nomes de arquivos passwd, group, fstab e THISDOESNTEXIST.

$ awk '{print $1}' listfile.txt  | while read fileName;do find /etc -name "$fileName*" -printf "%p FOUND\n" 2> /dev/null; done
/etc/pam.d/passwd FOUND
/etc/cron.daily/passwd FOUND
/etc/passwd FOUND
/etc/group FOUND
/etc/iproute2/group FOUND
/etc/fstab FOUND

Como você mencionou que o diretório é plano, você pode usar a opção -printf "%f\n" para apenas imprimir o próprio nome do arquivo

O que isto não faz é listar arquivos perdidos. A pequena desvantagem do find é que ele não informa se ele não encontra um arquivo, apenas quando ele corresponde a algo. O que se pode fazer, no entanto, é verificar a saída - se a saída está vazia, então temos um arquivo faltando

awk '{print $1}' listfile.txt  | while read fileName;do RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; [ -z "$RESULT"  ] && echo "$fileName not found" || echo "$fileName found"  ;done

Mais legível:

awk '{print $1}' listfile.txt  | \
   while read fileName;do \
   RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; \
   [ -z "$RESULT"  ] && echo "$fileName not found" || \
   echo "$fileName found"  
   done

E aqui está como ele funciona como um pequeno script:

skolodya@ubuntu:$ ./listfiles.sh                                               
passwd found
group found
fstab found
THISDONTEXIST not found

skolodya@ubuntu:$ cat listfiles.sh                                             
#!/bin/bash
awk '{print $1}' listfile.txt  | \
   while read fileName;do \
   RESULT="$(find /etc -name "$fileName*" -printf "%p\n" 2> /dev/null )"; \
   [ -z "$RESULT"  ] && echo "$fileName not found" || \
   echo "$fileName found"  
   done

Pode-se usar stat como alternativa, já que é um diretório simples, mas o código abaixo não funcionará recursivamente para subdiretórios se você decidir adicioná-los:

$ awk '{print $1}' listfile.txt  | while read fileName;do  stat /etc/"$fileName"* 1> /dev/null ;done        
stat: cannot stat ‘/etc/THISDONTEXIST*’: No such file or directory

Se pegarmos a idéia de stat e rodarmos com ela, poderíamos usar o código de saída de stat como indicação para saber se um arquivo existe ou não. Efetivamente, queremos fazer isso:

$ awk '{print $1}' listfile.txt  | while read fileName;do  if stat /etc/"$fileName"* &> /dev/null;then echo "$fileName found"; else echo "$fileName NOT found"; fi ;done

Execução da amostra:

skolodya@ubuntu:$ awk '{print $1}' listfile.txt  | \                                                         
> while read FILE; do                                                                                        
> if stat /etc/"$FILE" &> /dev/null  ;then                                                                   
> echo "$FILE found"                                                                                         
> else echo "$FILE NOT found"                                                                                
> fi                                                                                                         
> done
passwd found
group found
fstab found
THISDONTEXIST NOT found
    
por Sergiy Kolodyazhnyy 04.02.2016 / 17:04