Paper Plane xUI(PPx)からエントリーへのfind

この記事では、選択したエントリーへLinuxコマンドのfindを使って検索する方法を紹介します。

# ターミナル上のfindとの違い

- Pros :+1:
    - 選択したエントリーの検索ができる
        - 検索結果をからさらに選択をして、とインクリメンタルな検索
    - find結果の履歴が作れる(方法は後日の記事で)
    - ファイル名一覧にgrepが使える(これも後日の記事で)
        - ListFileへのgrepなので、ファイルアクセスが発生しない
    - agのファイル指定オプションやripgrepの--type-listのように、拡張子パターンを指定できる
        - 「cpp:」でC++ソースのみを絞り込み、等
        - [findargparser.js](#findargparserjs)に実装
    - .gitや.svn等をデフォルトで除外できる
        - コマンド群に実装してます
- Cons :-1_tone3:
    - PPx上で動くので、ターミナルは使えない
    - 当然シェルスクリプトも組み込めない

ただ、最近リリースされた検索コマンドは.gitignoreも参照してくれたりともっと強力な除外をしてくれるため、findで除外できてもメリットとしては薄いかもしれません。
欠点はコマンド群がスクリプト代わりになるので何とか補えると思います、それよりはfind結果に対してのエントリー操作がpecoよりも使い勝手がよくなるのが嬉しいです。


# デモ

![findtest.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/27d9cdb1-3a6d-afcd-0dc1-22a53384c80e.gif)
 
「.」ですべてのファイルを対象に、エントリー選択でそのエントリーのみを対象に検索できます。

-SEPinameというのは独自オプションです、-inameを毎回直接書くのは面倒なので、パーサ用スクリプト([findargparser.js](#findargparserjs))を通しています。
「-SEPiname "\*.cpp;\*.c"」は「\\( -iname "\*.cpp" -o -iname "\*.c" \\)」へ展開されます。
それと、ripgrepの--type-listのように拡張子パターンも指定できます、パターン名はファイル名にできない「:」を末尾に付けるルールにしています、「;」で区切って非パターンと混ぜる事も出来ます。
「-SEPiname "js:"」は「-SEPiname "\*.js;\*.jsx;\*.vue"」と変換され、そこから「\\( -iname "\*.js" -o -iname "\*.js" -o -iname "\*.vue" \\)」へと展開されます。

[findxコマンド](#findx)はGNU findの改造コマンドです、GNU findオリジナルだと複数エントリーをxargsを使って引数として渡す必要があり、エントリー数が増えると検索が目に見えて遅くなってしまいました、findのソースコードを読んでみましたがファイルパス一つ毎に検索するロジックになってました、約1000個のエントリーの検索で約10秒かかります、うーん遅い。
その為、標準出力からファイル一覧を受け取れるように'-'オプションを追加し、複数のファイルパスを検索処理に渡すように効率化しています、これにより約1000個のエントリーを約0.2秒ほどで完了するようになりました(それ以上は未測定です)。

# ツール構成

## findx

こちらで配布しています、バイナリはcygwin版しか用意できませんでした。
findutils改造品のためGPLです。
[wordijp/findutils-x](https://github.com/wordijp/findutils-x) - [Binary](https://github.com/wordijp/findutils-x/releases)

## tolistfile.rb

標準出力のListFile化で利用しています、Rubyで作成しています。

```rb:tolistfile.rb
# coding: utf-8
require 'kconv'

def putsUtf8(str)
  puts NKF.nkf('-w', str)
end

putsUtf8 ";ListFile"
putsUtf8 ";Base=" + `cwd`
# Windows用バイナリのRubyならこちらでも良い
#putsUtf8 ";Base=" + Dir.pwd.gsub(/\//, '\\')

ARGV.each{|arg|
  putsUtf8 arg
}

while STDIN.gets
  putsUtf8 $_
end
```

cygwin等のLinux版RubyでもWindows形式のパスが取得できるように、独自コマンドのcwdを作成しています、Windows版のRubyならDir.pwdを使っても問題ありません。

## cwd

こちらで配布しています。
[wordijp/cwd](https://github.com/wordijp/cwd) - [Binary](https://github.com/wordijp/cwd/releases)

## findargparser.js

-SEPiname等の独自オプションのパーサです、PPx Script Module用です。

```js:findargparser.js
//! script

//
// find用の引数を展開する
// 独自オプションを使えるようにするヘルパスクリプト
//
// PPx.Arguments.item(0) : 検索オプション
//
// usage)
//   -SEPname "*.cpp;*.h" ->
//     \( -name "*.cpp" -o -name "*.h" \)
//
//   -SEPname "cc:" ->
//     -SEPname "*.H;*.c;*.h" ->
//     \( -name "*.H" -o -name "*.c" -o -name "*.h" \)
//
//   -SEPname "cc:;h:" ->
//     -SEPname "*.H;*.c;*.h;*.h;*.hpp" ->
//     \( -name "*.H" -o -name "*.c" -o -name "*.h" -o -name "*.h" -o -name "*.hpp" \)


function argsJoin(args) {
	if (args.length == 0) return "";
	if (args.length == 1) return args[0];

	return '\\( ' + args.join(' -o ') + ' \\)';
}

///

// 独自オプション内の、ファイルパターンを展開する
// NOTE : ripgrepの--type-listを真似ている
function preReplacePattern(pattern) {
	pattern = ';' + pattern; // 先頭をなくし、常に/\Wxxx:/で単語単位検索になるように
	// C/C++
	pattern = pattern.replace(/\Wcpp:/, ';*.C;*.H;*.cc;*.cpp;*.cxx;*.h;*.hh;*.hpp');
	pattern = pattern.replace(/\Wcc:/, ';*.H;*.c;*.h');
	pattern = pattern.replace(/\Wh:/, ';*.h;*.hpp');
	// JavaScript
	pattern = pattern.replace(/\Wjs:/, ';*.js;*.jsx;*.vue');
	// その他
	// ...

	pattern = pattern.substring(1); // ';'を取り除く
	//PPx.Echo(pattern);

	var m = pattern.match(/\w+:/);
	if (m) {
		PPx.Echo('未実装のファイルパターンがあります\n>' + pattern.substring(m.index, m.lastIndex));
	}

	return pattern;
}

// 独自オプションをオリジナルへ置換する
function sepReplaceInternal(str, sepstr, restr, origname) {
	var m2 = sepstr.match(/"[^\"]*"/);
	if (!m2) return str;

	var pattern = sepstr.substring(m2.index + 1, m2.lastIndex - 1);
	pattern = preReplacePattern(pattern);
	var patterns = pattern.split(';');

	var origs = [];
	for (var i = 0; i < patterns.length; i++) {
		if (patterns[i] != "") origs.push(origname +' "' + patterns[i] + '"');
	}
	return str.replace(new RegExp(restr), argsJoin(origs));
}
function sepReplace(str, sepname, origname) {
	var restr = sepname + ' "[^\"]*"';
	while (true) {
		var m = str.match(new RegExp(restr));
		if (!m) break;

		var sepstr = str.substring(m.index, m.lastIndex);
		str = sepReplaceInternal(str, sepstr, restr, origname);
	}

	return str;
}

///

var result;
var arg = PPx.Arguments.item(0);
if (arg == "") {
	result = "";
} else {
	// 独自オプションの展開
	arg = sepReplace(arg, '-SEPiname', '-iname');
	arg = sepReplace(arg, '-SEPname', '-name');
	//PPx.Echo(arg);

	result = arg;
}

PPx.Result = result;
```

## PPx設定

いつものように差分のみです。
**※自身の設定が消えないように、適用前にバックアップを取るか、追加取り込みをしてください**
**※スクリプトのパスは自身の環境へと合わせて下さい**

```
KC_main	= {	; PPcメイン窓
^F	,
	*set dummy='エントリへのfind'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set CIN_LSTFILE=%'temp'\cin2listfile.tmp
	*set dummy='---検索内容の入力-------------'
	*set ARG=%"xargs findx entry... [オプション](-SEPname:;区切り、i付きで大小無視)"%{-SEPiname "%|*%|"%}
	*set EXTRACT_ARG=%*script(%0\script\findargparser.js, %'ARG')
	*set dummy='---コマンド内容---------------'
	*set OPT=\( -type d \( -name .git -o -name .svn \) -prune \( -wholename .git -wholename .svn \) -o -type f \)
	*set CMD_BAK=xargs -P4 -I'{}' find {} %'OPT' %'EXTRACT_ARG'
	*set CMD=findx - %'OPT' %'EXTRACT_ARG'
	*set dummy='---実行-----------------------'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | ruby %0\script\shell\tolistfile.rb ';MySearch=xargs findx entry... %'ARG'' | nkf -Lw -s > %'CIN_LSTFILE'
	*jumppath %'CIN_LSTFILE'
}
```

ListFileの「;MySearch ...」というメタデータを追加していますが、これは後の記事で利用しています。

# まとめ

標準のwhere isで物足りない方はご利用ください。