1 概要

RubyGems.org に公開してあります。

テキストフィルターコマンドのフレームワークと、 それを使った幾つかのフィルターコマンドを提供します。

2 コマンド

2.1 calc

bc コマンドは小数点の前のゼロが省略されたりして不便なので作りました。

% echo '1+2' | calc
3

% echo '1.0+2.0' | calc
3.0

% echo 'sqrt(2)' | calc
1.41421356237309504880

% echo 'l(2)' | calc
0.69314718055994530941

% echo 'e(1)' | calc
2.71828182845904523536

-r, --ruby オプションを付けると Ruby 式の表現での演算ができます。

% echo 'Math::PI' | calc -r
3.141592653589793

-p, --preserve オプションを付けると、元の式を出力に含めて「=」で結んだものを出力します。

% echo 'e(1)' | calc -p
e(1) = 2.71828182845904523536

2.2 columnanalyze

ps や qstat の出力は数千行以上にもなり得ます。 これらの表形式のストリームを簡単に取り纏めて把握するためのコマンドです。

example ディレクトリに実行例が同梱されています。 qstat.out は gridengine の qstat コマンドの出力で、以下のような長いファイルです。

job-ID  prior   name       user         state submit/start at     queue                          slots ja-task-ID 
-----------------------------------------------------------------------------------------------------------------
 205712 0.75000 vasp-all.q alice        r     05/11/2017 15:37:12 Tc.q@Tc18.calc.atom                4        
 205714 0.75000 vasp-all.q alice        r     05/11/2017 15:37:12 Tc.q@Tc19.calc.atom                4        
 269156 0.40572 vasp-Tc.qs bob          r     05/30/2017 02:43:57 Tc.q@Tc05.calc.atom                4        
 269159 0.40572 vasp-Tc.qs bob          r     05/30/2017 03:47:12 Tc.q@Tc02.calc.atom   
 (snip)

2.2.1 引数なしで実行

example/columnanalyze/ ディレクトリで以下のように実行すると、 以下のような出力が得られます。

% cat qstat.out | columnanalyze
1      2       3          4       5     6                   7                   8     9
job-ID prior   name       user    state submit/start at     queue               slots ja-task-ID
205712 0.75000 vasp-all.q alice   r     05/11/2017 15:37:12 Tc.q@Tc18.calc.atom 4
205714 0.75000 vasp-all.q alice   r     05/11/2017 15:37:12 Tc.q@Tc19.calc.atom 4
269156 0.40572 vasp-Tc.qs bob     r     05/30/2017 02:43:57 Tc.q@Tc05.calc.atom 4
269159 0.40572 vasp-Tc.qs bob     r     05/30/2017 03:47:12 Tc.q@Tc02.calc.atom 4
(snip)
293952 0.25011 vaspgeomop dave    qw    05/30/2017 11:51:05                     4

All:       78
Extracted: 78 ()

key head            types
1   job-ID          78
2   prior           29
3   name            7
4   user            5
5   state           4
6   submit/start at 49
7   queue           25
8   slots           2
9   ja-task-ID      2

「All: 78」というのはストリームで流れてきた行数です。 「Extracted: 78 ()」というのはそのうち抽出した行数です。 ここでは抽出条件を指定していないので All と同じ数になっています。

key はコマンドで扱う列番号です。 sort コマンドと同様に 1始まりにしてあります。

head はストリームで流れてきた1行目における要素の内容です。 多くのコマンドでタイトルとなっている事でしょう。 types はそれぞれの列におけるユニークな要素の数です。 たとえば job-ID は全てのジョブで一意であるため、 All と同じ数になっています。 user は 5種類の名前がこの位置に現れたことを示しています。

なお、qstat.out で現れた以下の横棒は無視されています。 プログラムとしては、単一文字種のみで構成された行を無視するようにしています。

-----------------------------------------------------------------------------------------------------------------

2.2.2 要素解析

各ユーザの出現数を調べてみましょう。 以下のように、解析したい対象のキー番号をコマンドライン引数に含めます。

% cat qstat.out| columnanalyze 4
(snip)

key analysis
(key=4)
user    1
dave    8
charlie 10
bob     22
alice   37

ストリームのうちで出現する回数の昇順で表示されます。 user は1行目のタイトルですね。

複数のキーを一度に指定できます。

