01 月 15 日(月)

シェルスクリプト

今回も bash を活用し, コンピュータを更に効果的に操作するための技を修得しよう.

作業の前に,bash のマニュアルを開いておこう:


変数置換

文字列の連結

シェルの変数は,基本的にはすべて文字列として取り扱われる. 確かめてみよう:

$  var=100

$  echo $var + 100
100 + 100		# 数値 200 ではなく,文字列 "100 + 100"

$  echo $var"100"	# 文字列 "100100" になる
$  echo ${var}100	#   同上
$  echo $var"$var"	#   同上

$  echo $var100		# var100 という名前の変数になってしまう
			#   この変数は未定義なので,空文字列

$  echo $var'$var'	# 文字列 "100$var" になる
$  echo ${var}\$var	#   同上
$  echo $var\$var	#   同上

なお,特殊記号 $ { } " ' \ については, 前回理解したハズだ.

算術式(数値計算)

bash のデータ型は基本的には文字列なのだが, 数式の文字列を数値として計算することもできる.

$  echo $(( 1 + 2 ))	# たし算 1 + 2 の計算結果を表示
3

たし算 + の他にどんな演算子が利用できるか? bash マニュアル の「算術式展開」および「算術式評価」 の項目を参照せよ.

配列(ワードリスト)

ワードリスト(単語の羅列)を代入すれば, シェル変数を配列のように使うことも可能だ:

$  list=( scheme C java )	# 配列として代入
$  echo ${list[0]}	# 要素0
$  echo ${list[1]}	#  〃 1
$  echo ${list[2]}	#  〃 2

$  echo ${list[*]}	# すべての要素(ワードリスト)
$  echo ${list[@]}	#     〃

$  echo ${list}		# 何かな?(ありがちなまちがい)
$  echo $list		#     〃
$  echo $list[*]		#     〃

変数と配列のサイズを調べるには:

