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