% cat qstat.out| columnanalyze 4 5
(snip)

key analysis
(key=4)
user    1
dave    8
charlie 10
bob     22
alice   37

(key=5)
state 1
Eqw   2
r     23
qw    52

2.2.3 絞り込み

bob の出現する情報だけを取得し、他を捨てたいとしましょう。 以下のように、対象のキー番号と文字列をイコールで結んでコマンドライン引数に含めます。

% cat qstat.out| columnanalyze 4=bob
1      2       3          4    5     6                   7                   8     9
job-ID prior   name       user state submit/start at     queue               slots ja-task-ID
269156 0.40572 vasp-Tc.qs bob  r     05/30/2017 02:43:57 Tc.q@Tc05.calc.atom 4
269159 0.40572 vasp-Tc.qs bob  r     05/30/2017 03:47:12 Tc.q@Tc02.calc.atom 4
269161 0.40572 vasp-Tc.qs bob  r     05/30/2017 10:01:27 Tc.q@Tc06.calc.atom 4
269162 0.40572 vasp-Tc.qs bob  r     05/30/2017 10:52:12 Tc.q@Tc07.calc.atom 4
293923 0.25243 vasp-Pd.qs bob  r     05/30/2017 09:45:27 Pd.q@Pd12.calc.atom 4
293935 0.25081 vasp-Pd.qs bob  r     05/30/2017 11:18:42 Pd.q@Pd06.calc.atom 4
293936 0.25076 vasp-Pd.qs bob  r     05/30/2017 11:25:57 Pd.q@Pd09.calc.atom 4
293937 0.25068 vasp-Pd.qs bob  r     05/30/2017 11:26:57 Pd.q@Pd07.calc.atom 4
293938 0.25067 vasp-Pd.qs bob  r     05/30/2017 11:32:12 Pd.q@Pd05.calc.atom 4
293939 0.25066 vasp-Pd.qs bob  r     05/30/2017 11:33:27 Pd.q@Pd11.calc.atom 4
293940 0.25064 vasp-Pd.qs bob  r     05/30/2017 11:36:42 Pd.q@Pd13.calc.atom 4
293941 0.25064 vasp-Pd.qs bob  r     05/30/2017 11:37:57 Pd.q@Pd15.calc.atom 4
269299 0.38432 vasp-Tc.qs bob  qw    05/25/2017 10:26:16                     4
269300 0.38430 vasp-Tc.qs bob  qw    05/25/2017 10:27:25                     4
269301 0.38428 vasp-Tc.qs bob  qw    05/25/2017 10:28:36                     4
269302 0.38426 vasp-Tc.qs bob  qw    05/25/2017 10:29:17                     4
269314 0.38410 vasp-Tc.qs bob  qw    05/25/2017 10:38:05                     4
269320 0.38354 vasp-Tc.qs bob  qw    05/25/2017 11:08:47                     4
269321 0.38352 vasp-Tc.qs bob  qw    05/25/2017 11:09:36                     4
293942 0.25063 vasp-Pd.qs bob  qw    05/30/2017 11:22:57                     4
293943 0.25060 vasp-Pd.qs bob  qw    05/30/2017 11:24:33                     4
293944 0.25059 vasp-Pd.qs bob  qw    05/30/2017 11:24:55                     4

All:       78
Extracted: 22 (3=bob)

key head            types
1   job-ID          22
2   prior           18
3   name            2
4   user            1
5   state           2
6   submit/start at 22
7   queue           13
8   slots           1
9   ja-task-ID      1

複数条件による絞り込みもできます。

