検索:大容量ファイル名のリストを効率的に検索する方法

検索:大容量ファイル名のリストを効率的に検索する方法

いくつかのリストで、デフォルトの名前が指定されている何百ものファイルを見つける必要があります(と呼びますbaseNames)。次に、基本名+指定された3つの拡張子を検索する必要があります。

例:入力リストから抽出されたデフォルト名の1つがで、指定されたFOO拡張子が.txt、、.csvと仮定されます.py。だから、、FOO.txtを見つける必要がありますFOO.csvFOO.py

私のbashスクリプトの現在の方法は次のとおりです。

for bn in ${baseNames}; do
  find ${searchDir} '(' -name "$bn.txt" -o -name "$bn.csv" -o -name "$bn.py" ')'
done

これは機能しますが、非効率的です。各デフォルト名に対してジョブfind全体を再実行する必要があります。searchDirこれにはかなりの数のファイルが含まれているため、時間がかかります。

findオプションまたはパイプで検索する必要があるファイルのリストを提供する方法はありますか?

明らかに知っているが、-name ... -or何百ものファイルがある場合、このアプローチは明らかに実用的ではありません。単純化のために拡張子を無視することもできます。 find.

答え1

配列を使用してください。例えば

#!/bin/bash

baseNames=(FOO BAR BAZ)

findNames=('(')
for bn in "${baseNames[@]}"; do
  for ext in txt csv py; do
    findNames+=("$bn.$ext" '-o' '-name')
  done
done
# replace the final '-o' and '-name' in the array with a close parenthesis
unset 'findNames[-1]'
findNames[-1]=')'
# If using a version of bash before v4.3, use:
#unset 'findNames[${#findNames[@]}-1]'
#findNames[${#findNames[@]}-1]=')'


declare -p findNames

出力は次のようになりますdeclare -p(改行とスペースを追加して読みやすくなりました)。

declare -a findNames=(
  [0]="("
    [1]="-name" [2]="FOO.txt" [3]="-o" [4]="-name" [5]="FOO.csv"
    [6]="-o" [7]="-name" [8]="FOO.py" [9]="-o" [10]="-name" [11]="BAR.txt"
    [12]="-o" [13]="-name" [14]="BAR.csv" [15]="-o" [16]="-name" [17]="BAR.py"
    [18]="-o" [19]="-name" [20]="BAZ.txt" [21]="-o" [22]="-name" [23]="BAZ.csv"
    [24]="-o" [25]="-name" [26]="BAZ.py"
  [27]=")"
)

で配列を使用するには、find次のことが必要です。

searchDir="./"
find "$searchDir" "${findNames[@]}"

これにより、次のfindコマンドが実行されます(読みやすくするために改行が追加されました)。

find ./ ( -name FOO.txt -o -name FOO.csv -o -name FOO.py \
  -o -name BAR.txt -o -name BAR.csv -o -name BAR.py \
  -o -name BAZ.txt -o -name BAZ.csv -o -name BAZ.py )

そしてここでエスケープする必要はありません。なぜなら、シェルはそれをサブシェルの起動を指示するのではなく、リテラル引数(配列はbash拡張)として扱うからです()シェルに入力する場合は、エスケープまたは引用する必要があります。

答え2

次のshスクリプトは、1行に1つの名前を含むファイルからデフォルトの名前を読み取りnames(名前にスペースなどが含まれている場合は引用符で囲む必要があります)、その名前(一度に50)を含むインラインスクリプトのバッチを呼び出します。sh -c。データが単一の呼び出しと比較して長すぎるリストに展開される場合に備えて、入力をバッチに分割します(合計結合長が入力データの長さを超えるfindコマンドを構成する必要があります。ここで、ファイル名のサフィックスの数は次のとおりです。同じです。探す)。nn

インラインスクリプトは、-name指定された基本名に基づいてテストの「ORリスト」を作成しますfind。各デフォルト名は、3つのファイル名のサフィックスとバリアントを含むリストに入力されます.txt.csv.py

このリストは場所パラメータリストに保存されます"$@"

リストが完成したら、find関数を呼び出してディレクトリ内または下のこれらの名前と一致する一般的なファイルを見つけます$topdir

topdir=$HOME

<names xargs -L 50 sh -c '
        topdir=$1; shift

        for name do
                for suffix in txt csv py; do
                        set -- "$@" -o -name "$name.$suffix"
                done
                shift  # shift off current base name
        done
        shift  # shift off the initial "-o"

        find "$topdir" -type f \( "$@" \) -print
' sh "$topdir"

50より小さい数字で実行し、sh -x -c代わりにsh -cインラインスクリプトが実際に実行しているコマンドを確認してください。


名前付き配列とシェルを使用するには、次の手順を実行しますbash

topdir=$HOME

<names xargs -L 50 bash -c '
        topdir=$1; shift
        unset tests

        for name do
                for suffix in txt csv py; do
                        tests+=( -o -name "$name.$suffix" )
                done
        done

        find "$topdir" -type f \( "${tests[@]:1}" \) -print
' bash "$topdir"

ここでは、tests位置引数リストの代わりに配列が使用されます。"${tests[@]:1}"最初の要素(例:)を除いて、配列要素のリストに展開するのは奇妙です-o

ただし、以下を使用する場合は、bashグロービングツール(元のシェルから継承)を使用することもできますksh

shopt -s extglob globstar dotglob nullglob

topdir=$HOME

printf -v pattern '%s/**/@(%s).@(txt|csv|py)' "$topdir" "$(paste -s -d '|' - <names)"

eval "pathnames=( $pattern )"

# The following loop is only for illustration.
# If you really just wanted to list the names, use
#     printf '%s\n' "${pathnames[@]}"

for pathname in "${pathnames[@]}"; do
        printf '%s\n' "$pathname"
done

これはファイルの内容に基づいて拡張されたワイルドカードパターンを構築しますnames。このパターンは、最終的に次のように見えることがあります。

/home/myself/**/@(name1|name2|name3).@(txt|csv|py)

...あなたが興味のある名前と一致します。 (ディレクトリなどで一般的なファイルをフィルタリングするために)、ループ内ですべてのファイルタイプテストを直接実行する必要があります。

スクリプトの上部に設定されているシェルオプションを使用すると、拡張モード@(...|...)()の使用、サブディレクトリ()の下向きの一致のextglob使用、非表示のサブディレクトリ()にある、または存在する名前を非表示にできます。また、一致するものがまったくない場合、パターンが消えるようにtaを設定しました。**globstardotglobnullglob

答え3

zshを使用する(引用符なしでこのパラメータを拡張すると、コードはzsh構文を使用します):

names=(foo bar baz)
exts=(txt csv py)
print -rC1 - **/(${(~j[|])names}).${(~j[|])exts})(ND)

ここで、aはグローバル演算子と見なされ、配列${(j[|])array}の要素を連結するために使用されます|。 .​|~NnullglobDdotglob

またはもちろんこれを行う:

print -rC1 - **/(foo|bar|baz).(cvs|py|txt)(ND)

一部のファイルで1行に1つの名前と拡張子を見つけた場合は、次のようにします。

names=( ${(f)"$(< names.txt)"} )
 exts=( ${(f)"$(< exts.txt)"}  )

次のようにすることもできます。

print -rC1 - **/$^names.$^exts(ND)

しかし、これは、名前+拡張の各組合せに対して再帰的グローブを拡張するので効率が悪い。

検索用find:

cmd=(find . '(') or=()
for name ($^names.$^exts) cmd+=($or -name ${(b)name}) or=(-o)
cmd+=(')')
$cmd

関連情報