Last-modified:2018/09/13 23:47:44.
RubyGems.org に公開してあります。
テキストフィルターコマンドのフレームワークと、 それを使った幾つかのフィルターコマンドを提供します。
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
-s, –split オプションを付けると、任意の文字を区切り文字とし、 各要素を別個に演算します。
% echo "1+2 , 3 * 4" | calc --split=","
3,12
-S, –supply オプションを付けると、 ストリームで与えられた演算に対して、最後に式を追加します。
% echo "1.2 3.4" | calc --split=" " --supply="/2"
0.60000000000000000000 1.70000000000000000000
ただし、単純に追加するだけなので 演算の優先順序が作用します。 以下の例では、1+2 に /2 を補足するので、“1+2/2” となり、結果が 2 となります。
% echo "1+2" | calc --supply="/2"
2.00000000000000000000
以下のような表形式のデータを加工するのに向いています。
0.0 0.1 0.2
0.3 0.4 0.5
0.6 0.7 0.8
インデントの字下げ幅を変更します。 以下の例では字下げ単位を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
ファイル内のインデント幅の集計を取ります。
% 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
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 -->
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"-->
URL などに用いられる % を伴う 16 進数表記を バイナリにパッキングします。
% cat sample.txt
%E3%83%86%E3%82%B9%E3%83%88
% percentpack sample.txt
テスト
文字列のうち、 zsh で特別な意味を持つものをバックスラッシュエスケープします。 ファイル名の変更スクリプト作成などに使えるでしょう。
% cat sample.txt
abcdABCD * * *
% zshescape sample.txt
abcdABCD\ \*\ \*\ \*
行ごとに文字を置換します。 以下の例は「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
-o, –overwrite で入力ファイルを上書きします。 -O, –overwrite-backup=suffix は入力ファイルを上書きしますが、 元ファイルのバックアップを取ります。 バックアップファイル名は suffix を末尾に付けたものになります。
行を分割して複数の行にします。 句点の後ろに改行を挿入するようなことができます。
% 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."
columnform, columnanalyze, table コマンドは Tefil から削除されました。 tabmani ジェムをご利用ください。 [2018-09-13]
テキストフィルタ、使ってますか? 文字列からなるストリームを適当に加工するものですが、 unix は様々なフィルタが用意されています。 (see フィルタ (ソフトウェア) - Wikipedia) grep, cat, head, tail, wc, sort などが該当します。 Tefil ライブラリはこのようなフィルタを自作するためのフレームワークを提供します。 本節では 幾つかのフィルタプログラムを見ながら、 フレームワークにどのような機能が必要かを確認していきましょう。
grep は入力を行ごとに処理し、文字列を含む行を抽出します。
% grep foo file.txt
% grep foo *.txt
% dmesg | grep eth
上記の 1 行目では、ファイル file.txt から foo という文字列を含む行のみを表示し、 それ以外の行を無視します。 2 行目のように複数のファイルを指定できます。 フィルタは 1, 2 行目のように、可変個数のファイルを扱えると便利です。
3 行目のように、ファイルの中身だけでなく、他のプログラムの出力に フィルタをかけることもできます。 3 行目は地味なようですがパイプ処理によって他のコマンドと連携させて使用することが可能になり、 応用がぐっと広がります。 grep では引数で与えるファイルの個数が 0 個のときに、入力をファイル入力から標準入力に切り替えます。 ファイル入力と標準入力は一見大きく異なるものに見えるかもしれませんが、 プログラム上はいずれもストリームという上位概念で包括されます。 プログラムに順にデータが流し込まれ、流れ出ていくと考えましょう。 フィルタは標準入力を扱えるべきです。
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 ストリーム として情報を処理すべきだということです。
grep, bc は行ごとに完結した処理で出力を行っていました。 しかしフィルタには行ごとでは処理を行わず、 ストリーム全体の情報を使って出力を行うものもあります。 この例として sort を挙げましょう。
% dmesg | sort
出力は割愛しますが、 dmesg の出力が行単位でソートされて出力されます。 ソートするということは最初に出力すべきものが 最後に入力される可能性があるわけで、 必然的に入力が完了するまで出力が行われないことになります。
このことから、フィルタは行単位の処理で完結できるとは限らないことが分かります。
ここまでをまとめると、以下のようになります。
Tefil はフィルタを自作するためのフレームワークを提供します。 Tefil::TextFilterBase がフィルタプログラムの骨格となるクラスです。 このクラスは process_stream
メソッドで例外を生じる抽象クラスになっています。 プログラマはこのクラスを継承したサブクラスで process_stream
メソッドを定義してください。 基本的には、bin/ に入っているプログラムおよび ここから require されているバックエンドのライブラリのソースを見れば分かると思います。
本節のサンプルファイルは以下に置いてあります。
% gem install tefil
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)
説明は不要だと思いますが、一応。
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 メソッドを使う方法は簡単なので、まずは これを使うと良いでしょう。
filter メソッドの output に :overwrite を付加すると 上書きモードになります。
f10.filter(paths: ARGV, output: :overwrite)
f10.filter(paths: ARGV, output: :overwrite_backup, suffix: ".bak")
filter メソッドの output に :overwrite_backup を付加すると バックアップ付きの suffix として文字列を与えるとバックアップファイルの末尾に 付加されます。 デフォルトは “.bak” です。
複数のフィルタを連携させましょう。 ここでは 別に行末に「!」を追加する 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.rb
で open_stream
に show_filenames: true
を渡しています。 これで複数ファイルを引数で渡したときに、 ファイルの先頭でファイル名を表示するようになります。
% ruby list020.rb ora.txt muda.txt
ora.txt:
オラオラオラオラオラオラオラオラオラオラ!
muda.txt:
無駄無駄無駄無駄無駄無駄無駄無駄無駄無駄!
IO#tefil_filter
を使った処理は、 ストリーム全体を使うため、ストリームが完結するまで処理が行われません。 このため、sort や wc のような処理に向いています。 しかし bc でキーボード入力と対話的に使用するようなことはできません。
行単位での処理を行うようにしてみましょう。 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 のような逐次処理する可能性のある処理もでき、最も汎用性の高い方法です。
require “tefil” すると、 IO クラス と StringIO クラスに tefil_filter
メソッドが追加されます。 このメソッドは フィルタオブジェクトを引数として取り、 自身(self) の ストリームを process_stream
で処理した StringIO を rewind して返します。 この機構により、メソッドチェインでフィルタをかけることが出来ます。
String クラス に tefil_filter
メソッドを追加します。 Tefil::TextFilterBase#process_stream
で定義されたのと 同様の処理を自身(self) に施した String を返します。 当然ですが、これもメソッドチェインでフィルタをかけることが出来ます。
Tefil::TextFilterBase はインスタンス変数を持っていないため、 クラスではなくモジュールでも良さそうに見えます。 しかし、これを継承したサブクラスを作る際に、 initialize でインスタンス変数を設定できた方が便利になることもあると判断しました。