% cat qstat.out| columnanalyze 4=bob 5=r
1      2       3          4    5     6                   7                   8     9
job-ID prior   name       user state submit/start at     queue               slots ja-task-ID
269156 0.40572 vasp-Tc.qs bob  r     05/30/2017 02:43:57 Tc.q@Tc05.calc.atom 4
269159 0.40572 vasp-Tc.qs bob  r     05/30/2017 03:47:12 Tc.q@Tc02.calc.atom 4
269161 0.40572 vasp-Tc.qs bob  r     05/30/2017 10:01:27 Tc.q@Tc06.calc.atom 4
269162 0.40572 vasp-Tc.qs bob  r     05/30/2017 10:52:12 Tc.q@Tc07.calc.atom 4
293923 0.25243 vasp-Pd.qs bob  r     05/30/2017 09:45:27 Pd.q@Pd12.calc.atom 4
293935 0.25081 vasp-Pd.qs bob  r     05/30/2017 11:18:42 Pd.q@Pd06.calc.atom 4
293936 0.25076 vasp-Pd.qs bob  r     05/30/2017 11:25:57 Pd.q@Pd09.calc.atom 4
293937 0.25068 vasp-Pd.qs bob  r     05/30/2017 11:26:57 Pd.q@Pd07.calc.atom 4
293938 0.25067 vasp-Pd.qs bob  r     05/30/2017 11:32:12 Pd.q@Pd05.calc.atom 4
293939 0.25066 vasp-Pd.qs bob  r     05/30/2017 11:33:27 Pd.q@Pd11.calc.atom 4
293940 0.25064 vasp-Pd.qs bob  r     05/30/2017 11:36:42 Pd.q@Pd13.calc.atom 4
293941 0.25064 vasp-Pd.qs bob  r     05/30/2017 11:37:57 Pd.q@Pd15.calc.atom 4

All:       78
Extracted: 12 (3=bob 4=r)

key head            types
1   job-ID          12
2   prior           8
3   name            2
4   user            1
5   state           1
6   submit/start at 12
7   queue           12
8   slots           1
9   ja-task-ID      1

絞り込んだ上で、情報を解析することができます。 以下はさらにキー番号3の要素で解析しています。

% cat qstat.out| columnanalyze 4=bob 5=r 3

2.2.4 備考

2.2.4.1 要素の区切り

このコマンドは等幅フォントによる表形式のテキスト出力を想定しています。 全ての行でスペースになっている桁は区切りの領域に属し、 どれかの行でスペース以外の文字が入っている桁は文字列の領域と見做します。 以下に例を示します。

abcde ghij lmn pq       y 
 bc e ghi    nopqr t    yz
a cd      k        t   xyz
--------------------------
ooooo oooooooooooo o   ooo
--1-- -----2------ 3   -4-

上3行が解析対象の行です。 この場合、[f,k,s,u,v,w] に相当する桁が全ての行で空白になっているので区切りになります。 ただし、[u,v,w] の桁は一続きの区切りなので間に空文字列を要素として見做したりしません。 4, 5, 6 行目は説明のための文字列です。 5行目は 1〜3行の非空白文字の有無の投影となっています 6行目にキー番号を示しています。

2.2.4.2 コンパクトな出力

ストリーム各行の出力はそのまま出すのではなく、 無駄な空白桁を減らしてコンパクトに出力するようにしています。

job-ID  prior   name       user         state submit/start at     queue                          slots ja-task-ID 
-----------------------------------------------------------------------------------------------------------------
 205712 0.75000 vasp-all.q alice        r     05/11/2017 15:37:12 Tc.q@Tc18.calc.atom                4        

↑ qstat.out。 ↓ columnanalyze の出力。

1      2       3          4       5     6                   7                   8     9
job-ID prior   name       user    state submit/start at     queue               slots ja-task-ID
205712 0.75000 vasp-all.q alice   r     05/11/2017 15:37:12 Tc.q@Tc18.calc.atom 4

2.2.4.3 引数

このコマンドはファイル名を引数に取ることができません。 理由は、ファイルから取得するよりも、 絞り込み条件と解析対象を修正しつつ何度も実行して情報を掘り出すことがメインの目的で、 ファイルからのストリームを扱うことはあまりなさそうだと判断したためです。 ファイルからストリームをとるときは cat から パイプするなどしてください。

2.3 columnform

空白区切りで要素ごとに、表示の桁を揃えます。

% cat sample1.txt
a ab
abc a

% columnform sample1.txt
a   ab
abc a

行頭がインデントされている場合は、 ストーム内で最小のインデント幅を基準とします。

% cat sample2.txt
  a ab
    abc a

% columnform sample2.txt
  a   ab
  abc a

-t, --transpose オプションを付けると転置します。

% columnform sample2.txt -t
  a  abc
  ab a

-l, --left-just オプションで左詰め(デフォルト)ですが、 -r, --right-just オプションで右詰めとなります。

% columnform sample2.txt -l
  a   ab
  abc a

% columnform sample2.txt -r
    a ab
  abc  a

書き出しの要素間に挟むセパレータを -s, --separator=char オプションで指定できます。

