Bash - Verifique o diretório em busca de arquivos em relação à lista de nomes parciais de arquivos

8

Eu tenho um servidor que recebe um arquivo por cliente todos os dias em um diretório. Os nomes de arquivos são construídos da seguinte maneira:

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 do formato:

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

Preciso verificar se todo uuid listado no arquivo tem 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, tanto quanto possível, mas não sou contra o uso de comandos, se necessário.

Arronical
fonte
Pitão? E o diretório do servidor é "simples"?
Jacob Vlijm
Sim, é plano, sem subdiretórios. Eu prefiro ficar apenas com o bash, se possível.
Arronical 04/04
1
Ok, eu não vou postar.
Jacob Vlijm
Eu realmente não vejo o que há de errado com o que você tem. Você precisará percorrer os UUIDs ou os arquivos. Por que um loop seria melhor que o outro?
terdon

Respostas:

5

Percorra os arquivos, crie uma matriz associativa sobre os uuids contidos em seus nomes (usei a expansão de parâmetros para extrair o uuid). Leia a lista, verifique a matriz associativa de 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"
choroba
fonte
1
Bom (+1), mas por que isso é melhor do que o OP estava fazendo? Você parece estar fazendo a mesma coisa básica, mas em duas etapas, em vez de uma.
terdon
1
@terdon: A principal diferença é que isso funciona :-) A expansão do curinga é feita apenas uma vez, não toda vez que você lê uma linha da lista, o que também pode ser mais rápido.
choroba
Sim, essa é uma diferença importante. Justo o suficiente :) #
306 terdon
Isso é maravilhoso, obrigado, recebi o meu +1. Existe alguma maneira de incluir o caminho para o diretório que contém os arquivos? Eu sei que posso cdentrar no diretório dentro do script, mas me perguntei apenas para obter conhecimento.
Arronical 04/04
@ Arronical: É possível, mas você terá que remover o caminho da string, possível com file=${file##*/}.
181 chorora
5

Aqui está uma abordagem mais "bashy" 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 lento se você precisar processar muitos. Se for esse o caso, use a solução do @ 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 horário, testei minha abordagem de bash, 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. Minha festança (~ 3,5 min)

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

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

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s
Terdon
fonte
+1 para um método fantasticamente conciso, isso teria que ser executado a partir do diretório que contém os arquivos. Sei que posso cdentrar no diretório do script, mas existe um método pelo qual o caminho do arquivo possa ser incluído na pesquisa?
Arronical 04/04
@ Arronical certeza, veja a resposta atualizada. Você pode usar ${source_directory}como estava usando no seu script.
terdon
Ou use "$2"e passe-o para o script como um segundo argumento.
Alexis
Verifique se isso é executado com rapidez suficiente para seus propósitos - seria mais rápido fazê-lo com uma única verificação de diretório, em vez de várias pesquisas de arquivos como esta.
Alexis
1
@exex sim, você está certo. Fiz alguns testes e isso fica muito lento se o número de UUIDs / arquivos aumentar. Adicionei uma abordagem perl (que pode ser executada como um liner a partir do script bash, portanto, tecnicamente, ainda bash se você estiver aberto a alguma nomeação criativa), que é muito mais rápida.
terdon
3

Este é o Bash puro (ou seja, sem comandos externos), e é a abordagem mais coincidente em que consigo pensar.

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

Ele lerá cada linha de path/to/file; para cada linha, ele armazenará o primeiro campo $uuide imprimirá uma mensagem se um arquivo correspondente ao padrão nãopath/to/directory/$uuid* for 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"

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

Saída de amostra usando o arquivo de entrada de amostra na pergunta em uma hierarquia de diretórios 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
kos
fonte
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 idéia aqui não é se preocupar em relatar erros que o shell reportará para você. Se você tentar <abrir um arquivo que não existe, seu shell irá reclamar. De fato, ele acrescentará o número do seu script $0e da linha em que o erro ocorreu à saída do erro quando ocorrer ... Essas são boas informações que já são fornecidas por padrão - portanto, não se preocupe.

Você também não precisa colocar o arquivo linha por linha assim - pode ser muito lento. Isso expande tudo em um único tiro para uma matriz de argumentos delimitada por espaço em branco e lida com dois de cada vez. Se seus dados forem consistentes com o seu exemplo, $1sempre será o seu uuid e $2será o seu $name. Se bashpode abrir uma correspondência para o seu uuid - e apenas existe uma dessas correspondências -, isso printfacontece. Caso contrário, isso não ocorre e o shell grava diagnósticos no stderr sobre o porquê.

mikeserv
fonte
1
@ kos - o arquivo existe? caso contrário, ele se comportará como pretendido. unset IFSgarante que $(cat <uuid_file)seja dividido em espaço em branco. Os shells dividem-se de maneira $IFSdiferente quando são compostos apenas por espaços em branco ou não estão definidos. Essas expansões de divisão nunca têm campos nulos porque todas as seqüências de espaço em branco permanecem como apenas um delimitador de campo. Contanto que existam apenas dois campos separados por espaços não brancos em cada linha, deve funcionar, eu acho. de bashqualquer maneira. set -fgarante que a expansão não citada não seja interpretada para globs e o conjunto + f garante que os globs posteriores sejam.
mikeserv
@ kos - eu apenas consertei. Eu não deveria estar usando <>porque isso cria um arquivo inexistente. <relatará como eu pretendia. o possível problema com isso - e a razão pela qual eu usei incorretamente <>- é que, se for um arquivo de pipe sem um leitor ou como um char dev com buffer de linha, ele travará. isso poderia ser evitado manipulando a saída de erro mais explicitamente e executando [ -f "$dir/$1"* ]. estamos falando de uuids aqui e, portanto, ele nunca deve se expandir para mais do que um único arquivo. é meio legal como ele relata os nomes dos arquivos com falha para stderr assim.
mikeserv
@kos - na verdade, suponho que eu poderia usar o ulimit para impedir a criação de arquivos e, portanto <>, ainda seria utilizável dessa maneira ... <>é melhor se a glob puder se expandir para um diretório, porque em um linux a leitura / gravação será falhe e diga - isso é um diretório.
mikeserv
@kos - oh! Sinto muito - só estou sendo burro - você tem duas partidas, e está fazendo a coisa certa. quero dizer que ele erra dessa maneira, se duas combinações puderem ser feitas, elas devem ser uuids - nunca deve haver a possibilidade de dois nomes semelhantes que coincidam com o mesmo globo. isso é totalmente intencional - e é ambíguo de uma maneira que não deveria ser. Voce entende o que eu quero dizer? nomear o arquivo como glob não é o problema, - caracteres especiais não são relevantes aqui - o problema é que bashsó aceitará um glob de redirecionamento se ele corresponder apenas a um arquivo. veja man bashem REDIRECÇÃO.
mikeserv
1

A maneira como eu abordaria isso é obter os uuids do arquivo primeiro e 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 facilitar a 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 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 -printf "%f\n"opção para imprimir o próprio nome do arquivo

O que isso não faz é listar os arquivos ausentes. findA pequena desvantagem é que ele não informa se não encontra um arquivo, apenas quando corresponde a algo. O que se poderia fazer, no entanto, é verificar a saída - se a saída estiver vazia, temos um arquivo ausente

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 statcomo 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 statideia e executá-la, poderíamos usar o código de saída stat como indicação para a existência ou não de um arquivo. Efetivamente, queremos fazer o seguinte:

$ 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

Exemplo de execução:

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
Sergiy Kolodyazhnyy
fonte