はじめに

私の書いているコードは基本的に普段使い、おうち使い、その場しのぎ、等々のものです。
コピペは自由ですが、その結果については自己責任でお願いいたします。

やりたいこと

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を取り出したい、というケースになろうかと思います。

つまり、フォルダ構造を綺麗に保っておくか、事前に検索対象フォルダをきちんと選定するという処理を事前に組み込むかしないと、意図せず検索もれを起こしたりとか、フィルタリングする意味がなくなったりして、大変です。そこはそれぞれの環境次第なので、頑張ってください。
まあ深さでフィルタリングみたいな面倒な処理を省いてしまえばこんな苦労もないのですけれど。