web小説礼賛記

さらなる俺 TUEE をあなたに!

シェル実用 #1: グレッグ様の数を数える (grep, less)

† Mission †

 作品:嘆きの亡霊は引退したい 〜最弱ハンターは英雄の夢を見る〜
 作者:槻影 [Amazon]

 に現れる、グレッグ様の数を数えよ!

> What's this?

 グレッグ様を礼賛するネタ記事です。ネタ活動にもターミナルは役に立つ、という具体例としての意義はあります。

 この程度なら、シェルが初見でも数時間で使えるようになる(なった)ので、やってみてください。こんなんできるし! という方はスルーしてください。

グレッグ様とは?

 ベテラン冒険者です。調子に乗って『グレッグ様』を自称したため、今日の最新話でもネタにされています。一定の域にいる人物のため、未熟な冒険者グレッグ様と呼ぶなら、あながち間違っていません。

† Solution †

1. 本作をダウンロードする

 narou.rbを使いました。事前のインストールが必要です。

$ cd path/to/your/narou/directory
$ narou d https://ncode.syosetu.com/n6093en/

 自力でhtmlをダウンロードしてパースするのもアリです。

2. テキストファイルを連結する (必要なら)

 grep再帰的な検索もできますが、lessで読むときに楽なため、連結しておきます。

 ripgrepを使うと、より楽に再起検索ができます。

cd "$(grep)"

 生データ(.txt)の入ったrawディレクトリを見つけ、移動します。GUIから移動しても構いません。CLIが初見なら、飛ばしてください。

 最新のnarou.rbを使う場合は、rawディレクトリにはhtml、本文ディレクトリの方にtxtファイルが入っています。

実践

 findにより再帰的にディレクトリを検索し、grep -vでフィルタリングします。

$ pwd # print working directory
/Users/moba/Desktop/narou
$ # まずrawディレクトリを探す
$ find . -type d | grep 嘆きの亡霊 # .は*でもいい
./小説データ/小説家になろう/n6093en 嘆きの亡霊は引退したい _最弱ハンターは英雄の夢を見る_
./小説データ/小説家になろう/n6093en 嘆きの亡霊は引退したい _最弱ハンターは英雄の夢を見る_/本文
./小説データ/小説家になろう/n6093en 嘆きの亡霊は引退したい _最弱ハンターは英雄の夢を見る_/raw
$ # 3行目を抜き出す
$ find . -type d | grep 嘆きの亡霊 | tail -1 # sed -n 3p や grep raw でも良い
./小説データ/小説家になろう/n6093en 嘆きの亡霊は引退したい _最弱ハンターは英雄の夢を見る_/raw
$ # rawディレクトリに移動する
$ cd "$(find . -type d | grep 嘆きの亡霊 | tail -1)"
備考
  • grep | cd "$(cat)"とパイプを通すと、cdがサブシェルで実行されるために無効です。
  • grep --color=alwaysだと、cd "$(grep)"に失敗します。着色文字(ANSIエスケープコード)のためです。ただし、ターミナルの出力からは、エラーの原因が分かりにくいかもしれません。

プロンプトを短くする(必要なら)

 パスをプロンプトに表示している場合は、短くした方が見やすいです。

$ pwd
/Users/moba/Desktop/narou/小説データ/小説家になろう/n6093en 嘆きの亡霊は引退したい _最弱ハンターは英雄の夢を見る_/raw
$ # パスがあまりに長かった
$ PS1='$ ' # プロンプトを短くする

 mobaの環境では、ターミナルの横幅に対して、プロンプトが長過ぎるとバグが出るため、短くしました。基礎編#3で作ったps-sを使ってもいいです。

 fishシェルの場合は、省略されたパス表示がデフォルトなので、プロンプト文字の変更は必要無いでしょう。

連結する

 3つの改行で区切ってファイルの内容を連結します。

whileで連結する

 標準出力は、消費しない限り書き足されていきます。そのため、ファイル毎にprintfすれば、printfした内容が連結された出力になります。

$ find * | sort -n # -n: naturally / numerically
1 1 メンバー募集.txt
2 2 メンバー募集②.txt
<省略>
$ find * | sort -n | while read f; do
> printf '%s\n\n\n' "$(cat "$f")"; done > nageki.txt
$ mv nageki.txt where/you/like/
別解: xargsで連結する

 xargs -Iの実行に伴う文字{}の置換は、コマンド置換$()よりも後に行われます。$()が処理順の優先順位を変えるものだと考えると、当然ですね。

 代わりにbash -cを使ってみました。

