システムプログラム(第10週): スクリプト言語


電子・情報工学系/システム情報工学研究科CS専攻
新城 靖
<yas@is.tsukuba.ac.jp>

このページは、次の URL にあります。
http://www.coins.tsukuba.ac.jp/~syspro/2005/No10.html
あるいは、次のページから手繰っていくこともできます。
http://www.coins.tsukuba.ac.jp/~syspro/2005/
http://www.coins.tsukuba.ac.jp/~yas/

連絡

Rubyの作者、まつもとゆきひろさんによる集中講義があります。 教室が変更になりました。

スクリプト言語

コンピュータ言語の種類 スクリプト言語とは、アプリケーション本体ではなく、アプリケーションの細 かな動作を変更したり、アプリケーション本体を変更することなく機能を追加 したりするために使われる言語。

インタプリタとスクリプト

スクリプト言語は、多くの場合、インタプリタで実行される。

プロセスはインタプリタの実行形式(機械語)から作られ、インタプリタのソー ス・プログラムは、そのプロセスが読み込む単なるデータとなる。

Unix には、スクリプトを簡単に実行する仕組みとして #!がある。

インタプリタ/bin/cat

標準入力を標準出力に出力する cat コマンド 「#!」の働きを調べる。 cat は「プログラムを表示する」インタプリタである。 まず、「catインタプリタ」用のプログラムを作成する。
% cat > run-cat [←]
#!/bin/cat[←]
hello[←]
^D
% chmod +x run-cat [←]
% ls -l run-cat [←]
-rwxr-xr-x  1 yas            20 Sep 13 04:11 run-cat
% []
「catインタプリタ」用のプログラムを実行する。
% ./run-cat [←]
#!/bin/cat
hello
% []
これは次のように実行したものと同じになる。
% /bin/cat ./run-cat [←]
#!/bin/cat
hello
% []

一般のインタプリタ

「#!」の形式は次のようになる。

インタプリタの実行形式の絶対パス名: /dir/interpreter

そのソース・プログラムのファイル名: run

run の内容:

#!/dir/interpreter arg-1 arg-2 arg-3
<以下、プログラム>
このファイルに実行可能属性を付ける(chmod +x)と、 ファイル名を入力して実行することができる。
% chmod +x ./run [←]
% ./run arg-a arg-b arg-c [←]
<実行結果>
これは、次のようにインタプリタを起動したものと同じ結果になる。
% /dir/interpreter ./run 'arg-1 arg-2 arg-3' arg-a arg-b argc [←]
./run ファイルに書いた引数は、シェルから実行する時場合には、 ''で囲まれた時と同じような形式でインタプリタのプロ セスへの引数として渡される。

◆#!の解釈

#!」行は、Unix のカーネルが解釈する。 シェルが、ファイルの先頭を読み、指定されたインタプリタを起動するのでは ない。「#!」行では、シェル変数、環境変数、エイリアスはつ かえない。

スクリプト言語のプログラムでは 「#」から始まる行がコメントであると都合がよい。

awksed のように、プログラムが含まれたファイルを指定 する時に -f (program file) オプションが必要なものは、次の ように、この行に -f を付ける。

#!/bin/awk -f
{ print }

シェル

シェルはインタプリタであり、シェル・スクリプトはシェル・インタプリタの プログラムである。

シェル・スクリプトの先頭の #!/bin/sh#!/bin/csh は、 そのインタプリタを起動するという意味である。

csh

Coins での標準のログインシェルは、tcsh。

tcsh は、csh に terminal での編集機能や補完機能を付けたもの。シェル・ スクリプトを書く時には、多くのシステムで備わっている /bin/csh を使うこ とが多い。

cshスクリプトの作り方

まず端末から実行してみる。 端末から打ち込んだものを結果を、ファイルに保存する。