% columnform sample2.txt -s "|"
a  |ab
abc|a

2.4 indentconv

インデントの字下げ幅を変更します。 以下の例では字下げ単位を2から 4 に変更しています。

% cat sample0.txt
a
  b
    c
  b
    c

% indentconv 2 4 sample0.txt
a
    b
        c
    b
        c

0 は特殊な数字で、タブ文字1個を意味します。 以下の例では字下げをスペース2個からタブに変更しています。

% cat sample1.txt
  a
    b
      c
    b
      c

% indentconv 2 0 sample1.txt
    a
        b
            c
        b
            c

以下の例では字下げをタブからスペース2個に変更しています。

% cat sample2.txt
a
    b
        c
    b
        c

% ~/git/tefil/bin/indentconv 0 2 sample2.txt
a
  b
    c
  b
    c

2.5 indentstat

ファイル内のインデント幅の集計を取ります。

% cat sample0.txt
a
  b
    c
      d
        e

    c
      d
        e

        e

% indentstat sample0.txt
 0|***
 2|*
 4|**
 6|**
 8|***

引数のファイルが1つならば上記のようにファイル名を表示しませんが、 複数ならばファイルごとにファイル名を表示します。

% cat sample1.txt
  a
    b
      c
        d
          e
  
      c
        d
          e
  
          e


% indentstat sample0.txt sample1.txt
sample0.txt:
 0|***
 2|*
 4|**
 6|**
 8|***

sample1.txt:
 2|***
 4|*
 6|**
 8|**
10|***

-m, --minimum オプションは 非ゼロの最小のインデント幅を出力します。 ただし、全ての行のインデント幅が 0 ならば 0 を出力します。

% indentstat sample0.txt -m
2

2.6 md2fswiki

Markdown 形式のフォーマットを FreeStyleWiki 形式に変更します。 ただし完璧に動作するわけではなく、複雑な構文は処理できません。

% cat sample.md
# head1

## head2

### head3

abc *italic* def
abc **bold** def
* item
    * item
        * item
            * item
1. enum
    1. enum
        1. enum
            1. enum