$ # GNU xargsで改行のみを区切り文字にする (-d: delimiter)
$ find * | sort -n | gxargs -d '\n' -I{} bash -c 'printf "%s\n\n\n" "$(cat '{}')"'
$ # もしくは、ヌル文字区切りで処理する (移植性と確実性から最善)
$ find * -print0 | sort -zn | xargs -0 -I{} bash -c 'printf "%s\n\n\n" "$(cat '{}')"'
$ # ファイル名が改行文字を含まないなら、trで\nを\0に変換してもいい (tr: translate)
$ find * | sort -n | tr '\n' '\0' | xargs -0 -I{} bash -c 'printf "%s\n\n\n" "$(cat '{}')"'

 シェルには基本的に文字列しか無いため、関数的にfind |> sort |> map cat |> joinと書けないのが残念です。PowerShellなら楽々書けるのでしょうか。

3. グレッグ様を数える

 検索結果を数えます。ここで正規表現を使えると、会話文や地の文中のグレッグ様に限定できます。

grep: Global Regular Expression Print で検索する

 grepは、パタンにマッチするすべての行(段落のこと)を抜き出してくれるコマンドです。--color=alwaysで、マッチした部分を着色してくれます。

 出力が過ぎる場合、| less -Rで閲覧するのもアリです。(-Rで着色を許す)

$ # 「グレッグ」を含む行を抜き出す(グレッグを着色して表示)
$ grep --color=always グレッグ nageki.txt
..
$ # -o --onll-matching; wc word count; -l: line
$ # -o無しだと「グレッグ」を含む行の数を数えることになるため注意
$ grep --color=always -o グレッグ nageki.txt | wc -l
     148

$ # 実はマッチ数を数えるオプションもある(grepの実装による?)
$ grep -c グレッグ nageki.txt # -c: --count
148

f:id:moba_lake:20181011043124p:plain
グレッグ様で溢れるターミナル

4. グレッグ様の活躍を読む

 マッチしたグレッグ様の前後の文章を読みたいとします。grep -<数値>で、マッチ部分の前後段落も抜き出してくれますが、そもそもファイルは、lessで読むことができます。

less the pager

 less <file>でファイルを読めます。&キーでgrepに似た検索ができますが、件数は(たぶん)表示できません。

options

 less -iNMRエイリアスを設定してもいいかもしれません。

オプション 意味 効果
i ignore case 検索時、小文字を大小文字を区別しない意味として扱う
N number 行番号を表示
M more verbose prompt 左下のプロンプトを詳しくする
R raw grepの出力を読むとき、着色を許す(結果的に)

 -Mでは、ファイルの行数、表示されている行の範囲、スクロール位置(%)などが表示されます。

less内の操作

 スクロール以外のショートカットをまず覚えます。

入力 意味 解説
q quit 閉じる
h help ヘルプ
/ search 検索
n next 次の検索結果へ
N previous 前の検索結果へ
<数値>G Go 指定行へ飛ぶ
& grep風検索 パタンを含む全行を表示

 grep風検索で興味深いグレッグ様を見つけたら、& Enterで検索を止めて、行番号Gで指定行へジャンプ! なお、検索/は、ハイライトの代わりにも使えます。

 lessには、GUIであるようなディレイが一切ありませんCLIのポテンシャルを感じます。

5. 余談: Vimでカウント

 Vimでもlessと同様に/<pattern>で検索できますが、やはりヒット数は表示されまん。

単語をカウントする

 方法は色々ヒットしますが、『置換しない置換』gnを使います。(n: do not substitute)

:%s/グレッグ//gn
148 matches on 147 lines
:%s/グレッグ様//gn
67 matches on 67 lines
:%s/「[^」]*グレッグ様[^」]*」//gn
6 matches on 6 lines

 実質的に、単なるカウントですね。

note: 正規表現 (regex; regular expressions)

 会話文のグレッグ様は、正規表現を使って検索・置換しました。今回使用した正規表現は:

文字 効果
s/old/new/g substitute old with new globally
. 改行以外の1文字
* 0回以上のリピート
[] []内で指定された一文字
^ 否定

 [^」]で、以外の任意の一文字となります。最短マッチのために使いました。正規表現では、最長マッチがデフォルトです。

note: グレッグ様

 全6件の「グレッグ様」発言がありました。4件がクライ、1件がティノによるもので、最初の1件のみが自称のようです。強く生きて……いや、とにかく生きて欲しい。

マッチさせられなかった「グレッグ様」

 「グレッグ様」では検出できないグレッグ様がいました。

グレッグさん

 ルーダの発言です。これは想定外でした。結局、自分の目で確認するのが安全かもしれません。そんな時も、grepやless内の&サーチが有効です。

グレッグ・ザンギフ様

 こちらも漏らしていました。表記がブレるキャラの場合は、検索に工夫が要りそうです。

余談

grep マスターは神 nageki.txt

 検出数は6、すべて同じ章の中です。同じネタを二度使わないのは流石ですね。

 今後も心置きなく礼賛できるというものです。