% gcc -I/usr/local/include/ file1.c -llib1 -llib2 -o prog [←]
(^p で1行もどす。^a で、行頭に移動して echo と打ち、^e して > run と打つ)
% echo gcc -I/usr/local/include/ file1.c -llib1 -llib2 -o prog > run [←]
% csh run [←]
% []
tcsh の機能で、^p (Control+P) で1行戻して、echo でファイルに落とす。 「|」があれば、'' でくくる。csh の引数にファイル名を与えて実行できる。

いちいち csh と打たないでもいいようにするには、chmod する。

% chmod +x run [←]
% ./run [←]
% []
#! がなければ、execve() システム・コールがエラーになる。その場合、tcsh が sh か csh (先頭が#の時)を実行する。

必要なら、エディタで「#!/bin/csh」か「#!/bin/sh」を入れる。

数行にわたるものの場合、history コマンドを使う。

% history [←]
% history | tail -5 > run [←]
% emacs run [←]

「#!/bin/csh -f」と、-f を付けた方が、~/.cshrc を読み込まないので起動 が速い。ただし、~/.cshrc での設定(aliasなど)は効かないことがある。環境 変数は、今の状態が引き継がれる(~/.cshrc を読み込ませない方が都合がよい ことが多い)。

デバッグ

-x オプションを付けて実行すると、画面にスクリプトを表示さながら実行す る。
% csh -f -x run [←]

-n オプションを付けて実行して、構文のチェックだけ行う。

シェルに1行ずつ与えて実行してみる。

シェル変数path(環境変数PATH)とrehash

~/.cshrcなどを設定して、~/bin をpath シェル変数(PATH 環境変数)に含まれるようにすることを奨める。そして自分で作成したプログ ラムやスクリプトを、~/bin に置くと./ などで実行する必 要はない。 ただし、ファイルを作成し、chmod +x した後で、1度だけ rehash コマンド を打つ必要がある。
% emacs ~/bin/newcommand [←]
% chmod +x ~/bin/newcommand [←]
% rehash [←]
% newcommand [←]
% emacs ~/bin/newcommand [←]
% newcommand [←]
% emacs ~/bin/newcommand [←]
% newcommand [←]
% []
chmod も rehash も、シェルごとに1度だけやればよい。端末をたくさん開い ていた時には、作成したスクリプトをすぐに使いたい時にはそれぞれのシェル でrehash コマンドを実行する。

rehash の意味は、ハッシュ表を作り直すことである。path にあるコマンドは、 csh は、コマンドを打つたびに探すのではなくてハッシュ表に入れてそれを検 索している。

rehash は、新しいシェルが実行される時には自動的に行われている。次にロ グインした時、chmod +x した後に開いた端末ではrehash を実行する必要はな い。

cshスクリプトでよく使われる機能

shスクリプトでよく使われる機能

シェルスクリプト作成の時のlsとgrep

ls の動き

ls コマンドは、標準出力が端末ではない(パイプやファイル)の場合、結果 を縦に並べて出力する。
% ls [←]
file1  file2  file3
% ls | cat [←]
file1
file2
file3
% []
強制的に縦に並べるオプション ls -1 や横に並べるオプション ls -C もある。

grep

grep (egrep, fgrep) は、ファイルの内容を検索し、引数で与えられたパタン が見つかった行を出力するコマンドである。次の例は、「UTMP_FILE」 または、「WTMP_FILE」という文字列を含む行を、ファイル /usr/include/utmp.h から探し、結果を表示している。
% egrep '[UW]TMP_FILE' /usr/include/utmp.h [←]
#define UTMP_FILE       "/var/adm/utmp"
#define WTMP_FILE       "/var/adm/wtmp"
% []

