JLine3のHistoryのメモ。
Historyは、JLine3でユーザーが入力した文字列の履歴を扱うクラス(インターフェース)。
import org.jline.reader.History;
HistoryはLineReaderで保持される。
デフォルトではDefaultHistoryというクラスが使われる。
LineReaderのreadLineメソッドで読み込まれた文字列がHistoryに登録される。
デフォルトでは、カーソルキーの上で過去に入力した文字列が表示される。下を押すとその履歴より後の文字列が表示される。
Ctrl+Rで、文字列の先頭を入力してそれにマッチする履歴だけを表示できる。
これらのキーバインドはLineReaderImplで定義されている。
(デフォルトはemacsバインドになっていて、viバインドにも変更できる模様)
bind(emacs, DOWN_LINE_OR_HISTORY, ctrl('N')); bind(emacs, UP_LINE_OR_HISTORY, ctrl('P')); bind(emacs, HISTORY_INCREMENTAL_SEARCH_BACKWARD, ctrl('R')); bind(emacs, HISTORY_INCREMENTAL_SEARCH_FORWARD, ctrl('S'));
Historyは、入力された履歴をファイルへ読み書き(保存)する機能を備えている。
→履歴をファイルに保存する例
履歴の取得に関係するHistoryのメソッドは何種類かある。
分類 | メソッド | 説明 |
---|---|---|
全体 | int size() |
保持している履歴の個数 |
boolean isEmpty() |
履歴が空かどうか | |
String toString() |
全履歴を文字列にして返す。 | |
取得 | int first() |
先頭の履歴のインデックス |
int last() |
最後の履歴のインデックス | |
String get(int index) |
指定されたインデックスの履歴(文字列)を取得する。 | |
ListIterator<Entry> iterator() |
履歴のIterator | |
Iterator<Entry> reverseIterator() |
履歴のIterator(逆順) | |
ナビゲーション (たぶん内部的に使用するもの) |
int index() |
現在のインデックス |
String current() |
現在の位置の履歴(文字列)を取得する。 | |
boolean moveTo(int index) |
現在のインデックスを指定された位置にする。 | |
boolean moveToFirst() |
現在のインデックスを履歴の先頭にする。 | |
boolean moveToLast() |
現在のインデックスを履歴の最後にする。 | |
void moveToEnd() |
||
boolean previous() |
現在のインデックスをひとつ前に移動する。 | |
boolean next() |
現在のインデックスをひとつ後に移動する。 | |
void resetIndex() |
現在のインデックスを末尾に戻す。 |
History history = lineReader.getHistory(); System.out.println(history.get(history.last()));
HistoryはIterableを実装しているので、iteratorメソッドやfor文が使用できる。
for (History.Entry entry : history) { System.out.println(entry.index()); System.out.println(ZonedDateTime.ofInstant(entry.time(), ZoneId.of("Asia/Tokyo"))); System.out.println(entry.line()); // 履歴の文字列 }
履歴はHistoryのaddメソッドで追加する。
History history = lineReader.getHistory(); history.add("abc");
内部的には、現在時刻も一緒に登録するようになっている。
ユーザーが入力した文字列の履歴をファイルに読み書きするHistoryのメソッドは何種類かある。
メソッド | 説明 |
---|---|
void load() |
履歴をファイルから読み込む。read(null, false) 相当。 |
void save() |
履歴をファイルに保存する。append(null, true) 相当。 |
void write(Path file, boolean
incremental) |
履歴をファイルに保存する。 既存の履歴ファイルを削除してから append を呼び出すのと同等。 |
void append(Path file,
boolean incremental) |
履歴をファイルに追加する。 fileがnullの場合、LineReader.HISTORY_FILEにセットされているパスを使用する。それもnullの場合は何もしない。 fileの親ディレクトリーが存在しない場合は作成される。 incrementalをtrueにすると、前回保存時より後に登録された履歴だけをファイルに保存する。 |
void read(Path file, boolean
checkDuplicates) |
履歴をファイルから読み込む。 ファイルのパスに関しては append と同様。checkDuplicatesをtrueにすると、同じ履歴は最初の1回しか登録しない。 (ファイルから読むときに重複を排除するだけで、ファイル内の重複を削除するわけではない) |
void purge() |
履歴ファイルを削除する。 ファイルのパスに関しては(fileがnullの場合の) append と同様。 |
import java.nio.file.Path;
var path = Path.of(System.getProperty("java.io.tmpdir"), "jline-example-history.txt"); lineReader.setVariable(LineReader.HISTORY_FILE, path); lineReader.setOpt(Option.HISTORY_TIMESTAMPED); // History history = lineReader.getHistory(); // history.attach(lineReader); // attachの中でloadされる(LineReaderImpl.readLine()内でattachされるので、明示的にattachする必要は無い) // history.load(); for (;;) { String line = lineReader.readLine("jline> "); if (line.equals("exit")) { break; } if (line.equals("history")) { System.out.println("history=" + history); } terminal.writer().println(line); } // history.save();
LineReader.HISTORY_FILEで履歴ファイルのパスを設定しておけば、履歴は(LineReaderImpl.readLine()内で)自動的に読み込まれる
(loadされる)。
また、デフォルトでは(Option.HISTORY_INCREMENTALがtrueなので)readLine()で1行入力する毎に履歴ファイルに保存される(saveされる)。
ちなみに、履歴ファイルは1行にひとつの履歴が入ったテキストファイル(UTF-8)。
(複数行の文字列でも、改行コードが「\r
」や「\n
」にエスケープされて必ず1行になる)
1663663742270:aaa 1663663744867:bbb 1663663746476:abc
各行の形式は2通りある。デフォルトは「タイムスタンプ:文字列
」という形式。
→Option.HISTORY_TIMESTAMPED
LineReaderのオプションで履歴関連の設定が出来る。
History関連のオプションをLineReaderでセットする仕様はいただけないと思うが^^;
オプションをセットする機構がLineReaderにあるので、それを利用している形かなぁ…。
import org.jline.reader.LineReader; import org.jline.reader.LineReader.Option;
分類 | オプション | 説明 |
---|---|---|
履歴登録 | LineReader.DISABLE_HISTORY |
trueにすると、入力された文字列を履歴に登録しない。 |
Option.HISTORY_IGNORE_SPACE |
trueの場合、入力された文字列の先頭が空白文字の場合は履歴に登録しない。 デフォルトはtrue。 |
|
Option.HISTORY_REDUCE_BLANKS |
trueの場合、入力された文字列をtrimしてから履歴に登録する。 デフォルトはtrue。 |
|
Option.HISTORY_IGNORE_DUPS |
trueの場合、入力された文字列が直前の履歴と同一なら登録しない。 デフォルトはtrue。 |
|
LineReader.HISTORY_IGNORE |
HISTORY_IGNOREで指定されたパターンに合致したら登録しない。 パターンは文字列を「 : 」で区切って指定する。ワイルドカード「* 」も使用可能。デフォルトはnull。 |
|
Option.HISTORY_INCREMENTAL |
trueの場合、メモリー上の履歴に登録する度にファイルに保存(save)する。 デフォルトはtrue。 |
|
LineReader.HISTORY_SIZE |
保持する履歴の最大件数。デフォルトは500件。 | |
履歴ファイル | LineReader.HISTORY_FILE |
履歴ファイルのパス。デフォルトはnull。 nullの場合、(loadやsaveメソッドで)履歴ファイルの 処理は行われない。 |
Option.HISTORY_TIMESTAMPED |
trueの場合、履歴ファイルの中は「タイムスタンプ:文字列 」という形式になる。falseだと「文字列 」という形式。(複数行の文字列の場合は改行コードがエスケープされて1行になる) デフォルトはtrue。 ファイルの中が「文字列」という形式(「 : 」が無い)で当オプションがtrueだと、履歴ファイル読み込み時にIllegalArgumentExceptionが発生する。逆だと、エラーにはならないが、タイムスタンプまで含めた文字列になってしまう。 |
|
LineReader.HISTORY_FILE_SIZE |
ファイルに保持する履歴の最大件数。デフォルトは10000件。 |
import java.nio.file.Path;
Path path = Path.of(System.getProperty("java.io.tmpdir"), "jline-example-history.txt"); lineReader.setVariable(LineReader.HISTORY_FILE, path); lineReader.setOpt(Option.HISTORY_IGNORE_DUPS); // lineReader.getHistory().attach(lineReader);
LineReaderで定義されている定数を使って設定する場合はLineReader#setVariable()(あるいはvariable())を使う。
LineReader.Optionで定義されている定数を使って設定する場合はLineReader#setOpt()やunsetOpt()(あるいはoption())を使う。
それから、History(DefaultHistory)の中でLineReaderの設定を参照する為に、HistoryにLineReaderをアタッチする必要がある。
ただし、LineReaderImpl.readLine()の中でアタッチされるので、自分で明示的にsave/loadを実行したい場合以外はattachメソッドを呼ぶ必要は無い。
(attachメソッドを呼ぶと、内部でload(履歴ファイルの読み込み)が実行される)
HistoryのisPersistableメソッドをオーバーライドすると、特定の履歴(文字列)だけファイルに保存しないということが出来る。
import org.jline.reader.impl.history.DefaultHistory;
public class MyHistory extends DefaultHistory { @Override public boolean isPersistable(Entry entry) { String line = entry.line(); if (line.startsWith(":") || line.startsWith("\\")) { // :や\で始まる文字列は履歴ファイルに保存しない return false; } return true; } }
LineReader lineReader = LineReaderBuilder.builder() .history(new MyHistory()) .build();
ちなみに、LineReader.HISTORY_IGNOREという設定値もあるが、これは履歴ファイルへ保存しないものではなく、履歴自体へ登録しないパターンを設定する。
(ちなみにそれもmatchPatternsメソッドをオーバーライドすれば、自由に変更できる)