CACAnet福岡フリースクールat 久留米:2001年6月13日

発表者:佐塚 秀人

最終修正:2001年6月27日



第2回CACAnet福岡フリースクールat久留米

Ruby入門(2)

 

内容

前回はRuby言語の特徴を中心に概要を説明したので、今回は実際にプログラムを作る、あるいは既に作られたプログラムを読む上で必要なプログラミング技術について説明し、後半は、サンプルプログラムを実際に見ていく。

1. Ruby基本プログラミング技術

1.1 文字列の扱いと正規表現の活用

1.2 配列とハッシュ

1.3 入出力 -- ファイル,プロセス間通信

1.4 例外処理

2. Rubyらしいプログラミングへ

2.1 手続きブロックオブジェクトの扱い

2.2 イテレータと手続きブロック

2.3 クラスとメソッドの定義の実際

2.4 クラスとモジュールの活用の実際

3. サンプルプログラムで学ぶ

 

1. Ruby基本プログラミング技術

実際のプログラミングに必要な基本プログラミング技術について要点を説明していく。

 

1.1文字列の扱いと正規表現の活用

Rubyではすべてのデータは何らかのクラスのインスタンス(オブジェクト)で、操作はほぼすべてオブジェクトのメソッドとして実現されている。文字も文字列オブジェクトであり、文字列には強力なパターンマッチ機能を適用することができる。そして、そのパターンの記述である正規表現もまたオブジェクトとして実現され、正規表現の機能(メソッド)としてパターンマッチは実現されている。

1.1.1文字列

・文字列インスタンスのプログラム上での表現

"Rubyはよいプログラミング言語\n"              # \nは改行

'Rubyはよいプログラミング言語\n'              # \nは解釈しない

"3 + 4 = #{3 + 4}\n"              # 文字列中の式を展開 "3 + 4 = 7\n"

'3 + 4 = #{3 + 4}\n"              # 式の展開は行われない

`date`              # コマンドdateを実行した結果の文字列

%Q!"Ruby"はよいプログラミング言語\n!              # ""の別形式

%|"Ruby"はよいプログラミング言語|              # Qは省略可

%q{Rubyはよいプログラミング言語}              # ' 'の別形式

%x(date)              # ` `の別形式

%w(Ruby Python Perl)              # 配列["Ruby", "Python", "Perl"]

%r|^Subject: (.*)$|              # 正規表現

・ヒヤ・ドキュメント

print <<EOS          # EOS文字までを文字列として扱う

Rubyのデータ

3 + 4 = #{3 + 4}          # 式の展開を行う

EOS

 

print <<"EOS"          # 同じ

Rubyのデータ

3 + 4 = #{3 + 4}

EOS

 

print <<'EOS'          # 式の展開は行われない

Rubyのデータ

3 + 4 = #{3 + 4}

EOS

 

print <<`EOS`          # コマンド列の実行結果

date

cal 6 2001

EOS

 

1.1.2文字列の機能

文字列はCのようなバイト列でなく、オブジェクトなので文字列操作の豊富なメソッドを持っている。

   s + x       # 文字列の連結

     s * n       # 文字列の繰り返し

     s.length     # 文字列の長さ

     s[a..b]      # a番目からb番目までの部分文字列

     s[a, n]       # a番目からn文字の部分文字列

     s.chomp      # 改行の削除 s.chmop!

     s.to_i       # 整数への変換

     s.upcase     # 大文字への変換s.upcase

 

1.1.2正規表現

・正規表現クラスのオブジェクト表現

/PATTERN/   # PATTERNを含む文字列

/^Subject: (.*)$/   # メールヘッダのSubject:行のパターン

・パターンマッチの式

正規表現 =~ 文字列 # マッチの結果の真偽が返る

・パターンマッチの結果(成功した場合)

決められた大域変数にマッチした結果が代入される

$&          マッチした部分

$1, $2,, $n          n番目の括弧に対応した部分文字列

$+          最後の括弧に当てはまる部分

