HTMLツールを作る

locate_h

Unix を使って感激することの一つはパイプラインでしょう。例えば、ruby という文字列を含むファイルを検索するとき locate を使いますが、ファイルの数が多いと大部分が画面から消えてしまいます。そのようなときに、locate ruby | less のように locate の出力をページャにパイプさせるのはよくやる操作です。また、locate の出力をさらに sample という文字列で絞りこみたい時があります。そのようなときには、locate ruby | grep sample のように locate の出力を grep にパイプしてやります。Unix の特徴はこのようにツールを組合せてデータを操作することが簡単にできることです。

しかし、locate の出力は単なる文字列なので、目的のファイルを開こうと思ったら、less の後に長いパス名をキーボードから入力しなくてはなりません。もし、locate からの出力が HTML ファイルになっていてそれをブラウザーで表示できたらかなり便利でしょう。実は、それは可能なのです。また、locate の出力とブラウザの間に grep で絞りこみのフィルターをかけることもできるのです。

このページでも紹介した w3m はキャラクターベースの WWW ブラウザーですが、標準入力から HTML 文書を読み込んでレンダリングすることができます。それで、結果の表示にはこの w3m を利用することにします。

出力の表示は簡単にできそうなので、locate の出力を HTML にするのに Ruby を使いましょう。次のスクリプトを locate_h.rb で作成します。

#!/usr/local/bin/ruby
p = IO.popen( "locate #{ARGV[0]}", "r" )
p.each_line { |line| line.chop!; print "<a href=\"#{line}\">#{line}</a><br>\n" }
p.close

一行目のパスは自分の環境に合わせて変更してください。また chmod +x locate_h.rb でファイルを実行可能にしてください。それでは、コンソールから次のようにタイプしてテストして見ましょう。

$ ./locate_h.rb ruby

HTML 形式のファイルリストが画面に表示されれば成功です。コマンドの先頭に ./ をつけたのは最近のディストリビューションの設定ではカレントワーキングディレクトリーはコマンドサーチパスに登録されていないからです。コマンドサーチパスに登録されている(かもしれない) ~/bin/ ディレクトリーに locate_h.rb ファイルをいれておけば ./ は省略できます。それではいよいよ本番です。locate_h.rb の出力を w3m へパイプします。次のように入力してください。

$ ./locate_h.rb ruby | w3m -T text/html

ハイーパーリンクされた、ファイルリストが表示されたはずです。リンクにカーソルを移動して CR を押すだけでファイルを見ることができます。Shift + B で元のリストに戻ります。ちょっと感動的です。さらに ruby 関連のファイルのうち特に sample ディレクトリーのファイルを見たいときは grep でフィルタリングすることができます。

$ ./locate_h.rb ruby | grep sample | w3m -T text/html

w3m -T text/html は少し長いので alias w3m_h="w3m -T text/html" でエイリアスを作ります。また locate_h.rb もコマンドサーチパスの通った ~/bin/ ディレクトリーに移しておきます。すると上の操作は次のようになります。

$ locate_h.rb ruby | grep sample | w3m_h

通常のストリームを使っているのと全く同じ感覚で使えます。w3m_h は標準入力からの HTML 文書を読み込めますから、namazu の出力を -h オプションで HTML 文書にしたときの受け皿にすると便利です。具体的には次のようにします。namazu の利用法についてはこのホームページの他の場所で詳しく述べています。

$ namazu -h ruby ~/index/ | w3m_h

wlocate.rb

locate_h.rb は便利ですが、grep のフィルターを使わない場合もっとキー入力を少なくしたい気もします。そんなときは、次の wlocate.rb を使うこともできます。

#!/usr/local/bin/ruby
pr = IO.popen( "locate #{ARGV[0]}", "r" )
pw = IO.popen( "w3m -T text/html", "w" )
pr.each_line { |line| line.chop!; pw.print "<a href=\"#{line}\">#{line}</a><br>\n" }
pr.close
pw.close

これは次のように使います。

$ wlocate.rb ruby

キー入力が随分減って楽になりました。locate_h.rb も wlocate.rb も IOクラスの popen メソッドを使っています。popen メソッドは子プロセスを実行してその標準入力および標準出力を Ruby の IO オブジェクトに繋げるメソッドです。popen の使い方は、ファイルを開くときと同じ感覚です。popen の第1引数は、実行する子プロセスのコマンドライン入力で、第2引数は Ruby 本体のスクリプトが子プロセスからの標準出力を読み取るときは "r" で、子プロセスの標準入力に書き込むときは "w" です。wlocate を例にとると、コマンドラインから次のように入力したのと同じになります。

$ locate ruby | Ruby 本体の処理 | w3m -T text/html

スクリプトの最後に、pr.close と pw.close でIOオブジェクトを閉じます。IO オブジェクトは close しないとハングアップしてしまうので注意が必要です。

wgrep.rb

grep も HTML 化したいコマンドの一つです。次のスクリプトを wgreb.rb で作成します。