一般的には、次のようになる。
% egrep pattern file1 file2 ... [←]
pattern としては、次のような正規表現(regular expressions)が使える。
.
任意の1文字
.*
任意の1文字が0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
A*
文字Aが0回以上繰り返したもの
^
行の先頭
$
行の末尾
[a-m]
文字aから文字mまでの1文字
[A-Za-z0-9]
アルファベットと数字
\c
c そのもの。\. や \*, \$ などで使う。
patter1|patter2
patter1、または、patter2 (egrepのみ)
シェルで「*」と書く所は、grep では「'.*'」と書く点に注意する。 たとえば、 シェルで「*.[ch]」と書く所は、grep では「'.*\.[ch]'」と書く。

-v オプションを付けると、マッチしなかった行が表示される。

-s オプションを付けると、パタンを含むか含まないかの判定だけを行い、画 面に結果を表示しない。シェル・スクリプトの if 文の中で使う。

「*」、「$」、「[]」など、シェルが展開してしまう文字をパタンとして指定 する時には、シングル・クォート「' '」で括る。

egrep は、機能が拡張され、アルゴリズムも改良されていて速い。

RubyによるCGI

cgi-printarg.c を Ruby で書直す。
   1:	#!/usr/local3/bin/ruby
   2:	# cgi-printarg-ruby.cgi -- CGI プログラムに対する引数を表示するプログラム
   3:	# ~yas/syspro/www/cgi-printarg-ruby.cgi
   4:	# Created on 2005/06/27 01:52:36
   5:	
   6:	require "cgi"
   7:	
   8:	def main()
   9:	        $SAFE = 1
  10:	        @cgi = CGI.new()
  11:	        print_header()
  12:	        print_content()
  13:	        exit( 0 )
  14:	end
  15:	
  16:	def print_header()
  17:	        printf("Content-Type: text/html\n")
  18:	        printf("\n")
  19:	end
  20:	
  21:	def print_content()
  22:	        printf("<HTML><HEAD></HEAD><BODY><PRE>\n")
  23:	        printf("request_method: %s\n",e(@cgi.request_method))
  24:	        printf("script_name: %s\n",e(@cgi.script_name))
  25:	        printf("query_string: %s\n",e(@cgi.query_string))
  26:	        printf("CONTENT_LENGTH: %s\n",e(ENV['CONTENT_LENGTH']))
  27:	# do not call "@cgi.content_length()".
  28:	# It might remove the environmet variable, so the CGI library cannot read params.
  29:	        qh = @cgi.keys
  30:	        i = 0
  31:	        qh.each { |name|
  32:	            val = @cgi[name]
  33:	            printf("qv[%d]: %s=%s \n",i,e(name),e(val) )
  34:	            i = i + 1
  35:	        }
  36:	        printf("</PRE></BODY></HTML>\n")
  37:	end
  38:	
  39:	def e( str )
  40:	        return( str == nil ? "" : CGI::escapeHTML(str) )
  41:	end
  42:	
  43:	main()

coins の環境では、ruby は2種類ある。/usr/bin/ruby のものは、古い(1.6)。

Ruby では、require で、必要なライブラリを読み込む。 def から end までがメソッドの定義である。

main() では、CGI.new() により、CGI クラスのインスタンスを生成している。 その結果を @cgi という変数(インスタンス変数、mainの外でも使える)に保存 している。

このプログラムでは、main() という名前のメソッドを定義しているが、、 main() というメソッドから実行が開始させるわけではない。メソッド定義で はないものは、即座に実行される。このプログラムは最後に main() を呼び出 す文がある。これを忘れると何も実行されない。

$SAFE は、グローバル変数である。Ruby では、ファイルからの入力や環境変 数は汚染されたものとして扱われる。$SAFE を 1 にすると、汚染された文字 列でファイルを開くとエラーになる。標準では、0 。安全を確認したら、 obj.untaint() メソッドで汚染を解除する。明示的に obj.taint() で汚染さ せることもできる。

print_header()では、HTTP のヘッダのうち、Content-Type: 行だけを 出力している。

print_content() では、本文を出力している。

