はじめに
最近よくログ確認をします。
300万行(1.5GB)くらいのファイルを開いて、文字列を検索しつつエラー箇所を見つけたり、意図通りの動作をしているか確認したりします。
ripgrepであたりをつけつつ、lessコマンドでファイルを開いて該当箇所に飛んだりします。
lessコマンドはファイルを開く動作は軽いのですが、検索が重く、検索してもなかなか結果が返ってきません。
かといって、vim(view)コマンドを使うと検索はlessより速いですが、それでも待たされるのと、ファイルを開いた直後から内容を見れるまでがかなり重いです。(Ctrl+Cとかで途中で読み込みを止めたりします。)
なんかless並みに軽くファイルを開くことができて、viewまたはそれ以上に速く検索できるようなコマンドが欲しいなぁと思い立ちました。
というわけで、完成しないかもしれませんが、検索が速いlessクローンを実装してみたいと思います。
今考えている戦略
- Rustを使う
- 検索にripgrepを使う
以上。今考えてるのはこれくらい。
あとはターミナルアプリケーションということで、以下の記事を参考にRustでターミナル扱うcrateを選定しました。
速度と程よい抽象化がされているということで、crosstermを使うことにしました。
2019年の記事なので少し事情は変わってるかもですが、大枠変わらんだろうという感じで記事は読みました。
最低限必要な機能
- コマンドラインからファイルを指定してファイルを閲覧できる
- hjklキーでカーソル移動
-
/
で検索モードに入って/{WORD}↩︎
で検索できる- できれば検索ワードをハイライトしたい
- できればインクリメンタル検索したい
- 検索ワードが存在する状態でnキーで次の検索候補にジャンプ、n+shiftキーで前の検索候補にジャンプ
- ESCキーで終了
less
ちなみにlessコマンドはどんな実装か見てみたいなーと思って調べてみると、GitHub上にソースはあるようでした。
まだソースコードは読んでいませんが、実装にどん詰まったら気分転換に読んでみようかと思います。
古くからあると思うのですが、今なおアクティブにメンテナンスされてるのがすごいです。
見習わねば。
実装
コマンドラインオプション
定番のclapを使います。
#[derive(Parser)]
#[clap(version = "0.0.1", author = "hhatto")]
struct Opts {
input: String,
}
fn main() -> Result<()> {
let opts: Opts = Opts::parse();
opts.input // これで入力ファイル名取れます
}
ファイル読み込み
ナイーブな実装でBufReaderとか使うと大変そうということがわかってしまい、いろいろ調べているうちに以下の記事でropeyというライブラリがあることがわかりました。
ファイル読み込ませてみると、体感ですがlessよりは遅いがviewよりは速い感じだったので、一旦これで実装を進めていきます。
以下のような感じで使えます。
let f = File::open(filename)?;
let lines = ropey::Rope::from_reader(f)?;
// ターミナルの高さ(行数)取得して、最初の行数分だけ出力
let (_, rows) = terminal::size()?;
for idx in 0..rows {
println!("{}", lines.line(idx as usize));
execute!(stdout(), MoveTo(0, idx as u16))?;
if idx as usize >= line_count - 1 {
break
}
}
main
fn main() -> Result<()> {
let opts: Opts = Opts::parse();
let mut stdout = stdout();
// rawモード使う
enable_raw_mode()?;
// ターミナルを一旦クリア
execute!(stdout, Clear(ClearType::All))?;
// カーソル位置を左上に持っていく
execute!(
stdout,
SavePosition,
MoveTo(0, 0),
DisableBlinking,
)?;
// コア部分 イベントループ
if let Err(e) = less_loop(opts.input.as_str()) {
println!("error={:?}\r", e);
}
disable_raw_mode()
}
less_loop
入力イベントループ。
検索機能はまだないけどざっくり入れた。
fn less_loop(filename: &str) -> Result<()> {
let mut is_search_mode = false;
:
:
execute!(stdout(), MoveTo(0, 0))?;
loop {
let event = read()?;
if is_search_mode {
match event {
Event::Key(KeyEvent {
code: KeyCode::Esc,
modifiers: _,
}) => {
is_search_mode = false;
execute!(stdout(), RestorePosition)?;
},
_ => (),
};
} else {
execute!(stdout(), SavePosition)?;
match event {
Event::Key(KeyEvent {
code: KeyCode::Char('h'),
modifiers: _,
}) => execute!(stdout(), MoveLeft(1))?,
Event::Key(KeyEvent {
code: KeyCode::Char('j'),
modifiers: _,
}) => execute!(stdout(), MoveDown(1))?,
Event::Key(KeyEvent {
code: KeyCode::Char('k'),
modifiers: _,
}) => execute!(stdout(), MoveUp(1))?,
Event::Key(KeyEvent {
code: KeyCode::Char('l'),
modifiers: _,
}) => execute!(stdout(), MoveRight(1))?,
Event::Key(KeyEvent {
code: KeyCode::Char('/'),
modifiers: _,
}) => {
is_search_mode = true;
execute!(
stdout(),
MoveTo(0, rows+1),
Print("/"),
)?;
},
Event::Key(KeyEvent {
code: KeyCode::Esc,
modifiers: _,
}) => break,
_ => (),
};
}
}
おわりに
今日はここまで。
今日できたこと
- 入力したファイルを開いて画面に表示
- hjklキー入力でカーソル移動(スクロールはできない)
-
/
入力でサーチモードに移行。サーチモード状態でESCキー入力で元のカーソル位置に戻る - ESCキーでコマンド終了
ここまでのソースは以下にあります。明日つづきやろう。