Bash-部分的なファイル名のリストに対してファイルのディレクトリを確認します


8

クライアントごとに毎日ファイルをディレクトリに受信するサーバーがあります。ファイル名は次のように構成されています。

uuid_datestring_other-data

例えば:

d6f60016-0011-49c4-8fca-e2b3496ad5a7_20160204_023-ERROR
  • uuid 標準形式のuuidです。
  • datestringからの出力ですdate +%Y%m%d
  • other-data 長さは可変ですが、アンダースコアが含まれることはありません。

次の形式のファイルがあります。

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

ファイルにリストされているすべてのuuidに、bashを使用して、対応するファイルがディレクトリにあることを確認する必要があります。

これまでのところ、私はifステートメントを使用して間違った方向から来ているように感じ、ソースディレクトリ内のファイルをループ処理する必要があると感じています。

source_directoryおよびuuid_list変数は、スクリプトの前半で割り当てられています。

# 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}"

リスト内のファイルがディレクトリに存在することを確認するにはどうすればよいですか?可能な限りbash機能を使用したいのですが、必要に応じてコマンドを使用することに反対しているわけではありません。


Python?そして、サーバーディレクトリは「フラット」ですか?
Jacob Vlijm 2016

はい、フラットで、サブディレクトリはありません。できればbashだけを使いたい。
Arronical 2016

1
はい、投稿しません。
Jacob Vlijm 2016


私はあなたが持っているものの何が悪いのか本当にわかりません。UUIDまたはファイルのいずれかをループ処理する必要がありますが、なぜ一方のループがもう一方のループよりも優れているのですか?
terdon 2016

回答:


5

ファイルをウォークスルーし、名前に含まれるuuidに連想配列を作成します(パラメーター拡張を使用してuuidを抽出しました)。リストを読み取り、各UUIDの連想配列を確認し、ファイルが記録されたかどうかを報告します。

#!/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"

1
ナイス(+1)ですが、なぜこれがOPの処理よりも優れているのですか?あなたは同じ基本的なことをしているようですが、1つのステップではなく2つのステップです。
terdon 2016

1
@terdon:主な違いはこれが機能することです。
チョロバ2016

はい、それは重要な違いです。十分に公正です:)
terdon

これはすばらしい感謝です。私の+1をもらいました。ファイルを保持するディレクトリへのパスを含める方法はありますか?cdスクリプト内のディレクトリに移動できることはわかっていますが、知識を得るために疑問に思いました。
アロニカル