関数(メソッド) e() では、CGI ライブラリ (CGIクラス)の CGI::escapeHTML() を呼び出して、安全なものにして表示する。 たとえば、「<」は、「&lt;」と変換している。こ れで、<SCRIPT> のような危険なスクリプトが送り込まれたとし ても「&lt;SCRIPT&gt;」と表示と表示されるだけで、スクリプト は実行されない。

@cgi に保存されたCGI クラスのインスタンスの request_method() メソッド を呼び出すと、"GET" か "POST" が返される。環境変数は、ハッシュ表 ENV に対して ENV['REQUEST_METHOD'] のようにしてもアクセスできるが、環境変 数を CGI クラスで変更してしまうこともあるようである。

@cgi.keys により、パラメタの一覧が配列の形で得られる。配列の各要素につ いて(qh.each)、パラメタ名を得て表示している。この例では、どんなパラメ タでも表示しているので、このようなループになっているが、通常の CGI プ ログラムでは、@cgi['パラメタ名'] のようにして、パラメタの値を文字列と して取り出すだけでよい。

表示例:

CGI の GET メソッドを使う例

姓: 名:


その他
電子メール:

CGI の POST メソッドを使う例

姓: 名:


その他
電子メール:

実行例:

query_string: GET
script_name: /~yas/coins/syspro-2005/No10_files/cgi-printarg-ruby.cgi
query_string: lastname=arg1&firstname=arg2&sex=Male&email=arg3
CONTENT_LENGTH: 
qv[0]: firstname=arg2 
qv[1]: lastname=arg1 
qv[2]: sex=Male 
qv[3]: email=arg3 

query_string: POST
script_name: /~yas/coins/syspro-2005/No10_files/cgi-printarg-ruby.cgi
query_string: 
CONTENT_LENGTH: 50
qv[0]: firstname=arg2 
qv[1]: lastname=arg1 
qv[2]: sex=Female 
qv[3]: email=arg3 

コマンドラインからの実行

CGI クラスを使ったプログラムは、 環境変数を設定したデバッグ の他に標準入力から パラメタを与えてでデバッグすることもできる。
% echo "a=b&c=d" | ./cgi-printarg-ruby.cgi [←]
Content-Type: text/html

<HTML><HEAD></HEAD><BODY><PRE>
request_method: 
script_name: 
query_string: 
CONTENT_LENGTH: 
qv[0]: a=b 
qv[1]: c=d 
</PRE></BODY></HTML>
% []

練習問題と課題

練習問題(93) C言語のソース・プログラムだけを表示するls

与えられたディレクトリのC言語のソース・プログラムだけを表示するシェル・ スクリプトを作りなさい。
% ls-c ~/syspro/file/ [←]
fd-print.c
file-copy.c
mmap-head.c
stdio-thru.c
utmp-print.c
wtmp-last10.c
ystat.c
ystat.h
ヒント:ls の結果から、egrep で .c や .h で終わるものを抜き出す。ある いは、シェルのパタン・マッチの機能を使って、*.c や *.h だけを取り出す。

余裕があれば、-l などのオプションが付けられるようにしなさい。

練習問題(94) 「.」から始まるファイルだけを表示するls

ls コマンドや csh, tcsh, sh の * では、「.」から始まるファイルは表示 されない。逆に「.」から始まるファイルだけを表示する ls コマンドを 作りなさい。
% ls-dot ~ [←]
.
..
.cshrc
.emacs
.login
% []
ヒント:ls に -a オプションを付けると、全てのファイルを表示する。 先頭が「.」のものを抜き出す。

csh, tcsh, sh で 「.*」と指定して探す方法もある。

余裕があれば、-l などのオプションが付けられるようにしなさい。

練習問題(95) ディレクトリだけを表示するls

ディレクトリだけを表示する ls コマンドを作りなさい。
% ls-dir ~ [←]
Mail
lib
syspro
tmp
% []
ヒント:ls -l で、d から始まる行だけを抜き出す。あるいは、csh の -d や test の -d でディレクトリだけを選びだす。