[Google](http://www.google.co.jp/)" ,
    formatted text"

<!-- comment -->

% md2fswiki sample.md
!!! head1

!! head2

! head3

abc ''italic'' def
abc '''bold''' def
* item
*** item
     '' item
         '' item
+ enum
+++ enum
     1. enum
         1. enum
[Google|http://www.google.co.jp/]" ,
 formatted text"

<!-- comment -->

2.7 fswiki2md

FreeStyleWiki 形式のフォーマットを Markdown 形式に変更します。 ただし完璧に動作するわけではなく、複雑な構文は処理できません。

% cat sample.fswiki
!!! head1
!! head2
! head3
abc ''italic'' def
abc '''bold''' def
* item
** item
*** item
**** item
+ enum
++ enum
+++ enum
++++ enum
[Google|http://www.google.co.jp/]" ,
 formatted text"
----"
// comment"

% fswiki2md sample.fswiki
# head1

## head2

### head3

abc *italic* def
abc **bold** def
* item
    * item
        * item
            * item
1. enum
    1. enum
        1. enum
            1. enum
[Google](http://www.google.co.jp/)" ,
    formatted text"
---"
<!-- comment"-->

2.8 percentpack

URL などに用いられる % を伴う 16 進数表記を バイナリにパッキングします。

% cat sample.txt
%E3%83%86%E3%82%B9%E3%83%88

% percentpack sample.txt
テスト

2.9 zshescape

文字列のうち、 zsh で特別な意味を持つものをバックスラッシュエスケープします。 ファイル名の変更スクリプト作成などに使えるでしょう。

% cat sample.txt
abcdABCD * * *

% zshescape sample.txt
abcdABCD\ \*\ \*\ \*

2.10 linesub

行ごとに文字を置換します。 以下の例は「beggar」という文字列を「BEGGAR」に置換します。 デフォルトでは最初にヒットした文字列のみ置換します。

% cat sample1.txt
A bad workman always blames his tools. Once a beggar, always a beggar.

% linesub beggar BEGGAR sample1.txt
A bad workman always blames his tools. Once a BEGGAR, always a beggar.

ヒットする箇所全てを置換するには -g, --global オプションを付加します。

% linesub beggar BEGGAR sample1.txt -g
A bad workman always blames his tools. Once a BEGGAR, always a BEGGAR.

置換元を正規表現として扱うには -r, --reg-exp オプションを付加します。

% linesub "\S" x sample1.txt -r -g
x xxx xxxxxxx xxxxxx xxxxxx xxx xxxxxx xxxx x xxxxxxx xxxxxx x xxxxxxx

2.11 linesplit

行を分割して複数の行にします。 句点の後ろに改行を挿入するようなことができます。

% cat sample1.txt
A bad workman always blames his tools. Once a beggar, always a beggar.

% linesplit sample1.txt
A bad workman always blames his tools.
 Once a beggar, always a beggar.

デフォルトでは半角ピリオドのみに作用します。 上の例では切り分けられた後の行の先頭に空白が入っています。 これは元の文で 「. 」のようにピリオドのあとの空白がありますが、 ピリオドのあとに改行文字が入ったためです。 行頭や行末の空白を削除するために --strip オプションを用意しています。

% linesplit --strip sample1.txt
A bad workman always blames his tools.
Once a beggar, always a beggar.

区切り文字のデフォルトは半角ピリオドですが、 --separator オプションで文字列を複数指定できます。 指定は半角スペース区切りでまとめた文字列として与えます。

% linesplit --separator=", ." sample1.txt
A bad workman always blames his tools.
 Once a beggar,
 always a beggar.

--separator オプションで 文字列 を指定できるという例を示します。

% linesplit --separator=beg sample1.txt
A bad workman always blames his tools. Once a beg
gar, always a beg
gar.

半角スペースを区切り文字に追加する場合には --space オプションを使います。

% linesplit --space sample1.txt
A 
bad 
workman 
always 
blames 
his 
tools.

Once 
a 
beggar, 
always 
a 
beggar.

「Fig. 」などの略称のピリオドを除外したいことがあるかもしれません。 このために、 --except オプションで例外となる文字列を指定できます。 -e, --except オプションは例外となる文字列を追加します。 これまでのサンプルで、 tools. を除外することにしましょう。

% linesplit --except="tools." sample1.txt
A bad workman always blames his tools. Once a beggar, always a beggar.

この例外文字列も複数指定できます。 実行結果は示しませんが、以下のようにできます。

linesplit --except="FIG. Fig."

3 ライブラリの使用方法

3.1 テキストフィルタおさらい

テキストフィルタ、使ってますか? 文字列からなるストリームを適当に加工するものですが、 unix は様々なフィルタが用意されています。 (see フィルタ (ソフトウェア) - Wikipedia) grep, cat, head, tail, wc, sort などが該当します。 Tefil ライブラリはこのようなフィルタを自作するためのフレームワークを提供します。 本節では 幾つかのフィルタプログラムを見ながら、 フレームワークにどのような機能が必要かを確認していきましょう。

3.1.1 grep

grep は入力を行ごとに処理し、文字列を含む行を抽出します。

% grep foo file.txt
% grep foo *.txt
% dmesg | grep eth

上記の 1 行目では、ファイル file.txt から foo という文字列を含む行のみを表示し、 それ以外の行を無視します。 2 行目のように複数のファイルを指定できます。 フィルタは 1, 2 行目のように、可変個数のファイルを扱えると便利です。

3 行目のように、ファイルの中身だけでなく、他のプログラムの出力に フィルタをかけることもできます。 3 行目は地味なようですがパイプ処理によって他のコマンドと連携させて使用することが可能になり、 応用がぐっと広がります。 grep では引数で与えるファイルの個数が 0 個のときに、入力をファイル入力から標準入力に切り替えます。 ファイル入力と標準入力は一見大きく異なるものに見えるかもしれませんが、 プログラム上はいずれもストリームという上位概念で包括されます。 プログラムに順にデータが流し込まれ、流れ出ていくと考えましょう。 フィルタは標準入力を扱えるべきです。

3.1.2 bc

grep は行ごとに処理していますが、 キーボード入力に対してフィルタリングして便利になるケースは grep ではほぼありません。 このため入力を一括して取り込み、文字列として処理すれば良いような気がしてきます。 しかしテキストフィルタには、これでは不便な場面が出てきます。 その例に bc コマンドを挙げましょう。 bc コマンドは簡単な計算機です。 以下のように使います。

% echo "1+2" | bc
3

% bc
bc 1.06.95
Copyright 1991-1994, 1997, 1998, 2000, 2004, 2006 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'. 
1+2
3
2+3
5

前者は標準入力に "1+2" という数式を与えたもので、 プログラムは 3 を出力して終了します。 後者は 引数を与えずに実行したもので、 「bc 1.06.95」から「For details type `warranty'. 」を表示したあとキーボードからの入力を待ちます。 そこで 1+2[Enter] と打つと プログラムが 3 を返します。 終了するには Ctrl-d を打ちます。

もし入力を一括して受け取ってから文字列として処理するような処理だったら、 後者で 「1+2」と打っても 「3」が出力されず、 「2+3」と Ctrl-d を打ったあとに答えが「3」「5」と出力されることになります。 このようなプログラムは対話的に反応が返る方が好ましいです。 一般にフィルタは文字列として入力を受けるのではなく、 IO ストリーム として情報を処理すべきだということです。

3.1.3 sort

grep, bc は行ごとに完結した処理で出力を行っていました。 しかしフィルタには行ごとでは処理を行わず、 ストリーム全体の情報を使って出力を行うものもあります。 この例として sort を挙げましょう。

% dmesg | sort

出力は割愛しますが、 dmesg の出力が行単位でソートされて出力されます。 ソートするということは最初に出力すべきものが 最後に入力される可能性があるわけで、 必然的に入力が完了するまで出力が行われないことになります。

このことから、フィルタは行単位の処理で完結できるとは限らないことが分かります。

3.1.4 まとめ

ここまでをまとめると、以下のようになります。

3.2 Tefil の役割

Tefil はフィルタを自作するためのフレームワークを提供します。 Tefil::TextFilterBase がフィルタプログラムの骨格となるクラスです。 このクラスは process_stream メソッドで例外を生じる抽象クラスになっています。 プログラマはこのクラスを継承したサブクラスで process_stream メソッドを定義してください。 基本的には、bin/ に入っているプログラムおよび ここから require されているバックエンドのライブラリのソースを見れば分かると思います。

本節のサンプルファイルは以下に置いてあります。

3.3 フィルタのプログラミング

3.3.1 インストール

% gem install tefil

3.3.2 簡単な使い方

Tefil を使った簡単なフィルタ作成を実演してみましょう。 入力を行単位に分割し、 行の内容を10回繰り返すフィルタを作ってみます。 ファイル名を list010.rb とします。

#! /usr/bin/env ruby
# coding: utf-8

require "tefil"

class Filter10Times < Tefil::TextFilterBase
  def process_stream(in_io, out_io)
    in_io.each_line do |line|
      out_io.puts line.chomp * 10
    end
  end
end

f10 = Filter10Times.new
f10.filter(paths: ARGV)

説明は不要だと思いますが、一応。

  1. tefil を require する。
  2. Tefil::TextFilterBase を継承した Filter10Times クラスを作成。
  3. Filter10Times クラスで process_stream を作成。
  4. Filter10Times クラスインスタンス f10を作成。
  5. f10 のメソッド filter を作用させ、コマンドライン引数の配列を与える。

process_stream は 2 つの IO を引数に取ります。 これが文字列ではないということに注意してください。

filter メソッドに与える配列の要素をパスと見做して、 各個処理します。 この配列が空ならば、STDIN を扱います。

テスト環境を作って実行してみましょう。 以下の内容ファイルを用意して、

% cat ora.txt
オラ

% cat muda.txt
無駄

引数ファイル1個の場合。

% ruby list010.rb ora.txt
オラオラオラオラオラオラオラオラオラオラ

引数ファイル2個の場合。

% ruby list010.rb ora.txt muda.txt
オラオラオラオラオラオラオラオラオラオラ
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄

ファイル指定なしの場合、標準入力から。

% echo "オラ" | ruby list010.rb
オラオラオラオラオラオラオラオラオラオラ

キーボード入力してみましょう。 以下は「オラ[Enter]無駄[Enter]」と入力しています。

% ruby list010.rb
オラ
オラオラオラオラオラオラオラオラオラオラ
無駄
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄

filter メソッドを使う方法は簡単なので、まずは これを使うと良いでしょう。

3.3.2.1 上書き機能

filter メソッドの output に :overwrite を付加すると 上書きモードになります。

f10.filter(paths: ARGV, output: :overwrite)

3.3.2.2 バックアップ付き上書き機能

f10.filter(paths: ARGV, output: :overwrite_backup, suffix: ".bak")

filter メソッドの output に :overwrite_backup を付加すると バックアップ付きの suffix として文字列を与えるとバックアップファイルの末尾に 付加されます。 デフォルトは ".bak" です。

3.3.3 複数フィルタの連携(ストリーム全体を使った処理)

複数のフィルタを連携させましょう。 ここでは 別に行末に「!」を追加する FilterExclamation を作成し、 先に作った Filter10Times と連携してみましょう。 以下の list020.rb を作ります。

#! /usr/bin/env ruby
# coding: utf-8

require "tefil"

class Filter10Times < Tefil::TextFilterBase
  def process_stream(in_io, out_io)
    in_io.each_line do |line|
      out_io.puts line.chomp * 10
    end
  end
end

class FilterExclamation < Tefil::TextFilterBase
  def process_stream(in_io, out_io)
    in_io.each_line do |line|
      out_io.puts line.chomp + "!"
    end
  end
end

f10 = Filter10Times.new
fex = FilterExclamation.new
Tefil::TextFilterBase.open_stream(paths: ARGV, show_filenames: true) do |io|
  result = io.tefil_filter(f10).tefil_filter(fex)
  print result.read
end

実行してみましょう。

% ruby list020.rb ora.txt
オラオラオラオラオラオラオラオラオラオラ!

最後に「!」が付きました。 list020.rb を以下のように書き換えてみましょう。 ここでは list030.rb としています。

  #result = io.tefil_filter(fex).tefil_filter(f10)
  result = io.tefil_filter(f10).tefil_filter(fex)

実行すると以下のようになります。 作用する順番が変わってますね。

% ruby list030.rb ora.txt
オラ!オラ!オラ!オラ!オラ!オラ!オラ!オラ!オラ!オラ!

標準入力、特にキーボード入力はどうでしょうか? ruby list020.rb で実行し、「オラ」と入力しても下のように反応しないように見えます。 ここで Ctrl-d を入力するとストリームが完結し、処理されます。

% ruby list020.rb
オラ

list020.rbopen_streamshow_filenames: true を渡しています。 これで複数ファイルを引数で渡したときに、 ファイルの先頭でファイル名を表示するようになります。

% ruby list020.rb ora.txt muda.txt
ora.txt:
オラオラオラオラオラオラオラオラオラオラ!
muda.txt:
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄!

IO#tefil_filter を使った処理は、 ストリーム全体を使うため、ストリームが完結するまで処理が行われません。 このため、sort や wc のような処理に向いています。 しかし bc でキーボード入力と対話的に使用するようなことはできません。

3.3.4 複数フィルタの連携(行単位の処理)

行単位での処理を行うようにしてみましょう。 list020.rb を以下のように修正したものを list040.rb とします。

result = io.tefil_filter(f10).tefil_filter(fex)
print result.read

io.each_line do |str|
  print str.tefil_filter(f10).tefil_filter(fex)
end

実行してキーボード入力してみましょう。 以下のように、Enter 入力直後に反応します。

% ruby list040.rb
オラ
オラオラオラオラオラオラオラオラオラオラ!

IO#each_line を使うことで 1行ごとに文字列を取得し、 その単位で処理を行います。 このため、 bc のような逐次処理する可能性のある処理もでき、最も汎用性の高い方法です。

3.3.5 既存クラスへの影響

require "tefil" すると、 IO クラス と StringIO クラスに tefil_filter メソッドが追加されます。 このメソッドは フィルタオブジェクトを引数として取り、 自身(self) の ストリームを process_stream で処理した StringIO を rewind して返します。 この機構により、メソッドチェインでフィルタをかけることが出来ます。

String クラス に tefil_filter メソッドを追加します。 Tefil::TextFilterBase#process_stream で定義されたのと 同様の処理を自身(self) に施した String を返します。 当然ですが、これもメソッドチェインでフィルタをかけることが出来ます。

3.4 メモ

3.4.1 Tefil::TextFilterBase クラス

Tefil::TextFilterBase はインスタンス変数を持っていないため、 クラスではなくモジュールでも良さそうに見えます。 しかし、これを継承したサブクラスを作る際に、 initialize でインスタンス変数を設定できた方が便利になることもあると判断しました。