はじめに
私の書いているコードは基本的に普段使い、おうち使い、その場しのぎ、等々のものです。
コピペは自由ですが、その結果については自己責任でお願いいたします。
やりたいこと
AppleScriptでファイルを検索しようと思ったとき、まあFinderが第一候補であろうと思います。
何も考えずに書くと、こんな感じで再帰処理書けば良いわけですね。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | global return_list set return_list to {} set home to get path to home folder set target_folder to (((POSIX path of home) as text) & "desktop/") as POSIX file recSearch(target_folder as alias) if length of return_list = 0 then display alert "見つかりませんでした" else display alert concatByDelim(return_list, {return}) end if on recSearch(myFolder) tell application "Finder" set collect_flag to 0 set allFiles to every file of myFolder repeat with aFile in allFiles --検索条件に合致したら結果配列に追加 if (name of aFile contains "delete.jsx") then copy (aFile as alias) to the end of return_list --見つかった階層で処理を停止 set collect_flag to 1 end if end repeat -- get all folders set allFolders to every folder of myFolder end tell --再帰処理 if (collect_flag = 0) and (length of allFolders > 0) then repeat with aFolder in allFolders recSearch(aFolder) end repeat end if end recSearch |
再帰処理のいいところ
再帰処理のメリットとして、「ファイルを見つけたらそれ以上は深く潜らない」という処理が楽に書けるという点があります。
「深さはわからないけど、あることは確実だから、見つけたら教えてちょうだい」ということができて、時と場合によってはこれが非常に便利です。
再帰処理の(というかFinderの)だめなところ
処理が遅い。重い。tell Finderしてファイルを処理させようとすると、必要以上に時間がかかって、スクリプト的な快楽(?)を得られにくい傾向があります。私が業務で使用する場合は、予め検索先のフォルダを極力絞る処理を事前に入れてました。
代替案のご提案
代替案はいろいろ考えられる(mdfind、つまりspotlight検索をするとか、findコマンドで深さ指定するとか)わけですが、今回は、上記メリットである「ファイルを見つけたらそれ以上は深く潜らない」を擬似的に再現しつつ処理速度を上げる方法を考えます。
基本的戦略
今回もやることはシンプルです。
・do shell script でファイルを全て検索する。
・階層の深さでフィルタリングして、一番上の階層のファイルだけ取り出す。
これだけです。順に見ていきましょう。
findする
applescriptには do shell script という伝家の宝刀がありますので、これを利用します。
1 2 3 4 5 6 7 8 9 10 11 12 13 | set home to get path to home folder set target_folder to (((POSIX path of home) as text) & "desktop/k2script") as POSIX file try set res to "" set fileName to "delete.jsx" set command to "find '" & (POSIX path of target_folder) & "' -name '*" & fileName & "*' | grep -vsE -e '\\/\\. '" set res to do shell script command on error e if (e is not "0 以外の状況でコマンドが終了しました。") or (res is not "") then display alert e & return & res end if end try |
これで変数resに検索結果の文字列(findの出力)が格納されます。実験してみるとわかりますが、finderの再帰検索に比べて爆速です。
深さでフィルタリングする
さて、resにはfindの結果が入っているわけですが、同名のファイルがあれば複数行にわたる文字列を返します。
そこで、配列の中から階層が一番浅いものだけ取り出す以下のようなハンドラを実装しました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | --POSIXファイルパスを配列で渡す。最も階層の浅いものだけを集めた配列を返す。 on depthFilter(array) set min_depth to 100 set depth_record to {} set result_array to {} repeat with aPath in array set parsed to parseByDelim(aPath, {"/", ":"}) of me set cur_depth to length of parsed set cur_record to {filepath:aPath, depth:cur_depth} copy cur_record to the end of depth_record if cur_depth < min_depth then set min_depth to cur_depth end repeat repeat with aRec in depth_record if (depth of aRec) = min_depth then copy (filepath of aRec) as text to the end of result_array end if end repeat return result_array end depthFilter |
完成品と欠点
そこで、出来上がったコードは以下の通りです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 | set home to get path to home folder set target_folder to (((POSIX path of home) as text) & "desktop/k2script") as POSIX file try set res to "" set fileName to "delete.jsx" set command to "find '" & (POSIX path of target_folder) & "' -name '*" & fileName & "*' | grep -vsE -e '\\/\\. '" set res to do shell script command on error e if (e is not "0 以外の状況でコマンドが終了しました。") or (res is not "") then display alert e & return & res end if end try if res is not "" then set res to parseByDelim(res, {return}) of me set res to depthFilter(res) of me display alert res end if --文字列のうちaDelimをbDelimに置換した文字列を返す on textReplace(aText, aDelim, bDelim) set curDelim to AppleScript's text item delimiters set AppleScript's text item delimiters to aDelim set bList to text items of aText set AppleScript's text item delimiters to bDelim set bText to bList as text set AppleScript's text item delimiters to curDelim return bText end textReplace --配列aListを指定のデリミタaDelimで結合した文字列を返す。 on concatByDelim(aList, aDelim) set curDelim to AppleScript's text item delimiters set AppleScript's text item delimiters to aDelim set aData to aList as text set AppleScript's text item delimiters to curDelim return aData end concatByDelim --文字列aDataを指定のデリミタ配列aDelimで分割した配列を返す on parseByDelim(aData, aDelim) set curDelim to AppleScript's text item delimiters set AppleScript's text item delimiters to aDelim set dList to text items of aData set AppleScript's text item delimiters to curDelim return dList end parseByDelim --POSIXファイルパスを配列で渡す。最も階層の浅いものだけを集めた配列を返す。 on depthFilter(array) set min_depth to 100 set depth_record to {} set result_array to {} repeat with aPath in array set parsed to parseByDelim(aPath, {"/", ":"}) of me set cur_depth to length of parsed set cur_record to {filepath:aPath, depth:cur_depth} copy cur_record to the end of depth_record if cur_depth < min_depth then set min_depth to cur_depth end repeat repeat with aRec in depth_record if (depth of aRec) = min_depth then copy (filepath of aRec) as text to the end of result_array end if end repeat return result_array end depthFilter |
欠点
あくまで「階層が一番浅いもの」だけを取り出しているため、以下のようなフォルダ構造においては、対象ファイル2がフィルタリングされてしまいます。対象ファイル1だけ取り出せば良い、というケースなら良いのですが、下手すると検索漏れになってしまいます。
つまり、安全に利用可能なケースというのは基本的には以下のようなフォルダ構造で、対象ファイル1を取り出したい、というケースになろうかと思います。
つまり、フォルダ構造を綺麗に保っておくか、事前に検索対象フォルダをきちんと選定するという処理を事前に組み込むかしないと、意図せず検索もれを起こしたりとか、フィルタリングする意味がなくなったりして、大変です。そこはそれぞれの環境次第なので、頑張ってください。
まあ深さでフィルタリングみたいな面倒な処理を省いてしまえばこんな苦労もないのですけれど。