■ CGI: srcview ソースコードかんたんブラウザ

CGI で実現した、ソースコードブラウザです。 手ものにあるソースコードを軽く確認したいような場合にどうぞ。 参照先がわかるキーワードは、参照先へ飛ぶことが可能です。 C を対象にして書かれています。 一部、Ruby も対応しています。

セキュリティについては、CGI が実行されるシステムからのアクセスしか許さないようになっています。 アクセスを許可するホストを登録するようにするか、公開していい特定のディレクトリ以下だけを公開するように修正が必要と思います。 Web サーバの機能に依存してセキュリティ機能をインプリメントすると、設定に依存してしまうことになります。 今回は Web サーバへ依存するセキュリティ設定を利用しない方法にしています。

■ 使い方

準備

CGI の設定で URL として http://localhost/cgi-bin/src.rb で呼びだしが可能という前提で進めさせてください。 このために cgi-bin ディレクトリへスクリプトを入れてください。

参照するソースコードが入っているディレクトリで、コマンド etags を実行します。 これで必要な「TAGS」というファイルが作成されます。

$ etags *.c *.h
関連するソースコードが、複数のディレクトリに分散している場合は、そのディレクトリごとに TAGS を作成します。 このとき、関連するディレクトリのソースコードも参照するようにします。
$ etags *.c *.h lib/*.[ch]
$ cd lib
$ etags *.c *.h ../*.[ch]       # *.c *.h ../*.h でもいい場合もありますね

呼びだし URL

呼びだす URL は、次のような形になります。

「ディレクトリ」を指定した場合は、ディレクトリに登録されているファイルの一覧が表示されます。 ファイル名をクリックすると、そのファイルが表示されます。

「ディレクトリ」「:」「キーワード」と、「ディレクトリ」のあとに「:」で区切り、「キーワード」を指定した場合、「キーワード」に関連したファイルの一覧が表示されます。

「ソースファイル」を指定した場合には、そのソースが表示されます。

ソースが表示されると、etags コマンドで有効になっているキーワードは、クリックでリンク先へ飛べるようになっています。

ちょっと便利なので試してみてください。

■ ソースコード

Web ブラウザでの表示上は Ruby のスクリプトですが、HTML の特殊文字をエスケープしています。 テキストとしてセーブしてご利用ください。

このスクリプトは、漢字コードを EUC Japan として使用してください。 一部漢字コードに依存している記述をしています。

#! /usr/local/bin/ruby -Ke
# /home/tetsu/src/ruby/cgi/src.rb
# Created: March 28,2002 Thursday 14:56:15
# Author: tetsu(WATANABE Tetsuya)
# $Id: src.rb,v 1.8 2002/04/03 09:06:39 tetsu Exp $
# usage:

class Etags
  def initialize(dir)
    @path = dir + '/TAGS'
    @macro = {}
    @define = {}
    @typedef = {}
    @func = {}
    @ignore = ['switch', 'if', 'sizeof', 'while']

    if File.directory?(dir) and File.exist?(@path)
      f = File.open(@path)
      parse(f)
      f.close
    end
  end
  attr_reader :macro, :define, :typedef, :func

  def search(key, path = nil)
    re = /#{key}/i
    arr = []
    [@func, @typedef, @define, @macro].each do |h|
      h.keys.each do |k|
        if k =~ re
          arr.push([k, h[k]])
        end
      end
    end
    arr
  end

  def parse(f)
    srcfile = ''

    while f.gets
      chop

      if ~/\f/
        srcfile = ''
      elsif srcfile == ''
        file, no = $_.split(/,/)
        srcfile = file
      elsif ~/^\s/ && srcfile =~ /\.(?:c|h)$/
      elsif ~/^extern\s/i && srcfile =~ /\.(?:c|h)$/
      else
        case srcfile
        when /\.(?:c|h)$/
          if ~/^\#\s*define/
            if ~/\001/
              fn, l = $_.split(/\177/)
              fn, l, b = l.split(/[\001,]/)
              store(@macro, srcfile, fn, l, b)
            else
              fn, l, b = $_.sub(/^\#\s*define\s+/, '').split(/[\177\s,]+/)
              store(@define, srcfile, fn, l, b)
            end
          elsif (~/typedef/ || ~/struct/ || ~/\}/) && ~/\001/
            fn, l = $_.split(/\177/)
            fn, l, b = l.split(/[\001,]/)
            store(@typedef, srcfile, fn, l, b)
          else
            fn, l, b = $_.split(/[\177,]/)
            store(@func, srcfile, fn, l, b)
          end
        when /\.rb$/
          fn, l, b = $_.split(/[\177,]/)
          fn, l, b = l.split(/[\001,]/)
          store(@func, srcfile, fn, l, b)
        end
      end
    end
  end
  private :parse

  def store(h, src, fn, l, b)
    fn.sub!(/\W+$/, '')
    fn = fn.split(/\W+/)[-1]
    return if (@ignore.grep fn).size == 1
  
    if h.key? fn
      h[fn].concat(" #{src},#{l},#{b}")
    else
      h[fn] = "#{src},#{l},#{b}"
    end
  end
  private :store
end

require 'cgi-lib'

class SrcView
  def initialize(path)
    @src = []
    @etag = nil
    @path = path
    @dir = File.dirname path

    if File.exist? path
      @etag = Etags.new(@dir)
      @src = File.readlines(path) # .collect {|x| x.chop}
    end
  end

  def pr_html
    pre_str = ''
    script = ENV['SCRIPT_NAME'] || '/cgi-bin/src.rb'
    script.concat('?' + @dir + '/')
    basename = File.basename @path

    lineno = 0
    name_flag = nil

    @src.each do |l|
      name_flag = false
      lineno += 1
# escape char
      l.tr!('<>&"', "\001\002\003\004")    # "
# include
      if l =~ /^\#\s*include\s+\004(.+?)\004/
        inc_file = $1
        l.sub!(inc_file, CGI::tag('a', 'href' => script + inc_file) { inc_file })
      else
# 「(」ありの関数かマクロ
        l = l.gsub(/(\w+)(\s*\(?)/) do |m|     # l.gsub! するとだめみたい?
          f_name = $1
          last_char = $2
          if basename =~ /\.rb$/ and f_name == 'new'
            w = @etag.func['initialize']
          else
            w = @etag.func[f_name] || @etag.macro[f_name] || @etag.define[f_name] || @etag.typedef[f_name]        
          end

          if w
            f = l_no = b = nil
            arr = w.split
            arr.each do |a|
              f, l_no, b = a.split(/,/)
              break if f == basename
            end
            name_flag = true if f == basename and l_no.to_i == lineno
            if l_no && (arr.size == 1 || f == basename)  # 「宣言が一つ」または同じファイル? (これはどうかな?)
              CGI::tag('a', 'href' => script + f + '#' + l_no) { f_name } + last_char
            else
              CGI::tag('a', 'href' => script + f) { f_name } + last_char
            end
          else
            m
          end
        end
# 「(」なしのマクロ
      end
# name tag
      pre_str.concat CGI::tag('a', 'name' => lineno.to_s) if name_flag
# escape char を戻す
      pre_str.concat l.gsub(/\001/, '&lt;').gsub(/\002/, '&gt;').gsub(/\003/, '&amp;').gsub(/\004/, '&quot;')
    end

    CGI::print("Content-Type: text/html") {
      CGI::tag("html") {
        CGI::tag("head") {
          CGI::tag("title") { @path } + \
          CGI::tag('link', 'href' => '/base.css', 'type' => 'text/css', 'rel' => 'stylesheet')
        } +\
        CGI::tag("body") { CGI::tag('pre') { pre_str } }
      }
    }
  end
end

class SearchList
  def initialize(dir, key)
    @etag = nil
    @dir = dir
    @key = key
    if File.directory? dir
      @etag = Etags.new(dir)
    end
  end

  def pr_html
    script = ENV['SCRIPT_NAME'] || '/cgi-bin/src.rb'
    script.concat('?' + @dir + '/')
    arr = @etag.search(@key)
    tr_str = ''
    arr.sort.each do |a|
      item = a[0]
      a[1].split.sort.each do |i|
        src, l, b = i.split(/,/)
        tr_str.concat CGI::tag('tr') {
          CGI::tag('td') { CGI::tag('a', 'href' => script + src + '#' + (l || '1') ) { item } } + \
          CGI::tag('td') { src }
        }
      end
    end

    CGI::print("Content-Type: text/html") {
      CGI::tag("html") {
        CGI::tag("head") {
          CGI::tag("title") { "serach: #{@key}" } + \
          CGI::tag('link', 'href' => '/base.css', 'type' => 'text/css', 'rel' => 'stylesheet')
        } +\
        CGI::tag("body") { CGI::tag('table') { tr_str } }
      }
    }
  end
end

class DirList
  def initialize(dir)
    @dir = dir
    @list = []
    if File.directory? dir
      Dir.foreach(dir) do |f|
        fst = File.stat(dir + '/' + f)
        if f =~ /\.(c|h|txt|sh|rb|pl)$/ || f =~ /(makefile)/i
          t =
            case $1.downcase
            when 'c', 'h'
              'C'
            when 'txt', 'makefile'
              'T'
            when 'sh', 'rb', 'pl'
              'S'
            else
              'U'
            end
          @list.push([f, fst.size, t])
        elsif f[0, 1] == '.'
        elsif fst.directory?
          @list.push([f, 0, 'D'])
        end
      end
    end
    @list.sort!
  end

  def pr_html
    tr_str = ''
    script = ENV['SCRIPT_NAME'] || '/cgi-bin/src.rb'
    script.concat('?')

    @list.each do |arr|
      f, size, t = arr
      size_str = if size == 0 then '' else size.to_s end
      img_icon =
        case t
        when 'C'
          'c'
        when 'T'
          'text'
        when 'S'
          'script'
        when 'D'
          'dir'
        else
          'unknown'
        end
      img_str = CGI::tag('img', 'src' => '/icons/' + img_icon + '.gif', 'alt' => img_icon)
      
      tr_str.concat CGI::tag('tr') {
        CGI::tag('td') { img_str } + \
        CGI::tag('td', 'align' => 'right') { size_str } + \
        CGI::tag('td') { CGI::tag('a', 'href' => script + @dir + '/' + f ) { f } }
      }
    end

    CGI::print("Content-Type: text/html") {
      CGI::tag("html") {
        CGI::tag("head") {
          CGI::tag("title") { @dir } + \
          CGI::tag('link', 'href' => '/base.css', 'type' => 'text/css', 'rel' => 'stylesheet')
        } +\
        CGI::tag("body") { CGI::tag('table') { tr_str } }
      }
    }
  end
end

def pr_welcome_page
  script = ENV['SCRIPT_NAME'] || '/cgi-bin/src.rb'
  script.concat('?')
  body_str = ''

  body_str.concat CGI::tag('h1') { '呼びだす場合の URL のサンプル' }
  body_str.concat CGI::tag('ul') {
    CGI::tag('li') { 'http://localhost' + script + 'ディレクトリの絶対パス' } + \
    CGI::tag('li') { 'http://localhost' + script + 'ディレクトリの絶対パス:キーワード' } + \
    CGI::tag('li') { 'http://localhost' + script + 'ソースファイルの絶対パス' }
  }

  CGI::print("Content-Type: text/html") {
    CGI::tag("html") {
      CGI::tag("head") {
        CGI::tag("title") { 'CGI: source view' } + \
        CGI::tag('link', 'href' => '/base.css', 'type' => 'text/css', 'rel' => 'stylesheet')
      } +\
      CGI::tag("body") { body_str }
    }
  }
end

require 'socket'

cgi = CGI.new

if ENV['REMOTE_ADDR'] == '127.0.0.1' || Socket.gethostname == Socket.gethostbyname(ENV['REMOTE_ADDR'])[0]
else
  w = File.open('/tmp/cgi-src.txt', 'a')
  w.puts Time.now.strftime('%x %X') +
    ' ' + ENV['SCRIPT_NAME'] +
    ' ' + ENV['REMOTE_ADDR'] +
    ' ' + ENV['HTTP_USER_AGENT'] +
    ' ' + cgi.keys.join(' ')
  w.close

  CGI::print("Content-Type: text/html") {
    CGI::tag("html") {
      CGI::tag("head") {
        CGI::tag("title") { 'no' } + \
        CGI::tag('link', 'href' => '/base.css', 'type' => 'text/css', 'rel' => 'stylesheet')
      } +\
      CGI::tag("body") { CGI::tag('p') { 'アクセスできまへん' } }
    }
  }
  exit
end

case cgi.size
when 1
  k = cgi.keys[0]
  if k =~ /:/
    dir, key = k.split(/:/)
    sl = SearchList.new(dir, key)
    sl.pr_html
  elsif File.exist? k
    fst = File.stat(k)

    if fst.directory?
      dirlist = DirList.new(k)
      dirlist.pr_html
    elsif fst.file?
      src = SrcView.new(k)
      src.pr_html
    end
  end
when 0
  pr_welcome_page
end

■ 解説

とりかえず、必要だった C のソースコードを追いかけるのと、Ruby のスクリプトをちょっとだけ確認できるようにしています。 まだ、リファレンス先の特定方法など調整が必要と思います。

キーワードサーチは、そこそこ便利です。 関数名などで、あるキーワードに一致するものを一覧表示させて確認できたりします。

$ w3m 'http://localhost/cgi-bin/src.rb?/usr/local/src/ruby-1.7.2:rb_'
$ w3m 'http://localhost/cgi-bin/src.rb?/usr/local/src/ruby-1.7.2:ruby_'
これでキーワード「rb_」とか「ruby_」を含むものを一覧表示します。 便利便利。

私は Mozilla を使っているのですが、Mozilla は、name タグ(タグは a かな)がたくさんあると、うまく動かないようです。 w3m がきびきび動くので、ソースコードを参照するなら w3m がいいと思います。

$ w3m 'http://localhost/cgi-bin/src.rb?/usr/local/src/ruby-1.7.2'

いまだに cgi-lib.rb を使っていたりします。 手軽に HTML を生成するためだったりします。 いいのかな?

扱うソースコードを C だけに限定したほうが記述がすっきりするのですが、私がよく扱う C と Ruby にしました。 Ruby については、ブラウザとして使いやすいかちょっと疑問がありますが、とりあえず。

■ 履歴

1.8 2002/4/3

TAGS ファイル中の相対パスのファイルを含めていなかったので修正。

1.7 2002/4/2

アクセス制御で exit していませんでした。

1.6 2002/4/2

チェックインミス。 欠番。 コメントだけ書いたけど...

1.5 2002/4/1 あらら

最初の公開 version です。


渡辺哲也(WATANABE Tetsuya): Tetsuya.WATANABE atmark nifty.com