ripgrep の -g にある除外指定が、まだありません — Wide Field⑧:ファイル指定を引用符ひとつに決め、直した5か所で36項目が一致

技術解説

引用符を忘れると、怒られます。

uvf *.log ERROR と打つと、エラーになって「引用符で囲んでください」と言われる。ripgrep なら、そのまま探してくれるところです。わざわざ使いにくくしているわけです。

なぜ、こちらだけが引用符を要求するのでしょうか。

先に結論: ファイル指定は引用符でくるんだ「1つの引数」に決めた。その文字列そのものが束ねた索引の名前になるから。ワイルドカードの解釈は5か所を直し、複数ファイルの突き合わせは Mac・Linux とも36項目が一致(不一致 0 件)。ただし ripgrep の -g にある除外指定(!)は、まだありません

uvf 'logs/*.log,err/*.log' ERROR のように、複数ファイルの指定はすべてまとめて引用符で囲みます。シェルに展開させず、uvf・uvp が自分で広げる。* がフォルダーの区切りをまたがないようにし、**/sub/*.log と dir/** が当たるようにし、[ ] を文字どおりに扱い、シンボリックリンクのフォルダーをたどらないようにしました。Mac(Apple M4・10コア)・メモリ32GB・外付け USB SSD。

uvf・uvp は私が作っている UwView(v1.7.3.6 “Wide Field”・2026年9月28日公開)の検索コマンドです。uvf は無料版、uvp は有料版 UwView Pro のものです。ファイル指定の直しは v1.7.2.2、索引の名前の決め方は v1.7.2.4 で入れ、v1.7.3.6 の統合テスト(2026年9月29日)で3つの OS の結果を確認しています。


先に、まだできないことを書きます

  • 除外の指定がありません。 ripgrep には -g/--glob があり、gitignore と同じ書式で何回でも足せて、先頭に ! を付けると除外になります(同じファイルに複数当たったら、後に書いたほうが勝つ)。こちらに '*.log,!debug*.log' のような書き方はまだ無い。次の版の候補として置いてあります。いまできる除外は、.ignore・.gitignore に書く形だけです。
  • {a,b} は使えません。 カンマを区切り文字に使うと決めたので、波かっこの展開と両立しません。'a.log,b.log' と書いてください。
  • [a-z] のような文字クラスも使えません。 文字どおりの名前として照合します。
  • フォルダー名だけ('logs')は指定できません。 'logs/**' と書く必要があります。次の版の候補です。
  • 大文字と小文字の扱いを指定できません。 ripgrep の --iglob に当たるものが無く、ファイルシステム任せです(Linux は区別し、Mac・Windows は区別しない)。
  • この直しの速さを、切り出して測っていません。「木全体を読んでから絞る」形をやめたので大きなフォルダーでは読む量が減っているはずですが、前後を比べた計測値は持っていません。この記事に秒数と倍率はありません。

ripgrep と、引数の形がそもそも違う

ripgrep のコマンドは「パターン」と「探す場所」です。場所はシェルが展開して渡す前提で、絞り込みは -g で足します。

rg ERROR logs/ -g '*.log' -g '!debug*.log'

こちらは違います。引数は「1つの指定文字列」と「検索語」の2つだけです。

uvf 'logs/*.log,err/*.log' ERROR

指定の中をどう区切るかは uvf・uvp の仕事です。空白とカンマの両方を区切りとして受けますが、Program Files のように名前に空白を含むパスは空白区切りでは表せないので、空白を含むときはカンマで区切ると決めました。空白区切りでどの断片も当たらなかったときは、黙って0件を返さずにその旨を案内します。

そして展開された形で届いたとき——引数の末尾以外がすべて実在ファイルだったとき——は、受け付けずにエラーにします。

エラー: 複数のファイルが直接渡されました(シェルが展開した可能性があります)。
       引用符で囲んでください:  uvf '*.log' ERROR

黙って受け付けたほうが親切に見えます。そうしませんでした。受け付けると構文が2つになり、次の節の理由で「索引を作れる場合と作れない場合」が混在してしまうからです。

なぜ引用符が必須なのか——その文字列が、索引の名前になる

ここが設計の核です。

uvp は複数のファイルを1本の .uwvz(uvp 用の圧縮形式+索引)に束ねます。2回目からは、その索引を読んで探す。ということは、「同じ指定で呼ばれたら、同じ索引に当てる」仕組みが要るわけです。

その鍵を、指定文字列そのものから作りました。指定 (A) を、ファイル名に使える文字列 (B) に可逆で変換します。

  • % → %%(エスケープ文字自身。これが無いと可逆になりません)
  • * → %a / ? → %q / , → %c / / → %s / \ → %b
  • : → %o / [ ] → %l %r / " < > | → %d %t %g %p

'*.log' なら uw-%a.log.uwvz、'logs/*.log,err/*.log' なら uw-logs%s%a.log%cerr%s%a.log。頭の uw- は「束ねた索引」の印で、1本のファイルの索引(app.log.uwvz)と見分けるためのものです。

可逆にした理由は、転がっている .uwvz を見たときに名前だけで中身が分かるようにするためです。

そして、シェルが展開してしまうと (A) がプログラムに届きません。届かなければ (B) が作れず、索引の名前が決まらない。引用符の必須化は作法の話ではなく、ここから来ている構造上の要請です。

なお、名前にする前に絶対パスを起動フォルダーからの相対パスに直します。相対で書いても絶対で書いても同じ索引に当たり、a.log b.log と a.log,b.log も同じ索引になります(区切りをカンマ1つにそろえてから名前とハッシュを作る)。

24文字を境に、名前の作り方を切り替えた

可逆変換には長さの上限がありません。指定が長いと、名前がファイル名の上限を超えます。

きっかけは Windows でした。混在ファイルのテストで、uvp の2回目が1回目と同じだけ時間がかかっていた。つまり束ねた索引が使い回されていない。名前が長すぎて保存に失敗し、しかもエラーが出ていないのではないか、と疑いました。

そこで決め方を変えました。

  • 変換表の名前(.uwvz を除いた部分)が 24文字以下ならそのまま使う
  • 超えたら uw-<見出し8文字>-<ハッシュ12文字>.uwvz に切り替える(.uwvz を除いて最大24文字・全体で29文字)

24 という数字は uw-(3)+見出し(8)+-(1)+ハッシュ(12)の合計で、「変換表の名前がこれより長くなったら、ハッシュの名前のほうが短い」という境目です。文字数は文字で数えます(日本語も1文字)。24文字なら UTF-8 でも最大72バイトなので、どの OS の上限にも余裕で収まります。

見出しは、指定の最初の断片から作ります。ワイルドカードを含まない最後のフォルダ名を採り、無ければファイル名から作って8文字で切る。'logs/*.log,err/*.log' なら logs、'/var/log/app/*.log,/var/log/db/*.log' なら app です。ハッシュは、正規化した指定の SHA-256 を base32 小文字(a〜z・2〜7)で12文字——大文字と小文字を区別しないファイルシステムでも衝突しない文字だけを使っています。

どちらの名前でも、指定の原文は索引のメタデータの pattern に残します。だから 短い指定は名前で分かり、長い指定は uvp <索引> --info で分かる。指定が失われる場合はありません。

疑ったところが、原因ではなかった

正直に書いておきます。長さが原因ではなかった可能性が高いと、あとで分かりました。問題の名前は約65文字、フォルダーを足しても約85文字で、Windows の上限260には届いていません。しかも2回目も1回目と同じだけ時間がかかっていた——「作り切ってから最後に失敗している」形に見えます。理由が見えなかったのは、計測スクリプトが標準エラー出力の1行目(作成中の表示)しか拾っていなかったからでした。エラーは出ていたのに、見ていなかった可能性があるわけです。

なので名前を短くしただけで終わらせず、次の3つも一緒に入れました。

  • 保存に失敗したら必ず知らせる。 置けなかったら一時フォルダーに作り直して検索結果は出し、「索引を保存できませんでした: パス(理由)」を出して exit 2 で終わる。一時フォルダーの索引は終わりに消します。以前は黙って終わっていた疑いがありました
  • Windows の本体に longPathAware を入れる。 OS 側で長いパスが有効なら260を超えても動きます
  • 一時ファイルの名前を <名前>.tmp-<8桁> にする。 固定名にすると、同じ索引を2つの uvp が同時に作ったときに片方が失敗します

結果として、Windows で束ねた索引が2回目に使われない件は v1.7.3.6 で解消しています。どの直しが効いたのかは、切り分けていません。

直した5か所

ワイルドカードの解釈そのものにも、シェルと違う振る舞いがありました。実装が .NET の簡易照合(* と ? だけ)で組まれていたためです。

書き方 直す前 直したあと(bash と同じ)
'*/*.log' 2段・3段下まで全部当たる。しかも木全体を下まで読んでから絞る ちょうど1段下だけ。段ごとに照合して、必要な段だけ読む
'**/sub/*.log' 1件も当たらない(** の後ろをファイル名だけと比べていた) 相対パスの末尾と照合して当たる
'logs/**' 1件も当たらない logs 以下の全ファイル
'app[12].log' [ をワイルドカードとして数えるが、[12] は解釈せず文字どおりに照合 [ ] はワイルドカードに数えず、文字どおりの名前
シンボリックリンクのフォルダー 未確認(.NET の再帰がたどるかどうか) たどらない(ループを避ける)

いちばん実害があったのは1行目です。'*/*.log' が2段下・3段下まで拾っていただけでなく、木全体を読み切ってから絞っていた。段ごとに照合する形に書き直したので、必要な段しか読まなくなりました。ただしこの前後を測った数字は持っていません。「読む量が減った」は設計の話です。

決まりごとも文書にしました。並びは書いた順で、1つの断片の中は名前順。同じファイルは1回だけ探す。隠しファイルと隠しフォルダーは対象外。.uwvz はワイルドカードでは拾わない(名前を書けば使えます)。1件も当たらない指定は名前で知らせ、1,000本を超えたら知らせる(止めません)。詳しい説明は --help 本体から uvf --help files に分けました。

数字——36項目、3つの OS

正しさの数字です。速さではありません。

段 内容 Mac Linux Windows
mtest 複数ファイルの突き合わせ PASS 36/FAIL 0 PASS 36/FAIL 0 PASS 34/FAIL 0/省略 2

Windows の省略2件は、テスト機に zip を作るコマンドが無くて飛ばしたものです。飛ばしたテストを、合格と数えません。

ほかに揃っているのは、こうです。

  • 単体テストは uvf 438件・uvp 780件(ファイル指定を直した v1.7.2.2 の時点)がすべて成功。索引の名前を直した v1.7.2.4 では uvp が802件
  • 名前の付け方だけを見るテストが19項目(変換表の名前・24文字の境目・日本語の数え方・見出し・相対と絶対・順序・2回目の使い回し・逆変換・--info・一時ファイル)
  • 直した5か所ぶんの項目を、複数ファイルのテストの A 群に追加('*/*.log'・'**/sub/*.log'・'logs/**'・'**/*.log'・[ ]・--help files)

テストの FAIL 1 は、テストのほうが間違っていた

A 群に追加した直後の結果は PASS 34/FAIL 1 でした。

落ちた1件の原因は、テストスクリプトの数え方の誤りです。--help files の文面に 'logs/**' が2回出てくるのを、1回のつもりで数えていた。製品側は正しく、テスト側が間違っていました。スクリプトを直して通しています。

「テスト側の不具合でした」は言い訳に見えます。ただ書かないと合格数だけが残るので、書くことにしています。前回の記事では、Windows で15項目が落ちた原因もテストスクリプト側でした。2回続くなら偶然ではなく、テストハーネスのほうに弱点があるという話です。

もうひとつ、--info は v1.7.2.3 までそもそも存在していませんでした。仕様書に書いてあったのに実装されていなかった。これも名前のテストを書いて初めて分かったことです。仕様書に書いた機能は、テストを書くまで「ある」と思い込める。

確かめ方

  • 相手: ripgrep(-g/--glob の挙動は ripgrep 自身のヘルプの記述に沿って比べています)
  • 版: uvf・uvp とも v1.7.3.6 “Wide Field”。ファイル指定の直しは v1.7.2.2、索引の名前は v1.7.2.4
  • 突き合わせ: 2026年9月29日の3 OS 統合テストの mtest(複数ファイル)
  • 機械: Mac(Apple M4・10コア・メモリ32GB)/外付け USB SSD。Linux は aarch64 の VM、Windows は別の機械
  • この記事に秒数と倍率はありません。したがって OS をまたいだ比較もしていません

持ち帰れる一般論

(1)引数の形は、後工程が決めることがある。 「引用符で囲んでください」は利用者にとって面倒です。それでも譲れなかったのは、その文字列が索引の名前になるからでした。キャッシュの鍵を入力そのものから作るなら、入力を壊さずに受け取る必要がある。シェルに展開させた時点で鍵の材料が失われます。

(2)鍵は、可逆と短さのどちらかを選ばされる。 可逆な名前は読めますが長くなり、ハッシュは短いですが読めません。境目を計算して切り替え、原文はメタデータに必ず残す形にしました。どちらの名前でも原文が取り出せるなら、長さの問題は名前の問題に閉じ込められます。

(3)当たりを付けた原因が違っていても、直しは無駄にならないことがある。 長さを疑って名前を短くし、結果としては長さのせいではなかった可能性が高い。ただし同時に入れた「保存の失敗を必ず知らせる」は、次に何かが起きたときに理由が見える形で残りました。黙って失敗する経路を消すのは、原因が分からないうちにやる価値がある。

(4)「シェルと同じ」を名乗るなら、違う行を数え上げる。 * が区切りをまたぐかどうかは実装の都合で簡単にずれます。ずれたまま「ワイルドカードが使えます」と書くと、利用者は自分のパターンを疑い続けることになる。できないことを数えられる状態にしておくほうが、全部できると言うより速く使ってもらえます。

正直に書いておくこと

  • この記事に速さの数字はありません。ファイル指定は正しさと使い勝手の話で、PASS 36/FAIL 0 以外に主張できる数字を持っていません
  • 「必要な段だけ読む」に直した前後を測っていません。大きなフォルダーで読む量が減っているはずですが、切り出した計測はしていません
  • ripgrep の -g にある除外指定(!)が、こちらにはありません。{a,b}・[a-z]・フォルダー名だけの指定・大文字小文字の指定も未実装です
  • Windows で索引が使い回されなかった原因を、切り分けていません。名前を短くしたことと、ほかに入れた3つの直しのどれが効いたのかは分かっていません
  • テストの FAIL がテスト側の不具合だった件が、2回続いています。製品側が正しかったという報告を、当人が書いています
  • Windows と Linux の結果は、正しさの確認としてだけ持ち込んでいます。秒数は OS をまたいで比べません
  • 名前の衝突は、原文を照合して食い違えばエラーにする形で防いでいます。実際に衝突を起こしてみたわけではありません
  • 私は比較の一方の開発者です。その分は割り引いてお読みください

使っている道具

UwView は、巨大なテキストファイル・巨大ログを調べるための CLI/GUI ツールです。v1.7.3.6 “Wide Field” では、複数のファイルと7つの圧縮形式を、外部のコマンドなしで1回で探せます。

uvp ならこう書く

# 今のフォルダーの .log(必ず引用符で囲む)
uvf '*.log' ERROR

# 1段下のフォルダーだけ/以下すべて
uvf '*/*.log' ERROR
uvf '**/*.log' ERROR

# どこかにある 2026-09 フォルダーの直下
uvf '**/2026-09/*.log' ERROR

# フォルダー以下の全ファイル
uvf 'logs/**' ERROR

# 空白を含むパスはカンマで区切る
uvf 'C:/Program Files/app/*.log,D:/logs/*.log' ERROR

# 何が対象になるか、先に確かめる(無料)
uvf '**/*.log' --files

# ファイル指定の詳しい説明
uvf --help files

# 束ねて探す。2回目からは索引を読む(UwView Pro)
uvp 'logs/**' ERROR

# その索引が何を束ねたものか見る(無料)
uvp 'logs/**' --info

# 索引を作り直す(古いものは .bak-日時 に退避)
uvp 'logs/**' ERROR --rebuild

終了コードは grep と同じで、0=あり・1=なし・2=エラーまたは結果が不完全です。

無料版は GitHub Releases から。Mac は brew install --cask amru195704/uwview/uwview、Windows は Scoop(scoop bucket add uwview https://github.com/amru195704/scoop-uwview → scoop install uwview)でも入ります。

リンク

  • ripgrep と同じ除外規則にそろえた(.ignore・.gitignore): https://uvp.y42u.net/blog/uwview-wf07-ignore-rules/
  • 束ねた索引を使い回す(1本消えても作り直さない): https://uvp.y42u.net/blog/uwview-wf04-bundle-keep/
  • 外部コマンドを呼ばずに7形式を探す: https://uvp.y42u.net/blog/uwview-wf02-seven-formats-no-external/
  • .uwvz を作って、中を探して、取り出す: https://uvp.y42u.net/blog/uwvz-compressed-archive-search-extract/
  • 圧縮ログの検索を ugrep・ripgrep・zgrep と比べた: https://uvp.y42u.net/blog/uwview-ugrep-ripgrep-zgrep-compressed-logs/
  • 読む回数だけを減らした13回のまとめ: https://uvp.y42u.net/blog/uwview-fl13-first-light-recap/
  • 実測の全データ(ベンチマーク一覧): https://uvp.y42u.net/benchmarks/
  • ソースコード(GitHub): https://github.com/amru195704/UwView

そして、ストレージを専有している巨大ログを圧縮して保管し、さらに高速に検索したいなら UwView Pro をどうぞ。永続索引・圧縮キャッシュ検索・約 1/9 保管で、開き直しも検索も一段速くなります(全 OS 対応・買い切り/月額プランあり)。


開発者より: アプリ・Kindle 本・公開プロジェクトの一覧は GitHub: amru195704 にまとめています。


タイトルとURLをコピーしました