$  echo ${#list[0]}	# 要素0 の文字数
$  echo ${#list[1]}	#   〃1 の〃
$  echo ${#list[2]}	#   〃2 の〃
$  echo ${#list[*]}	# 配列の要素数

配列の添字を省略すると,どうなるか?:(ありがちなまちがい

$  echo ${list}		# 変数 list ...だが実体は配列要素 list[0]
$  echo ${#list}

これで,「変数とは要素数 1 の配列である」ってことも推理できるハズだ.

配列を削除するには:

$  unset list[1]	# 配列要素の削除
$  echo ${list[*]}	# ↑ の確認

$  unset list		# 配列全体の削除
$  echo ${list[*]}	# ↑ の確認

以下,配列についてのありがちな間違いと混乱の更なる例を紹介しておく:

$  pig=boo foo woo	# エラー.前回も紹介.

$  pig="boo foo woo"	# これは要素数1の配列(3ではない!)
$  echo ${pig[0]}	# ↑ の確認
$  echo ${pig[1]}	#   〃
$  echo ${pig[2]}	#   〃
$  echo ${#pig[*]}	#   〃

$  breakfast=( "an apple" "a slice of bread" "a cup of coffee" )	# これは3要素の配列
$  echo ${breakfast[0]}		# ↑ の確認
$  echo ${breakfast[1]}		#   〃
$  echo ${breakfast[2]}		#   〃
$  echo ${#breakfast[*]}	#   〃

とにかく混乱しやすいので,充分理解できるまでは, 何か操作したら,必ず結果を確認しよう.


コマンド置換

コマンドの実行結果をワードリストとして再利用できる:

$  file=(`\ls /bin`)	# コマンド \ls /bin の実行結果が変数 file に代入される
$  echo ${#file[*]}
$  echo ${file[0]}
$  echo ${file[1]}
$ ...
コマンドの実行結果を目で見て,手で打ち直す, なんて無駄な作業とは,これでおさらばだ.

ここで,記号 ` はバッククォートだ, シングルクォート ' ではない. また,\ls\ は,前回説明の通り, ls のエイリアス置換を抑止するための指定だ.


ファイル名置換(パス名展開)

グロブパターンと呼ばれる特殊な文字列は, 指定されたパターンにマッチするファイル名の集合を ワードリストに変換してくれる... 要するに,これを利用すると, 複数のファイルに対する処理を一度にまとめて記述できる. とても便利な機能だ.

次の一連のコマンドについては, 適当なディレクトリ(たとえば,C言語のディレクトリ)へ移動してから, 試してみよう:

$  ls
Makefile  ReadMe.txt  boo.c  foo.c  woo.h

$  ls *.c	# .c で終わるファイル名
boo.c  foo.c

$  ls *.???	# 3 文字の拡張子をもつファイル名
ReadMe.txt

$  echo [A-Z]*		# 大文字で始まるファイル名
Makefile  ReadMe.txt

$  file=(*.c)		# file=( boo.c foo.c ) に置換される
$  echo ${#file[*]}
$  echo ${file[0]}
$  echo ${file[1]}

特殊パターン文字の説明については, bash マニュアルの「パターンマッチング」 の項目を参照せよ.

パス名置換を活用すれば, 複数のファイル名のすべてを明示する必要がなくなるので, 作業効率が向上する. ただし,ファイルの命名規則がお粗末だと,うまく利用できない. 今後,自作のファイル名については, パターンマッチを活用しやすくできるように, 規則を意識して行こう.

練習問題


シェルスクリプト

シェルコマンドはプログラミング言語としても使える. 既存のコマンドを組み合わせて,新しいコマンドを作り出せる.

まず,List 1 のようなスクリプトファイルを作ってみよう. これは,簡単な入出力の例だ.

List 1. シェルスクリプト hello1.bash
#!/bin/bash
# ↑ 1行目は特別なコメント行なので省略不可,他のコメントは省略可
# あいさつスクリプト 標準入力版

echo -n "Enter your name > "		# echo -n:改行せずに表示
read name				# 標準入力を変数 name に代入
echo					# 改行を表示

echo "Hello, ${name}."
echo "I am ${BASH}."						
					# 末尾に必ず空行			
C言語と比較すると, echo は puts() や printf() に, read は gets() や scanf() に, それぞれ相当する.

記号 # はコメントの開始であり,行末までは無視される. (削除してよい.)

ただし,1 行目のコメントだけは, このスクリプトを実行するために必要なものだ.削除・変更しないこと.) このスクリプトがシェルプログラム /bin/bash によって 解釈・実行されることを指定している.

そして,スクリプトの最後の行では必ず改行すること. 改行しないと,最後のコマンドが実行されない場合がある.

スクリプトを実行するには,まず, ファイルに実行可能な属性を与えておく必要がある:

$  chmod  +x  hello1.bash

では,実行しよう:

$  ./hello1.bash
Enter your name > Kitty
Hello, Kitty.
I am /bin/bash.

次は,コマンドライン引数を使ってみよう. コマンドライン引数の値は,位置パラメータ $0$1,…,$9 として利用できる. List 1 を改造して List 2 のスクリプトを作成しよう.

List 2. シェルスクリプト hello2.bash
#!/bin/bash
# あいさつスクリプト コマンドライン引数版

name=$1			# コマンドライン引数を name に代入	

echo "Hello, ${name}."
echo "I am ${BASH}."							
										
bash の変数 ${数字} は,C言語の argv[数字] に相当する.

では,実行属性を与えてから,実行してみよう. もちろん,実行時には引数を与えること:

...

$  ./hello2.bash  Kitty
Hello, Kitty.
I am /bin/bash.
なお,bash のプログラミングでは, List 1 や 2 のように個別にスクリプトファイルを作る必要はない. 前回のように関数(function)として登録するだけでも実現可能だ.

制御構造

bash には, C言語と同様な制御構造 ifforwhile がある. ただし,Cとは違うところもあるので注意が必要だ. マニュアルの「シェルの組み込みコマンド」,「条件式」 等を参照せよ.

選択構造,条件式

List 2 では,引数なしで実行した場合の動作が変だ. コマンドライン引数 $1 を使う前に, 次のエラーチェック処理を追加しよう:

...

if [ $# -lt 1 ]			# if ( argc < 1 )
then				# {
	echo "引数が必要です"
	exit 1
fi				# }
...

条件式に使われる論理演算子は,Cとはかなり異なる. マニュアルの「条件式」 に説明がある. (ちなみに,-lt は“less than”,つまり, 不等号「<」を意味する.)

その他の条件式についても試してみよう. List 3 を作成しよう.

List 3. シェルスクリプト file.bash
#!/bin/bash
# ファイルの種類の判定

...			# コマンドライン引数のチェック

file=$1
echo -n "$file は"
if [ -f $file ]; then  echo "ファイルです."; fi
if [ -d $file ]; then  echo "ディレクトリです."; fi
if [ ! -e $file ]; then  echo "存在しません."; fi

exit 0

実行例:

$  ./file.bash  file.bash
file.bash はファイルです.

$  ./file.bash  /usr/bin
/usr/bin はディレクトリです.

$  ./file.bash  notexist.txt
notexist.txt は存在しません.

ワードリストに対する反復

ワードリスト中の各要素について反復処理するために, 非常に便利な制御構造 for-in文がある. マニュアルの「シェルの組み込みコマンド」 を参照せよ.

定数列に対して繰り返す例:

$  sum=0
$  for i in 10 20 30 40		# i = 10, 20, 30, 40 について繰り返す
>  do				# {
>  	sum=$(( $sum + $i ))	#	sum = sum + i
>  done				# }
$  echo $sum			# 10 + 20 + 30 + 40 の計算結果

複数のファイルに対して繰り返す例:

$  for file in *
>  do
>  	echo -n "$file は "
>  	if [ -f $file ]; then echo "ファイルです"; fi
>  	if [ -d $file ]; then echo "ディレクトリです"; fi
>  done

List 4 は,複数のファイルに対して, バックアップコピーを作成するためのシェルスクリプトの例だ. コピーのファイル名には,元のファイル名に拡張子 .org が追加される.

List 4. シェルスクリプト backup.bash
#!/bin/bash
# 引数に指定されたファイルのバックアップコピーを作る

for file in $*
do
# if [ ! -e $file ]; then continue; fi	# 存在しないファイルを無視

	echo "$file -> $file.org"
	\cp $file $file.org
done								

なお,$* はコマンドライン引数の羅列 $1 $2 $3 ... に展開される.

このスクリプトの使い方:

$  ls  *.c
boo.c  foo.c

$  ./backup.bash  *.c
boo.c -> boo.c.org
foo.c -> foo.c.org

$  ./backup.bash  woo.c		# 存在しないファイルの場合
cp: cannot stat `poo.c': そのようなファイルやディレクトリはありません
		# エラー

# if 文を有効化(行頭の「#」を削除)すると...
$  ./backup.bash  woo.c
		# エラーを回避

$  ls  *.c*
boo.c  boo.c.org  foo.c  foo.c.org

練習問題

複数のテキストファイルを連結するためのシェルスクリプト mycat.bash を作成せよ. ただし,各ファイルの先頭に区切り文字列(ファイル名と記号列)を挿入すること. また,存在しないファイルが指定された場合,無視すること.

$  ls *.txt			# 適当にテキストファイルを用意しておくこと
dessert.txt  drink.txt  fluit.txt

$  cat dessert.txt
icecream
pie
pudding

$  cat drink.txt
cocoa
coffee
tea

$  cat fluit.txt
apple
banana
grape

$ ./mycat.bash  dummy.txt drink.txt fluit.txt notexist.txt dessert.txt
********************
  drink.txt
********************
cocoa
coffee
tea

********************
  fluit.txt
********************
apple
banana
grape

********************
  dessert.txt
********************
icecream
pie
pudding

内部コマンド for-in と 外部コマンド cat を利用すれば簡単.

問題の意味がわからなければ, 通常の cat コマンドによる連結結果と比較してみよう.


本日の課題

ゲーム作品の相互評価を実施します.
(c) 2018, yanagawa@kushiro-ct.ac.jp