#!/usr/local/bin/ruby
ARGV[1] = '*' if ARGV[1] == nil
pwd = `pwd`.chop!
# pr = IO.popen("grep #{ARGV.join(' ')}", "r")
pr = IO.popen("grep -H #{ARGV.join(' ')}", "r")
pw = IO.popen("w3m -T text/html", "w")
pr.each_line do |line|
  line =~ /.*?:/
  f = $&.dup.chop!
  s = $'.dup.chop!
  f = pwd + '/' + f if f =~ /[^\/]/
  s2 = s.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
  pw.print "<a href=\"#{f}\">#{f}</a><br>"
  pw.print "#{s2}<br><br>\n"
end
pr.close
pw.close

1行目のパスは環境に合わせて設定する必要があります。grep のバージョンが古いと -H オプション(ファイル名を必ず表示する)がない場合があります。その場合は5行目の文を # でコメントアウトして、4行目の # を消して使ってください。wgrep へのコマンドライン引数は直接 grep へ渡されるので、wgrep.rb の使い方は、ほとんど grep と同じになります。使い方は次のようになります。

$ ./wgrep.rb print *

grep に比べて速度は遅くなりますが、ハイパーリンクでファイルを閲覧できます。また、grep よりちょっぴり賢くなっていて次のように第2引数を省略するとカレントワーキングディレクトリーの全てのファイルからキーワードを検索してくれます。

$ ./wgrep.rb print

wgrep.rb で使った文で説明をしなければならないのは2行目の `pwd` くらいです。バッククォートでシェルコマンドを囲むとその出力が文字列または配列となって返されます。バッククォートの中では変数の展開もすることができます。

hgrep.rb

wgrep を使っていてもファイルが多く grep でフィルタリングしたいと思うこともあります。wgrep を改造して、出力を標準出力に出すようにしましょう。それには、wgrep のスクリプトのうち w3m へのパイプに関する部分を削除するだけです。次のスクリプトを hgrep.rb というファイルに作成します。機能削減による改良(?)です。