$`          マッチより前の部分

$'          マッチより後の部分

$~          マッチ情報(マッチ情報を他の変数に保存、復元する)

 

例:到着メールのFrom一覧

#!/usr/bin/env ruby

mbox = open("/usr/spool/mail/" + ENV["USER"])

while line = mbox.gets

   print line if line =~ /^From/

end

 

1.1.3日本語の扱い

Rubyが扱える文字コード

EUC

SJIS

UTF-8

NONE(日本語を意識しない)

・漢字コードの指定

 $KCODEのにコード名(頭の1文字だけで判断)を入れる

・プログラム中の日本語

起動時に Kオプションで与える

#!/usr/bin/env ruby Ks

# SJISを指定

#!/usr/bin/env ruby Ke

# EUCを指定

・文字コードの変換

NKFモジュール        組み込み

Kconvモジュール

 

1.2配列とハッシュ

1.2.1 配列(Array)

※オブジェクトを順番に並べ格納するオブジェクト

表現(例)

a = ["apple", "orange", "banana"]

b = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

c = [[1, "apple"], [2, "orange"], [3, "banana"]]          # 二次元配列

 

メソッド(例)

a[i]          i番目の要素

a[i..j]          i番目からj番目までの要素

a[i, n]          i番目からn個の要素

a[i] = x          i番目にxを代入

a + b          bの配列を連結

a b          集合の差

a << x          要素xを末尾に追加

a.length          配列の大きさ

a.shift          先頭の取り出し

a.sort          ソート

a.each          各要素に対してのイテレータ

 

1.2.1 ハッシュ(Hash)

※文字列による連想配列を実現するオブジェクト

表現(例)

a = {"abc" => 1, "efg" => 2, "hij" => 3}

b = {"name" => "Hideo Sazuka", "age" => 38}

 

空のHashの生成(見つからない場合のdefault値を設定可能)

c = Hash.new([default])

 

メソッド(例)

h[x]          キーxに対応する値

h[x] = y          キーxに対応する値をyとする

h.delete(x)          キーxを削除

h.key?          キーxが存在するとき真

h.each {|x, y| }          キーxとそれに対応する値yに対するイテレータ

h.each_key {|x| }          キーxについてのイテレータ

h.keys          全キーの配列

h.values          すべての値の配列

h.invert          値からキーへの連想配列

 

1.3入出力

1.3.1組み込み関数(実はKernelモジュールのメソッド)

open

file1 = open("/path/to/read", "r")

file2 = open("/path/to/write", "w")

proc1 = open("| command to read", "r")

proc2 = open("| command to write", "r")

IOまたはそのサブクラスのオブジェクトを返す

 

標準入出力関数

print     文字列出力

printf     書式付出力

p     デバック用出力

puts     1行出力

gets     1行入力

getc     1文字入力

 

IOクラスまたはそのサブクラス

IOクラス

Fileクラス

BasicSocketクラス

Socketクラス

IPSocketクラス

TCPSocket

TCPServer

UDBSocket

UNIXSocketクラス

UNIXServerクラス

Dirクラス

 

・ネットワーククラス

Net::FTP

Net::POP3

Net::POPMail

Net::APOP

Net::SMTP

Net::HTTP

Net::Telnet

Net::CGI

 

1.4例外処理

※例外もオブジェクト(Exceptionクラス)として扱われ例外処理へと渡される

・例外処理

 

begin

   処理本体

rescue [例外クラス ] # 捕捉する例外の指定

   例外処理

end

 

例外情報は例外オブジェクトが $!変数、例外が発生した場所が$@に格納

 

・例外発生

raiseエラーメッセージ # RuntimeErrorの発生

raise例外クラス, エラーメッセージ   # 指定した例外の発生

raise例外オブジェクト # 例外の再発生

raise          # とりあえずの例外発生(rescue外)

          # 最後の例外の再発生(rescue内)

 

・後処理

begin

   処理本体

ensure

   後処理(処理本体で例外が起こり大域脱出した場合でも必ず実行する)

end

 

   例:エラーが発生しても必ずファイルはクローズした場合

 

file = open("/tmp/some_file", "w")

begin

   # なんらかの処理

   file.close

ensure

   file.close

end

 

2. Rubyらしいプログラミングへ

Rubyの特徴を活用したプログラミング技術についてみていく

 

2.1 手続きブロックオブジェクトの扱い

※Rubyでは手続きもまたオブジェクトとして扱える(Procクラス)

・オブジェクトの生成

p = Proc.new 手続きブロック

p = proc 手続きブロック          # 組み込み関数procを使った場合

・ブロックの表現

(1) { |arg1, arg2, …, argn| 手続き記述 }

(2) do |arg1, arg2, …, argn| 手続き記述end

・手続きオブジェクトの呼び出し

 

   p.call(arg1, arg2, …, argn)

・ブロックを引数にとるメソッド

def proc_call(a, b, …, &p)

p.call(a, b, …)

end

 

2.2 イテレータと手続きブロック

※イテレータは繰り返しの抽象化のために導入されたが実際の実現は手続き引数と同様である。

・イテレータの定義

 

def iterx

なんらかの処理

yield arg1, arg2, …,argn

続き

end

 

yieldを呼ぶことによって、ブロックに引数が渡され呼ばれる。手続き引数とまったく同じ動作をする。すなわち、イテレータは繰り返しの抽象化以外にも使える

 

例:ファイルの処理

イテレータによらない場合

f = open("/some_where/some_file", "r")

while line = f.gets

   print line

end

f.close

 

イテレータを利用した記述

open("/some_where/some_file", "r") do

   |f|

   while line = f.gets

       print line

   end

end    # ファイルは自動的にクローズされる

 

ブロックが提供されているかどうかは、プログラマが判断して対応しなくれはいけない。判断のメソッドとしてblock_given?が用意されている。

 

def イテレータコンパチブルメソッド

なんらかの処理

   if block_given?

       yield 結果

       後処理

   else

       return 結果

   end

end

 

2.3 クラスとメソッドの定義の実際

※クラスはClassクラスのインスタンスであり、クラスへのメソッド定義は定義メソッドの実行である

 

2.3.1クラス定義

・一般的なクラス定義

class クラス名 [ < スーパークラス ]

メソッド定義等(ここでは定義以外の実行も可)

end

・特異クラスの定義

class << オブジェクト(機能拡張予定クラスのインスタンス)

追加メソッドの定義等(オブジェクトの仮想的なクラスに対して機能追加)

end

 

2.3.2メソッドの定義

・一般的なメソッド定義

def メソッド名(引数リスト)

定義本体

end

・引数リストについて

def method(a, b, …, *z, &p)

a, b,…     引数1つ1つに対応づけられる

*z     残った引数がリストで渡される(可変引数)

&p     ブロックがProcオブジェクトとして渡される

・初期化メソッド

def initialize(引数リスト)

初期化処理

end

インスタンス(オブジェクト)が生成されるときに呼ばれる

・特異メソッド定義

def オブジェクト.メソッド名

定義本体

end

クラス定義には影響を与えず、対象のオブジェクトだけ機能追加がなされる

・メソッドのアクセス権

private          クラス定義の中でだけ呼び出し可能

protected          クラス内とサブクラス内で呼び出し可能

public          すべてから呼び出し可能

 

class SOME_CLASS

   def a   # public

     

   def b

     

   protected   # この後はprotected

   def c   # protected

     

   private # この後はprivate

   def c   # private

     

   def d

     

   def e

     

   public :d   # d, eはpublic

   private :b  # bはprivate

end

 

accessorの定義

※インスタンス変数(@がついた変数)は外部からは直接参照できないため、アクセスメソッドを記述しなくてはいけない

attr_accessor :val1, :val2, …     # 参照と代入が可能なメソッドを生成

attr_writer :val1, :val2, … # 代入だけが可能なメソッドを生成

attr_reader :val1, :val2, … # 参照だけが可能なメソッドを生成

attr          # attr_readerと同様

 

2.4 クラスとモジュールの活用の実際

※クラスはインスタンスを生成するが、モジュールはインスタンスを生成しない

※ClassクラスはModuleクラスのサブクラスである

※モジュールはメソッドや定数、変数を定義する枠組みを提供する

 

2.4.1モジュール関数

Mathモジュールのように関数的に用いるモジュールメソッドを定義する。

module M1

   def M1.f1

       関数定義

   end

   def self.f2

       関数の定義

   end

      :

end

# 特異メソッド定義になりどちらもわかりにくい

 

module M2

   def f3

      関数定義

   end

   module_function :f3

end

# module_functionメソッドでモジュール関数へ変換ができる

 

2.4.2 モジュールのincludeとextend

・クラスへのモジュール組み込み(Mix-in)

モジュールはincludeメソッドによって、クラス定義に組み込むことができる。これによって多重継承と同じような機能を実現できる

module XXX

def mmm …

end

class YYY

include XXX

:

end

 

・オブジェクトへのモジュール組み込み

クラスへのモジュール組み込みがincludeならオブジェクトへのモジュール組み込みはextendである。includeはClassクラスのprivateメソッドであるが、extendはpublicである。

module XXX

   :

end

y = YYY.new

y.extend XXX

 

class ZZZ

extend XXX     # メソッド定義はクラス定義として組み込まれる

:

end

 

3. サンプルプログラムで学ぶ

鍵発行システムの共通部分

================================================================== racommon.rb

#CACAnet 証明書発行システム

#共通クラス

# (C) Copyright 2001 CACAnet Fukuoka

# Shigeichiro Yamasaki

# create date 2001/05/03

# last update date 2001/05/05

#-------------------------------------------------

 

require 'raconf'

include CACAnet_Constants #環境定数定義ファイル

 

include File::Constants                  #ファイル排他制御定数

require 'md5'                           #MD5

 

#---------------------------------------------------

#セッション管理DB

#

#セッションID(整数)をキーに、セッション状態を管理する

#

 

class SCDB        

  require 'gdbm'

 

  def initialize(dbfile)                #データーベースファイル名

    @lock_file=File::open(CACAnet_LOCK_FILE)

    @lock_file.flock(LOCK_EX)          #排他的ロック

    @db=GDBM::new(dbfile,mode=0600)

    if (@db.key?("newsid")) then

       @newsid=@db["newsid"].to_i

    else

       @db["newsid"]=1                           #セッションIDの初期化

       @newsid=1

    end

 

  end

 

  def get_new_sid                  #新しいセッションIDを取得する

      @newsid+1

  end

 

  def set_new_sid(newsid)        #新しいセッションIDを登録する

      if newsid < @newsid then

          raise CACAnet_Input_Data_Error,"invalid new session ID"

      else

       newsid_s=newsid.to_s

       @db[newsid_s]="create_time=>#{Time::new}"

       @db["newsid"]=newsid_s

       @newsid=newsid

     end

  end

 

  def set(sid,key,value)       #セッションID、に対してキー、値を設定

      v=value.to_s                  #値は無条件にストリングにしてしまう

      id=sid.to_s                  #セッションIDもストリングにしてしまう

      if @db.key?(id) then

         s=@db[id]

          h=to_hash(s)

         rec=key+"=>"+v

         h.each_key {|k|

           rec=rec+","+k+"=>"+h[k] unless k==key

          }

#        puts rec

      else

        rec=key+"=>"+v

      end

      @db[id]=rec     

  end

 

  def get(sid,key)         #セッションIDとキーから値を取り出す

      id=sid.to_s

      s=@db[id]

      r=to_hash(s)

      r[key]

  end

 

  def set_auth_info(sid,s1,s2)          #認証情報の登録

    h=MD5::new((s1.to_s) + (s2.to_s))         #秘密情報1と秘密情報2の連接

    ai=h.hexdigest                           #ストリング表現

    set(sid,"auth_info",ai)

  end

 

  def is_valid_subject?(sid,s1,s2)         #認証情報と秘密情報の検証

      h=MD5::new((s1.to_s) + (s2.to_s))

      sec=h.hexdigest

#      puts "secr=#{sec}"

      ai=get(sid,"auth_info")

#      puts "auth=#{ai}"

      sec == ai

  end

 

  def getrec(sid)

      id=sid.to_s

      s=@db[id]

      to_hash(s)

  end

 

  def to_hash(s)         #ストリングのレコードをハッシュに変換する

      h=Hash::new

      s.split(",").each {|x|

        t=x.split("=>")

         h[t[0]]=t[1]

      }

      h

  end

 

  def close                  #データーベースのクローズと排他制御の解除

      @db.close

      @lock_file.flock(LOCK_UN)          #ロック解除

  end

end

 

#---------------------------------------------------

#アクセス管理リスト DB

#

#RA の証明書のDNをキーに,RA権限のチェックをするために使用する

 

class ACLDB

 

  def initialize(dbfile)                #データーベースファイル名

    @db=GDBM::new(dbfile,mode=0600)

  end

 

  def add_RA(dn)

    if @db.key?(dn) then              #すでにDNが登録されている

       raise CACAnet_Input_Data_Error,"The DN already exist as a RA"

    else

      @db[dn]="create_time=>#{Time::new},state_of_RA=>1"

    end

  end

 

  def remove_RA(dn)

    @db.delete(dn)

  end

 

  def is_valid_RA?(dn)

    @db[dn]

  end

 

 

  def set_RA_data(dn,key,value) #RAのDNに対してキー、値を設定

      v=value.to_s                  #値は無条件にストリングにしてしまう

      if @db.key?(dn) then

         s=@db[dn]

          h=to_hash(s)

         rec=key+"=>"+v

         h.each_key {|k|

           rec=rec+","+k+"=>"+h[k] unless k==key

          }

#        puts rec

      else

        rec=key+"=>"+v

      end

      @db[dn]=rec     

  end

 

  def get_RA_data(dn,key)      #RAのDNとキーから値を取り出す

      s=@db[dn]

      r=to_hash(s)

      r[key]

  end

 

  def is_valid_RA?(dn)                  #DN のRA は有効か?

    @db.key?(dn) && get_RA_data(dn,"state_of_RA") == "1"

  end

 

  def getrec(dn)

      s=@db[dn]

      to_hash(s)

  end

 

  def to_hash(s)         #ストリングのレコードをハッシュに変換する

      h=Hash::new

      s.split(",").each {|x|

        t=x.split("=>")

         h[t[0]]=t[1]

      }

      h

  end

 

  def close                  #データーベースのクローズ

      @db.close

  end

end

 

#---------------------------------------------------

# アクセス権限クラス

 

class Permit

 

#アクセス制御ポリシー定義テーブル

 

#ページ毎のユーザー認証方法のルール

#"RA"はRAを意味する

#"subject"はsubjectを意味する

#"any"は任意のユーザーを意味する

 

  @@rule=Hash::new

  @@rule["s010"]="RA"

  @@rule["s020"]="RA"

  @@rule["s100"]="any"

  @@rule["s110"]="subject"

  @@rule["s120"]="subject"

  @@rule["s200"]="RA"

  @@rule["s210"]="RA"

  @@rule["s300"]="any"

  @@rule["s310"]="subject"

  @@rule["s310"]="subject"

  @@rule["s400"]="subject"

  @@rule["s410"]="subject"

  @@rule["s420"]="subject"

  @@rule["s500"]="subject"

  @@rule["s510"]="subject"

  @@rule["s520"]="subject"

  @@rule["s700"]="subject"

  @@rule["s710"]="subject"

 

  def initialize(page_id)      #ページ識別子によって初期化

    @attributes=@@rule[page_id]

  end

 

  def set_secrets(sid,s1,s2)   #秘密情報の設定

    @sid=sid

    @s1=s1

    @s2=s2

  end

 

  def is_valid_client?

    case @attributes

      when "RA" then valid_RA?

      when "subject" then valid_subject?

      when "any" then true

    end

  end

 

  def valid_RA?                           #RAの認証

    dn=ENV["SSL_CLIENT_S_DN"]

    db=ACLDB::new(CACAnet_ACLDB)

    begin

      value=db.is_valid_RA?(dn)

      @role="RA"

    ensure

      db.close

    end

    value

  end

 

  def valid_subject?                  #subjectの認証

     db=SCDB::new(CACAnet_SCDB)

     begin

       value=db.is_valid_subject?(@sid,@s1,@s2)

       @role="subject"

     ensure

       db.close                           #必ずロックの解除をする!

     end

     value

  end

 

  def valid_status?(sid,scode) #セッション状態の検査

     db=SCDB::new(CACAnet_SCDB)

     begin

       s=db.get(sid,"state_code")         #状態コードの取得

     ensure

       db.close                           #必ずロックの解除をする!

     end

puts "status"

puts scode

puts s

     scode == s

  end

 

end

 

 

 

#---------------------------------------------------

# 例外クラス

 

class CACAnet_Error <  Exception

end

 

#状態異常

class CACAnet_Status_Error <  CACAnet_Error

end

 

#認証失敗

class CACAnet_Authentication_Error <  CACAnet_Error

end

 

#入出力関連エラー

class CACAnet_IO_Error <  CACAnet_Error

end

 

#入力データ異常

class CACAnet_Input_Data_Error <  CACAnet_Error

end

 

 

#

#---------------------------------------------------

# test

#=begin

puts "セッション管理DBのテスト"

 

 

#i=1

#begin

 

db=SCDB::new(CACAnet_SCDB)

sid=db.get_new_sid

#db.set_new_sid(1)

db.set_new_sid(sid)

db.set(1,"state_code","s010")

db.set(1,"last_update_time",Time::new)

 

db.set_auth_info(1,'nana','ton')

 

puts "aut=#{db.get(1,'auth_info')}"

puts db.get(1,"state_code")

puts db.get(1,"create_time")

puts db.get(1,"last_update_time")

puts "auth=#{db.is_valid_subject?(1,'nana','ton')}"

db.close

 

dn="Yamasaki Shigeichiro"

acldb=ACLDB::new(CACAnet_ACLDB)

 

acldb.remove_RA(dn)

acldb.add_RA(dn)

 

puts acldb.get_RA_data(dn,"state_of_RA")

puts acldb.get_RA_data(dn,"create_time")

puts acldb.get_RA_data(dn,"state_of_RA")

puts acldb.is_valid_RA?(dn)

puts acldb.is_valid_RA?("aaaa")

acldb.set_RA_data(dn,"state_of_RA","2")

puts acldb.is_valid_RA?(dn)

acldb.set_RA_data(dn,"state_of_RA","1")

puts acldb.is_valid_RA?(dn)

 

acldb.close

 

ENV["SSL_CLIENT_S_DN"]="Yamasaki Shigeichiro"

p=Permit::new("s010")

puts p.is_valid_client?

 

p2=Permit::new("s030")

p2.set_secrets(1,'nana','ton')

puts p2.is_valid_client?

 

p2=Permit::new("s030")

p2.set_secrets(1,'nana','noi')

puts p2.is_valid_client?

 

puts p2.valid_status?(1,"s010")

puts p2.valid_status?(1,"s030")

 

#i=i+1

#end until i==1000

 

#=end

 

=======================================================================raconf.rb

 

#CACAnet certificate issuing system

#Environment Constants

#(C) Copyright 2001 CACAnet Fukuoka

# Shigeichiro Yamasaki

# create date 2001/05/03

# last update date 2001/05/05

#-------------------------------------------------

 

module CACAnet_Constants

 

  CACAnet_HOME="/usr/local/RA/"                #

  CACAnet_HTDOCS=CACAnet_HOME+"htdocs/"                           #htdocsのパス

  CACAnet_DB=CACAnet_HOME+"db/"                #DB用ディレクトリのパス

  CACAnet_SCDB=CACAnet_DB+"scdb.gdbm"                #セッション管理DB

  CACAnet_ACLDB=CACAnet_DB+"acldb.gdbm"                #アクセス管理リストDB

  CACAnet_LOCK_FILE=CACAnet_DB+"lock_file"                #排他制御用lock file

  CACAnet_RAA_DN=""                #RAAのDN

  CACAnet_RAA_DN2=""                #代替RAAのDN

  CACAnet_SMTP_SERVER="mail.pf6.so-net.ne.jp"                #SMTPサーバー

  CACAnet_CA="ca2.cacanet.org"                #CA

  CACAnet_LDAP="ra2.cacanet.org"                #LDAP

  CACAnet_OPENSSL="/usr/local/ssl/bin/openssl"                #openssl

 

end