Paper Plane xUI(PPx)でfindやgrep結果の履歴をたどる

この記事は[5日目(Paper Plane xUI(PPx)からエントリーへのfind)](http://qiita.com/wordijp/items/4d73c77f2993c309e246)と[6日目(Paper Plane xUI(PPx)からエントリーへのgrep)](http://qiita.com/wordijp/items/d90a64b807a7b39e4d1b)の機能拡張についてです。


# findやgrep結果の履歴をたどる

これらのコマンドを連続して実行すると、履歴をたどって以前の結果へ巻き戻したくなる時があります。
検索ミスをした時のやりなおしであったり、「単語A」and「単語B」の後に「単語A」and「単語C」が検索したくなり、「単語A」の時まで遡りたい、など、色んな場面で欲しくなると思います。


## デモ

ここでは、10日目[Paper Plane xUI(PPx)のText Moduleに自作のコマンドを追加する](http://qiita.com/wordijp/items/2170dc656d68253a5261)で登場した、PPx Text Moduleの戻り値用のresultstringは何バイト分か、をfindとgrep、履歴を戻ったりして調べてみます。
狙ったわけではないのですが、今までの集大成みたいになっています。

resultstringは何バイト分?
![search_demo.gif](https://qiita-image-store.s3.amazonaws.com/0/51728/5eb68eb5-dc23-4278-8a28-684f073857a1.gif)

ざっくりとした流れは以下の通りです

1. PPxソースディレクトリから、VC++のソース一式をfind
2. VC++ソース一式から「resultstring」をgrep
3. エディタでresultstringに紐づいた「CMDLINESIZE」を調査
4. 履歴を一つ戻し、VC++ソース一式へ
5. VC++ソース一式から「CMDLINESIZE」をgrep
6. 5.結果のListFileから「define」をgrep(and検索)
7. #define CMDLINESIZE 0x400を見つける

ここでは3.の場面で、1.find→2.grepから1.findへと履歴を巻き戻しfind結果を再利用しています。
なお、履歴の行き来のショートカットキーは「Ctrl+Shift+←」「Ctrl+Shift+→」にしています、行き来した際は、ラインメッセージにその時の検索内容を出力するようにしています。

**※なおこの機能ですが、過去の履歴を遡っているだけなので、ファイルを追加・更新・削除処理が入ると結果が食い違う場合がありますのでご注意ください**

# ツール構成

## スクリプト

共通処理が多いため、スクリプトを分割しています。
**※あとで今までのも含めてアップします**

```js:lib/util.js
//!*script

(function() {
	var o = {};

	var _insts = new Object();
	o.getFs = function() { return _insts.fs = _insts.fs || new ActiveXObject("Scripting.FileSystemObject"); }

	// 現在のパスを取得
	// NOTE : ListFileの場合は実際のListFileファイルパスを取得する
	o.getCurrentPath = function() {
		return PPx.Extract('%FDV'); // NOTE : V:RealPath?(V無しの時にcinfo->RealPathを返すが意味が逆?)
	};

	// listfile名が履歴用と一致してるかどうか
	o.checkListFileName = function(base_listfile, history_listfile) {
		return history_listfile.replace(/[0-9]+$/g, "") == base_listfile;
	}

	///

	o.tryDeleteFile = function(fs, src) {
		if (fs.FileExists(src)) fs.DeleteFile(src);
	}

	o.tryMoveFile = function(fs, src, dest) {
		if (fs.FileExists(src)) fs.MoveFile(src, dest);
	}

	///

	// 独自の検索条件(MySearch)の内容を表示
	o.dispMySearch = function(fs, listfile) {
		var mysearch = false;
		var file = fs.OpenTextFile(listfile, 1);
		while (!file.AtEndOfStream) {
			var line = file.ReadLine();
			if (line.substring(0, 1) != ';') break;

			if (line.substring(0, ';MySearch'.length) == ';MySearch') {
				PPx.Execute('*linemessage ' + line);
				mysearch = true;
				break;
			}
		}
		if (!mysearch) PPx.Execute('*linemessage ;MySearch=*not found*');
		file.Close();
	}

	// ラインメッセージをAAで表現
	// ex) '< 4 < 3 < [2] < 1 <   S :'
	//     '    E 9 < [8] < 7 < 6 < :'
	//     '        E [9] < 8 < 7 < :'
	function _historyLineMessage(number, last_index, arrow) {
		function toMarkStr(idx) {
			return (idx > last_index) ? ' '
				: (idx == last_index) ? 'E'
				: (idx >= 0)          ? arrow
				: (idx == -1)         ? 'S'
				: ' ';
		}
		function toNoStr(idx) {
			return (idx > last_index) ? ' '
				: (idx > 0)           ? ('' + idx)
				: (idx == 0)          ? ' '
				: ' ';
		}

		return toMarkStr(number + 2) + ' ' + toNoStr(number + 2) + ' '
			+ toMarkStr(number + 1)  + ' ' + toNoStr(number + 1) + ' '
			+ toMarkStr(number)      + ' ' + '[' + toNoStr(number) + ']' + ' '
			+ toMarkStr(number - 1)  + ' ' + toNoStr(number - 1) + ' '
			+ toMarkStr(number - 2)  + ' ' + toNoStr(number - 2) + ' '
			+ toMarkStr(number - 3)  + ' ' + ':';
	}
	o.prevLineMessage = function(number, last_index) { return _historyLineMessage(number, last_index, '<'); }
	o.nextLineMessage = function(number, last_index) { return _historyLineMessage(number, last_index, '>'); }

	return o;
})();
```

```js:config.js
//!*script
var MAX_COUNT = 9; // 履歴最大数
```

```js:backuplistfile.js
//!*script

//
// ListFileを1世代バックアップする、最大9世代
//
// PPx.Arguments.item(0) : 対象のListFile
//

function _read(file) {
	var fs = new ActiveXObject("Scripting.FileSystemObject");
	var cwd = fs.GetParentFolderName(PPx.ScriptFullName);
	var fullpath = cwd + '\\' + file;
	if (!fs.FileExists(fullpath)) { PPx.Echo('* file not found:' + file); PPx.Quit(-1); }
	var f = fs.OpenTextFile(fullpath, 1);
	try { return f.ReadAll(); } finally { f.Close(); }
}

eval(_read('./config.js'));
var util = eval(_read('./lib/util.js'));

///

// ListFileを1世代バックアップする
function backupListFile(fs, listfile) {
	util.tryDeleteFile(fs, listfile + MAX_COUNT);
	for (var i = MAX_COUNT - 1; i > 0; i--) {
		util.tryMoveFile(fs, listfile + i, listfile + (i + 1));
	}
	util.tryMoveFile(fs, listfile, listfile + 1);
}

///

var base_listfile = PPx.Arguments.item(0);
if (base_listfile == '') {
	PPx.Echo('not found listfile path');
	PPx.Quit(-1);
}

// ファイルシステムオブジェクトの作成
var fs = util.getFs();


var history_listfile = util.getCurrentPath();
if (!util.checkListFileName(base_listfile, history_listfile)) {
	// 新規にListFileを作った場合

	// ListFileの書き込み
	// バックアップする
	backupListFile(fs, base_listfile);
} else {
	// ListFile上から履歴を更新する場合

	// 途中の履歴を削除する
	var find_i = -1;
	for (var i = 0; i < MAX_COUNT; i++) {
		if (i > 0) {
			var prev_listfile = base_listfile + ((i - 1 > 0) ? (i - 1) : '');
			util.tryDeleteFile(fs, prev_listfile);
		}

		var listfile = base_listfile + ((i > 0) ? i : '');
		if (listfile == history_listfile) {
			find_i = i;
			break;
		}
	}
	if (find_i > 0) {
		// XXX : 無駄なmovefileがある
		for (var dest_i = 0, src_i = find_i; src_i < MAX_COUNT; dest_i++, src_i++) {
			var src = base_listfile + ((src_i > 0) ? src_i : '');
			var dest = base_listfile + ((dest_i > 0) ? dest_i : '');
			util.tryMoveFile(fs, src, dest);
		}
	}
	backupListFile(fs, base_listfile);
}
```

```js:nextlistfilehistory.js
//!*script

//
// ListFileの履歴を進める
//
// PPx.Arguments.item(0) : 履歴用の基本ListFile
//

function _read(file) {
	var fs = new ActiveXObject("Scripting.FileSystemObject");
	var cwd = fs.GetParentFolderName(PPx.ScriptFullName);
	var fullpath = cwd + '\\' + file;
	if (!fs.FileExists(fullpath)) { PPx.Echo('* file not found:' + file); PPx.Quit(-1); }
	var f = fs.OpenTextFile(fullpath, 1);
	try { return f.ReadAll(); } finally { f.Close(); }
}

eval(_read('./config.js'));
var util = eval(_read('./lib/util.js'));

///

var fs = util.getFs();

var base_listfile = PPx.Arguments.item(0);
if (base_listfile == '') {
	PPx.Echo('not found listfile path');
	PPx.Quit(-1);
}

var history_listfile = util.getCurrentPath();
// listfile名が履歴用と一致してるかどうか
if (!util.checkListFileName(base_listfile, history_listfile)) {
	// 一致していない(= 履歴ListFileではない、通常のディレクトリ)
	// 履歴の最初へジャンプしておく
	history_listfile = base_listfile;
	if (fs.FileExists(base_listfile)) {
		PPx.Execute("%j" + base_listfile);
	}
}

var last_index = -1;
for (var i = 0; i < MAX_COUNT + 1; i++) {
	if (!fs.FileExists(base_listfile + ((i > 0) ? i : ""))) {
		break;
	}
	last_index = i;
}
if (last_index < 0) {
	var names = base_listfile.split(/[\\\/]/);
	PPx.Execute('*linemessage !"履歴ListFile(' + names[names.length - 1] + ')のindex取得に失敗');
	PPx.Quit(-1);
}

// 履歴番号を取得
var number = -999;
{
	var m = history_listfile.match(/[0-9]+$/);
	if (m) number = parseInt(history_listfile.substr(m.index));
}

// 進める
var success = false;
if (number > 0) {
	number--;

	var prev_listfile = base_listfile + ((number > 0) ? number : "");
	if (fs.FileExists(prev_listfile)) {
		PPx.Execute("%j" + prev_listfile);
		PPx.Execute('*linemessage !"' + util.nextLineMessage(number, last_index));
		util.dispMySearch(fs, prev_listfile);
		success = true;
	}
}

if (!success) PPx.Execute('*linemessage !"' + util.nextLineMessage(0, last_index) + 'ここまで');
```

```js:prevlistfilehistory.js
//!*script

//
// ListFileの履歴を巻き戻す
//
// PPx.Arguments.item(0) : 履歴用の基本ListFile
//

function _read(file) {
	var fs = new ActiveXObject("Scripting.FileSystemObject");
	var cwd = fs.GetParentFolderName(PPx.ScriptFullName);
	var fullpath = cwd + '\\' + file;
	if (!fs.FileExists(fullpath)) { PPx.Echo('* file not found:' + file); PPx.Quit(-1); }
	var f = fs.OpenTextFile(fullpath, 1);
	try { return f.ReadAll(); } finally { f.Close(); }
}

eval(_read('./config.js'));
var util = eval(_read('./lib/util.js'));

///

var fs = util.getFs();

var base_listfile = PPx.Arguments.item(0);
if (base_listfile == '') {
	PPx.Echo('not found listfile path');
	PPx.Quit(-1);
}

var history_listfile = util.getCurrentPath();
// listfile名が履歴用と一致してるかどうか
if (!util.checkListFileName(base_listfile, history_listfile)) {
	// 一致していない(= 履歴ListFileではない、通常のディレクトリ)
	// 履歴の最初へジャンプしておく
	history_listfile = base_listfile;
	if (fs.FileExists(base_listfile)) {
		PPx.Execute("%j" + base_listfile); // XXX : prevがある時、ここのジャンプが無駄になる
	}
}

var last_index = -1;
for (var i = 0; i < MAX_COUNT + 1; i++) {
	if (!fs.FileExists(base_listfile + ((i > 0) ? i : ""))) {
		break;
	}
	last_index = i;
}
if (last_index < 0) {
	var names = base_listfile.split(/[\\\/]/);
	PPx.Execute('*linemessage !"履歴ListFile(' + names[names.length - 1] + ')のindex取得に失敗');
	PPx.Quit(-1);
}

// 履歴番号を取得
var number = 0;
{
	var m = history_listfile.match(/[0-9]+$/);
	if (m) number = parseInt(history_listfile.substr(m.index));
}

// 巻き戻す
var success = false;
if (number < MAX_COUNT) {
	number++;

	var next_listfile = base_listfile + number;
	if (fs.FileExists(next_listfile)) {
		PPx.Execute("%j" + next_listfile);
		PPx.Execute('*linemessage !"' + util.prevLineMessage(number, last_index));
		util.dispMySearch(fs, next_listfile);
		success = true;
	}
}

if (!success) PPx.Execute('*linemessage !"' + util.prevLineMessage(last_index, last_index) + 'ここまで');
```

## PPx設定

過去に紹介した内容([5日目のfind](http://qiita.com/wordijp/items/4d73c77f2993c309e246)、[6日目のgrep](http://qiita.com/wordijp/items/d90a64b807a7b39e4d1b))に履歴関係の処理を追加して、さらにListFileをfind、grep専用名に変更しています。

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

```:PPX.CFG
K_edit	= {	; 一行編集/PPe兼用
^F	,
	*set dummy='エントリへのfind'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set dummy='---検索内容の入力-------------'
	*set ARG=%"_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'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | ruby %0\script\shell\tolistfile.rb ';MySearch=findx entry... %'ARG'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
^W	,%M_menuCustomGrep
^\RIGHT	,
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*script %0\script\nextlistfilehistory.js, %'SEARCH_LSTFILE'
^\LEFT	,
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*script %0\script\prevlistfilehistory.js, %'SEARCH_LSTFILE'
}

M_menuCustomGrep	= {	** comment **
grep -&l(ascii | utf8)(一致ファイル検索)	= 
	*set dummy='エントリへのgrep -l'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set ARG=%"xargs grep -lオプション(-i:大小無視,-E:拡張正規表現,-w:単語単位,-L:以外)"%{-i -E "%|word%|"%}
	*set CMD=xargs grep -l %'ARG'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | ruby %0\script\shell\tolistfile.rb ';MySearch=%'CMD'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
jvgrep -&l(日本語)(一致ファイル検索)	= 
	*set dummy='エントリへのjvgrep -l'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set ARG=%"xargs jvgrep -lオプション"%{"%|word%|"%}
	*set CMD=xargs jvgrep -8 -r --no-color -l %'ARG'
	*set LAWK=gawk '{gsub(/\\\\/, "/", $0); print "./" $0;}'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | %'LAWK' | ruby %0\script\shell\tolistfile.rb ';MySearch=%'CMD'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
grep -&n(ascii | utf8)(一致行検索)	= 
	*set dummy='エントリへのgrep -nH'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set ARG=%"xargs grep -nH --textオプション(-i:大小無視,-E:拡張正規表現,-w:単語単位,-v:以外)"%{-i -E "%|word%|"%}
	*set CMD=xargs grep -nH --text %'ARG'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | ruby %0\script\shell\tolistfilegrepn.rb ';MySearch=%'CMD'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
	*set dummy='loadevent.js'と設定を合わせる
	*viewstyle -temp GrepN
jvgrep -&n(日本語)(一致行検索)	= 
	*set dummy='エントリへのjvgrep -n'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set ARG=%"xargs jvgrepオプション(-v:以外)"%{"%|word%|"%}
	*set CMD=xargs jvgrep -8 -r --no-color -n %'ARG'
	*set NAWK=gawk '{idx=index($0, ":"); path=substr($0, 0, idx-1); str=substr($0, idx); gsub(/\\\\/, "/", path); print "./" path str;}'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | %'NAWK' | ruby %0\script\shell\tolistfile.rb ';MySearch=%'CMD'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
	*set dummy='loadevent.js'と設定を合わせる
	*viewstyle -temp GrepN
grep entry &string(エントリ文字列の検索)	= 
	*set dummy='エントリ文字列へのgrep'
	*set ENTRY_FILE=%'temp'\entryfile.tmp
	*set SEARCH_LSTFILE=%'temp'\searchlistfile.tmp
	*set ARG=%"grep --textオプション(-i:大小無視,-E:拡張正規表現,-w:単語単位,-v:以外)"%{-i -E "%|word%|"%}
	*set CMD=grep --text %'ARG'
	*script %0\script\entry2file.js, %'ENTRY_FILE'
	*script %0\script\backuplistfile.js, %'SEARCH_LSTFILE'
	%Obsq cat %'ENTRY_FILE' | nkf -Lu -w | %'CMD' | ruby %0\script\shell\tolistfile.rb ';MySearch=%'CMD'' | nkf -Lw -s > %'SEARCH_LSTFILE'
	*jumppath %'SEARCH_LSTFILE'
}
```