#!/usr/local/bin/ruby
ARGV[1] = '*' if ARGV[1] == nil
pwd = `pwd`.chop!
# pr = IO.popen("grep #{ARGV.join(' ')}", "r")
pr = IO.popen("grep -H #{ARGV.join(' ')}", "r")
pr.each_line do |line|
  line =~ /.*?:/
  f = $&.dup.chop!
  s = $'.dup.chop!
  f = pwd + '/' + f if f =~ /[^\/]/
  s2 = s.gsub(/&/n, '&amp;').gsub(/\"/n, '&quot;').gsub(/>/n, '&gt;').gsub(/</n, '&lt;')
  print "<a href=\"#{f}\">#{f}</a><br>"
  print "#{s2}<br><br>\n"
end
pr.close

これで、grep によるフィルタリングが可能になりました。次のように使います。

$ hgrep.rb print | grep name | w3m_h

ここまで述べたツールは全て、実はシェルスクリプトと Perl で作ってこのホームページの他の所で紹介したものです。しかし、Ruby を使うと非常にすっきりと記述することができます。

anchor.rb

これは HTML ファイルからアンカーだけを取り出すプログラムです。次のように使うと index.html からアンカーだけを取り出して w3m でリンクをたどることができます。

$ anchor.rb index.html | w3m_h

カレントディレクトリーの全ての HTML ファイルからリンクを抜き出す事ができます。

$ anchor.rb *.html | w3m_h

結果をファイルに保存することもできます。

$ anchor.rb index.html > anchors.html

index.html ファイルの中のアンカー(リンク)の数を数えることもできます。

$ anchor.rb index.html | wc -l
     32

links.html ファイルのアンカーから w3m を含むものだけを取り出します。

$ anchor.rb links.html | grep w3m | w3m_h

anchor.rb のスクリプトは下のようになります chomod +x anchor.rb として実行可能にして、コマンドサーチパスの通ったディレクトリーに入れておくと便利です。

#!/usr/local/bin/ruby

line = ''
while ( s = gets )
  line += s.chop!
  if line !~ /<a href/
    line = ''
    next
  else
    while line =~ /<a href.*?<\/a>/
      print "#{$&}<br>\n"
      line = $'.dup
    end
  end
end

anchor.rb の実用的な用途を無理に考えると、リンク集の要約ができるかなと言う気もしますが、実用性ゼロと考えるのが素直です。でも、面白い。

txt2html.rb

今度はテキストファイルを HTML ファイルに変換してみましょう。変換に際しての簡単なルールを決めておきます。つまり、テキストファイルの1行が HTML ファイルの<p>と</p>に挟まれた段落に変換されます。また <pre> だけの行と </pre> だけの行に囲まれた部分はそのまま変換せずに置きます。たった二つだけの規則ですが個人的な HTML 文書を作るのには十分です。

text2html.rb の使い方は次のようになります。

$ ./text2html.rb test.txt

で画面に表示して変換がきちんと行われるか確認します。

$ ./txt2html.rb test.txt | w3m_h

でレンダリングがうまくいくか確認できます。

$ ./txt2html.rb test.txt > test.html

で html 文書をファイルに保存します。

txt2html.rb のスクリプトは次のようになります。

#!/usr/local/bin/ruby

class String
  def convert!
    self.gsub!(/\&/, '&amp;')
    self.gsub!(/\"/, '&quot;')
    self.gsub!(/\</, '&lt;')
    self.gsub!(/\>/, '&gt;')
    self
  end
end

line = gets
while ( line )
  line.chop!
  if line == '<pre>'
    puts line
    ln = gets.chop!
    while ( ln != '</pre>' )
      puts ln.convert!
      ln = gets.chop!
    end
    puts ln
  else
    print "<p>#{line.convert!}<\/p>\n"
  end
  line = gets
end

ここで強調したいテクニックは冒頭の class String の部分です。何と、組み込みクラスである String クラスに新しいメソッド convert! を定義できてしまうのです。そうしてこの convert! は String クラスのどのオブジェクトにも他の組み込みメソッドと同じように使うことができるのです。(うれしい!)

ところで convert! メソッドは名前の最後に ! がつけられていますがその場合は object.convert! でメソッドを呼び出した場合 object の内容が変わってしまうことを示します。a.chop は文字列 a の最後の一文字を取り去ったオブジェクトを返しますが a 自身はそのままです。a.chop! の場合は a 自身の最後の文字を取り去ります。String クラスには include? と最後に ? がついたメソッドがありますがこれはそのオブジェクトの中に引数の文字列があるかどうかを問い合わせるメソッドです。! や ? 自身に特別な意味があるわけではありませんが、これをメソッドの名前につけることによってメソッドの操作の種類を暗示しているわけです。

次のような test.txt を作成して、

これは txt2html.rb のテストです。&, ", <, >, は自動的に変換されます。
<pre>
for i in 1..3
  puts i
end
</pre>

txt2html.rb test.txt > test.html で変換を実行すると次のようになります。

<p>これは txt2html.rb のテストです。&amp;, &quot;, &lt;, &gt;, は自動的に変換されます。</p>
<pre>
for i in 1..3
  puts i
end
</pre>

txt2html.rb は HTML のタグが含まれているテキスト文書はきちんと変換してくれません。convert! メソッドの処理を変更すればタグにも対応することができるでしょうが、google 検索すると txt2html という名前のツールはたくさんあります。それらを利用したほうがいいかも知れません。CUI に限らず GUI でもコンピュータにやらせたいことの数だけツールやコマンドを憶える必要がでてきます。出来合いのツールを使いこなすために払う労力よりも自分で小さなツールを作った方がかえって楽なこともあるのではないでしょうか。「他人の作ったツールはできるだけ単機能で使い、自分のツールはできるだけ労力を少なく作りたい。」というのが私の夢です。Ruby を知ってその夢が少し叶いそうな気がして来ました。

header.rb

これまで述べた HTML ツールでは HTML の出力はボディ部分のみでした。この方がフィルターをかけて処理するのに都合がよいからです。header.rb はその出力にヘッダを付け加えて HTML 文書の形を整えます。

#!/usr/local/bin/ruby

print <<EOS
<html>
<head><title>no title</title></head>
<body>
EOS
while ( line = gets )
  puts line
end
print <<EOS
</body>
</html>
EOS

<<EOS はシェルスクリプトや Perl などでも使われているヒア・ドキュメントです。<<EOS の下の行から次の EOS の行までの間の部分が print メソッドの引数として渡されます。print で出力される行数が多いときはタイプ量を軽減できます。

header.rb は次のように使います。

$ header.rb foo.html > bar.html

また、複数の(ボディ部分だけの) HTML 文書を結合して一つの HTML 文書にまとめることができます。

$ header.rb foo1.src foo2.srb > bar.html
さらに、anchor.rb や txt2html.rb の出力を最終的に整形するフィルターとして使うこともできます。

$ anchor.rb *.html | grep w3m | header.rb > foo.html

body.rb

body.rb は header.rb とは逆の処理をするツールです。HTML 文書からボディ部分を抜きだします。簡単なプログラムですので説明は要らないと思います。

#!/usr/local/bin/ruby
line = gets
while( line !~ /<body/ )
  line = gets
end
line = gets
while ( line !~ /<\/body/ )
  puts line
  line = gets
end

使い方は次のようにします。

$ body.rb foo.html > foo_body.html

HTML 関係のツールとしては、この他にもリンク集の死んでいるリンクを検出してマークをつけるものとか、ホームページの更新を検出するツール(これは Ruby のアーカイブに既にあるようです)など欲しいものはたくさんあるのですが、著者の知識不足でまだ作れていません。Ruby のプログラミングは比較的楽で、また後で読み返しても暗号文になりにくい特徴があります。個人的な用途に小さいツール(多くはフィルターを)つくってちょこちょこっと問題解決をするのにも Ruby はうってつけだと思います。