@Arronical:可能ですが、文字列からパスを削除する必要がありますfile=${file##*/}
チョロバ2016

5

これは、より「バッシー」で簡潔なアプローチです。

#!/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

上記はかなりの数でいくつかのファイルで正常に動作しますが、その速度はUUIDの数に依存し、多くを処理する必要がある場合は非常に遅くなること注意してください。その場合は、@ chorobaのソリューションを使用するか、本当に高速なものを得るには、シェルを回避して次のように呼び出します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

時間の違いを説明するために、18001が対応するファイル名を持つ20000 UUIDのファイルで、bashアプローチ、choroba's、およびperlをテストしました。各テストは、スクリプトの出力をにリダイレクトすることによって実行されたことに注意してください/dev/null

  1. 私のバッシュ(〜3.5分)

    real   3m39.775s
    user   1m26.083s
    sys    2m13.400s
  2. チョロバ(bash、〜0.7秒)

    real   0m0.732s
    user   0m0.697s
    sys    0m0.037s
  3. 私のperl(〜0.1秒):

    real   0m0.100s
    user   0m0.093s
    sys    0m0.013s

素晴らしく簡潔な方法の+1、これはファイルを含むディレクトリ内から実行する必要があります。cdスクリプトのディレクトリに移動できることはわかっていますが、ファイルパスを検索に含める方法はありますか?
アロニカル

@確かに、更新された回答を参照してください。${source_directory}スクリプトで行ったように使用できます。
terdon 2016

または"$2"、それを使用して、2番目の引数としてスクリプトに渡します。
アレクシス2016

これがあなたの目的のために十分速く実行されることを確認してください-このような多くのファイル検索の代わりに、単一のディレクトリスキャンでそれを実行する方が速いでしょう。
アレクシス2016

1
@alexisはい、あなたはまったく正しいです。私はいくつかのテストを行いましたが、UUID /ファイルの数が増えると、これは非常に遅くなります。私は、はるかに高速なperlアプローチ(bashスクリプト内から1つのライナーとして実行できるため、技術的には、創造的なネーミングを利用できる場合でもbashを追加する)を追加しました。
terdon 2016

3

これは純粋なBash(つまり、外部コマンドなし)であり、私が考えることができる最も簡潔なアプローチです。

しかし、パフォーマンスに関しては、現在のパフォーマンスよりもはるかに優れているわけではありません。

から各行を読み取りpath/to/fileます。各行について、最初のフィールドを格納し$uuid、パターンに一致するファイルpath/to/directory/$uuid*が見つからない場合はメッセージを出力します。

#! /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"

でそれを呼び出しpath/to/script path/to/file path/to/directoryます。

質問のサンプルファイルを含むテストディレクトリ階層の質問のサンプル入力ファイルを使用したサンプル出力:

% 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

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

ここでの考え方は、シェルが報告するエラーの報告について心配することではありません。<存在しないファイルを開こうとすると、シェルは文句を言うでしょう。実際、$0エラーが発生すると、スクリプトとエラーが発生した行番号がエラー出力の前に追加されます...これは、デフォルトですでに提供されている優れた情報です-気にしないでください。

また、そのようにファイルを1行ずつ入力する必要はありません。非常に遅くなる可能性があります。これにより、全体が1つのショットで空白で区切られた引数の配列に拡張され、一度に2つ処理されます。データが例と一致している場合は、$1常にuuidになり、$2になります$namebashuuidとの一致を開くことができ、そのような一致が1つしか存在しない場合は、次にprintf発生します。それ以外の場合はそうではなく、シェルはその理由についてstderrに診断を書き込みます。


1
@kos-ファイルは存在しますか?そうでない場合は、意図したとおりに動作します。空白で分割されunset IFS$(cat <uuid_file)いることを確認します。シェルが$IFS空白のみで構成されている場合、または設定されていない場合、シェルの分割方法は異なります。すべての空白シーケンスは単一のフィールド区切り文字としてのみ機能するため、このような分割展開にはnullフィールドはありません。各行に空白以外で区切られたフィールドが2つしかない限り、機能すると思います。でbash、とにかく、。set -f引用符で囲まれていない展開がグロブに対して解釈されないようにし、+ fを設定すると、後のグロブが確実に解釈されるようにします。
mikeserv 2016

@kos-直した。<>存在しないファイルが作成されるので、私は使用すべきではありませんでした。<私が意図したとおりに報告します。ただし、それが原因で発生する可能性のある問題と、そもそも私が誤って使用<>した理由は、それがリーダーのないパイプファイルの場合、またはラインバッファされたchar devのようにハングアップすることです。これは、エラー出力をより明示的に処理して実行することで回避できます[ -f "$dir/$1"* ]。ここではuuidについて話しているので、単一のファイル以上に拡張するべきではありません。失敗したファイルの名前をそのようにstderrに報告する方法は少しいいですが。
mikeserv 2016

@kos-実際、私はulimitを使用してファイルをまったく作成しないようにでき、<>それでもそのように使用できると思います... <>linuxでは読み取り/書き込みができるため、グロブがディレクトリに展開する場合に優れています失敗して言う-それはディレクトリです。
mikeserv 2016

@kos-ああ!申し訳ありません-私は馬鹿げているだけです-あなたは2つのマッチを持っているので、それは正しいことをしています。2つの一致があった可能性がある場合、そのようにエラーが発生することを意味します。これらはuuidであると想定されます。同じglobに一致する2つの類似した名前が存在する可能性はありません。それ 完全に意図的です-そして、それあるべきでない方法で曖昧です。意味が分かりますか グロブのファイルに名前を付けることは問題ではありません-ここに関連する特殊文字はありません-問題は、bash1つのファイルにのみ一致する場合にのみリダイレクトグロブを受け入れることです。man bashリダイレクトの下を参照してください。
mikeserv 2016

1

私がそれに取り組む方法は、最初にファイルからuuidを取得してから使用することです find

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

読みやすさのために

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

のファイルリストを使用した例/etc/。passwd、group、fstab、および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

ディレクトリがフラットであると説明したので、-printf "%f\n"ファイル名自体を印刷するオプションを使用できます

これが行わないことは、不足しているファイルをリストすることです。findの小さな欠点は、ファイルが見つからないかどうかを通知せず、何かと一致する場合のみです。しかし、できることは出力をチェックすることです-出力が空の場合、ファイルがありません

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

より読みやすい:

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

そして、これは小さなスクリプトとしてどのように機能するかです:

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

statフラットディレクトリであるため、代替として使用することもできますが、次のコードは、サブディレクトリを追加することにした場合、再帰的に機能しません。

$ 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

statアイデアを取り入れて実行すると、statの終了コードをファイルが存在するかどうかの指標として使用できます。事実上、これを実行したいのです。

$ 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

サンプル実行:

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.