できるだけ上のようにディレクトリの名前だけ表示するようにしなさい。 モードや日付は表示しないようにしなさい。

余裕があれば、-l などのオプションが付けられるようにしなさい。

練習問題(96) ファイルの大きさの順に表示するls

ls -l は、ファイルの名前の順に表示する。これを、ファイルの大きさの順に 表示するシェル・スクリプトを作りなさい。

% ls-size ~yas/syspro/window [←]
-rwxr-xr-x    1 yas      lab         17045  6月 23 00:48 gtk-button3
-rwxr-xr-x    1 yas      lab         16166  6月 23 00:00 gtk-hello
drwxr-xr-x    2 yas      lab          4096  6月 23 01:25 CVS
drwx------    2 yas      lab          4096  6月 23 01:15 tmp
-rw-r--r--    1 yas      lab          1564  6月 23 00:48 gtk-button3.c
-rwxr-xr-x    1 yas      lab           910  6月 23 01:14 gtk-button3.rb
-rw-r--r--    1 yas      lab           895  6月 23 00:05 gtk-hello.c
-rwxr-xr-x    1 yas      lab           618  6月 23 01:14 gtk-hello.rb
-rw-r--r--    1 yas      lab           200  6月 23 01:25 Makefile
合計 64
% []
ヒント:ls -l の出力を sort コマンドでソートする。 ソートは、第5フィールド(-k5)で行う。

練習問題(97) ファイルの行数の順に表示するwc

wc コマンドは、ファイルの行数、単語数、バイト数を表示する。wc コマンド は、引数で与えられた順に表示する。これを行数の順に表示するようなスクリ プトを作りなさい。
% wc-lines *.c [←]
            73           181          1166 pipe-rw-dup.c
            65           155          1015 pipe-rw-nodup.c
            57           161          1217 proc-uid-print.c
            50            98           793 setjmp-longjmp.c
            39           100           920 signal-int.c
            38           112           842 proc-create.c
            21            49           483 exec-date.c
            20            50           419 cont-1.c
            20            50           419 cont-0.c
            16            52           447 env-print.c
            15            57           460 arg-print.c
            14            24           267 fork-hello.c
            12            32           300 cont-2.c
% []
この課題では、合計は表示されなくてもよい。

ヒント:wc の出力を sort コマンドでソートする。foreach か for で1つず つ wc コマンドを実行して、全体の結果を sort するか、引数 $* で wc した 後、sort して、合計の行を削る。

練習問題(98) 使っているメモリのサイズが大きいプロセスの表示

ps -le では、全プロセス(-e)が、サイズなどの情報も含めて(-l)表示される。
% ps -le  [←]
  F S   UID   PID  PPID  C PRI NI  P    SZ:RSS      WCHAN TTY     TIME CMD
 39 S     0     0     0  0  39 RT  *     0:0     80268270 ?       0:40 sched 
 b0 S     0     1     0  0  39 20  *    98:42    8027eb80 ?       0:15 init 
119 S     0     2     0  0  39 RT  *     0:0     80267c30 ?       2:06 vhand 
119 S     0     3     0  0  39 RT  *     0:0     802678a8 ?       1:09 bdflush 
...
 b0 S     0 11771   684  0  60 20  * 11480:1622  8053853c ?      22:26 Xsgi 
 b0 S     0 22729   684  0  60 20  *   862:140   80538474 ?       0:00 xdm 
 90 Z  1231 24304 24302  0   0 -   -     - -            - -       0:00  
% []
そのうち、メモリのサイズ(SZ)が大きいプロセスを 10 個だけ表示するシェル・ スクリプトを作りなさい。
% ps-sz-top10 [←]
 b0 S     0 11771   684  0  60 20  * 11480:1622  8053853c ?      22:26 Xsgi 
 b0 S     0   548   547  0  60 20  *  2678:1619  805384e0 ?       0:30 jserver 
 b0 S  1231 24145 23879  0  60 20  *  2114:909   80538494 pts/1   0:01 emacs 
 b0 S     0 24250 23879  0  60 20  *  1166:653   80538538 pts/1   0:01 kterm 
 b0 S 40508 24517 24509  0  28 20  *  1044:766   8027f5a0 pts/3   0:00 mnews 
 b0 S     0 22729   684  0  60 20  *   862:140   80538474 ?       0:00 xdm 
 b0 S     0   684     1  0  60 20  *   854:75    80538500 ?       0:01 xdm 
 b0 S 40508 24509 24507  0  39 20  *   740:336   8027eb80 pts/3   0:01 tcsh 
 b0 S  1231 24251 24250  1  39 20  *   704:437   8027eb80 pts/2   0:01 tcsh 
 b0 S  1231 23879 23876  0  39 20  *   703:321   8027eb80 pts/1   0:01 tcsh 
% []
ヒント:SZ の順(第10フィールド)に sort して head する。

余裕があれば、RSS の順、CPU 時間の順に表示するスクリプトを作りなさい。

類似のことを実行するプログラムとして top がある。

練習問題(99) バックアップ・ファイルとのdiff

emacs (mule) は、ファイルを保存する時に、1つの前のバージョンを「~」を 付けて保存する。そのようなファイルを見つけて、オリジナルのファイルと diff コマンドで比較するようなスクリプトを作りなさい。
% diff-backup *.c [←]
% []
ヒント:foreach または for 文で、引数のファイルについて、 "$file"~ のような名前のファイルが存在するかを調べる。 存在すれば、diff コマンドで表示する。

余裕があれば、比較しているファイルの名前を表示したり、ファイルごとに停 止する、引数を取る、diff に対するオプションを取る、などの工夫をしなさ い。

練習問題(100) 小文字のファイル名への変更

Windows 系のコンピュータから Unix へファイルをコピーすると大文字のファ イル名になってしまうことがある。そのようなファイル名を全て小文字にする ようなスクリプトを書きなさい。

% mv-lower [A-Z]* [←]

ヒント:ファイル名を echo して、tr で小文字にして、それを `` でシェル変数に入れる。元の名前から小文 字の名前に mv で変える。

余裕があれば、大文字と小文字を変換することで、ファイルが上書きされる時 には警告を出したり、ユーザに問い合わせたりするようにしなさい。

練習問題(101) カウント・ダウン

秒単位でカウント・ダウンをするようなシェル・スクリプトを作りなさい。
% countdown 5 [←]
5
4
3
2
1
0
% []
ヒント:sleep 1 で、1ごとに止める。

練習問題(102) ファイルのn行目からm行目までの表示

引数として2つの数 n, m 、および、ファイル名を取り、そのファイルの n 行めから m 行目までを表示するシェル・スクリプトを作りなさい。 たとえば、次の例では、ファイルの 10 行目から 20 行目までを表示する。
% show-n-m 10 20 filename [←]
ヒント:head コマンドと tail コマンドを組み合わせて使う。 head も tail も、表示する行数を与えることができる。

余裕があれば、-n オプションを付けなさい。これは、ファイルに行番号を振 るものである。nl コマンドを使うとよい。(cat -n が使えるシステムもある。)

練習問題(103) シェル・スクリプト自由課題

その他、上であげた課題と同程度に難しい課題を自由に設定して解きなさい。

練習問題(104) CGIによる足算(Ruby)

練習問題(86) CGIによる足算を Ruby で書直しなさい。

ヒント:strtol() のように、文字列から整数値を得るには、Ruby では、 Integer() を使って次のように行う方法がある。

     s = "100"
     i = Integer(s)

練習問題(105) カレンダの表示(Ruby)

練習問題(87) カレンダの表示を Ruby で書直しなさい。
Last updated: 2005/06/27 04:18:31
Yasushi Shinjo / <yas@is.tsukuba.ac.jp>