IT pass HikiWiki - [Exp2012]シェルスクリプト Diff

  • Added parts are displayed like this.
  • Deleted parts are displayed like this.

((<"スケジュール表・各回資料 (06/01)"|[Exp2012]スケジュール表・各回資料#06-2F01>))

{{toc}}

さて, vi の使い方を大雑把に理解したところで今日のメインである シェルスクリプト について学習しましょう.

= ◎ シェルスクリプト入門

これまで見てきたように, シェルはユーザとコンピュータの橋渡しをして, ユーザがコマンドを端末から打ち込む毎に, それを解釈し実行します. 一方, [1.2]節で触れたように, 「プログラムが可能である」という, もう一つの面もあります. この, プログラムとして手続きを書き込んだファイルを「スクリプト・ファイル」と言います. スクリプト(script)とは劇の「台本」のことであり, 台本を事前に決めていてそれに沿って行わせるためにこの名前があります.

シェル以外にも多くのスクリプト言語, 例えば Perl, Ruby などがあります. もちろん全部を覚える必要は全くありませんが, 一つでも使いこなせると非常に便利です. 機械的な作業を膨大に繰り返す場合, あるいは普段頻繁に行う一連の作業は機械に作業の手順を教えて人間は楽をするのが賢い方法です. すぐにこういった膨大な作業をする必要にせまられることは無いかも知れませんが, 早いうちに慣れておくと, いざ必要になったとき楽なものです. ここでは Linux で標準シェルである Bash を使ったシェルプログラミングを学習することにします.

具体的にどのようなものか, 簡単な例を見てみましょう.

次のような一連の仕事を行うコマンドを作ることを考えます.

* 日付・時刻を表示する.
* 自分の名前を表示する.
* 自分のログインシェルを/etc/passwd から調べて変数に代入する.
* 変数の内容を表示する.
* ユーザにかけ声をかけて励ます.

それぞれの働きをするコマンドは

* date
* echo $USER
  (注: $USER はユーザ名を示す環境変数( [1.6.2]参照) )
* loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7`
  (注: 「cut コマンド」 各行から特定の部分を抽出する.
  詳しい使用法を知りたい場合は man で調べよう.)
  * (注: コマンドを括る引用符がシングルクォーテーション「'...'」や
    ダブルクォーテーション「"..."」ではなく,
    バッククォーテーション「`...`」であることには意味があります.
    詳しく知りたい場合は ((<URL:#引用符>)) を調べてみましょう).
* echo $loginshell

です.

vi を使って次のようなファイルを作成しよう.

$ vi sample.sh
       ...


[ スクリプトファイル:sample.sh ]

  date
  echo "I am $USER."
  loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7`
  echo "My login shell is $loginshell"
  echo "Hey $USER !!"


編集・保存したら,

  $ bash sample.sh

とすると, アカウント名が mym の場合次のように出力される.

  Fri May  9 06:19:57 JST 2008
  I am mym.
  My login shell is /bin/bash
  Hey mym !!

この場合, bash をわざわざ起動して, その引数としてファイル名を指定し, そのファイルを bash が解釈して実行しています.

いちいちこのようなことをするのは非効率でもあるので, 以下の手順で先程のファイル (sample.sh) を実行可能なファイルにします.

* ファイル sample.sh の先頭行に "#!/bin/bash"という おまじないを書き込む.
   
  #!/bin/bash
  date
  echo "I am $USER."
  loginshell=`grep ^$USER: /etc/passwd | cut -d: -f 7`
  echo "My login shell is $loginshell"
  echo "Hey $USER !!"


* ファイル sample.sh に実行パーミッションを加える.

  $ chmod 744 sample.sh

もしくは

  $ chmod u+x sample.sh


* ls コマンドでパーミッションを確認.

  $ ls -lF sample.sh

  -rwxr--r--    1 inex inex  204 Oct 24 05:12 sample.sh*

所有者のパーミッションのところに, 実行権限である「x」がついている ことを確認する.

こうすることでスクリプトのファイル名を入力するだけで実行することができます.
※但し, PATH には注意して下さいね.

$ ./sample.sh

この場合, 自動的に(自分が使っている login シェル)が, スクリプトファイルを bash に解釈させ実行させています.


= ○ シェルスクリプト応用

== 引数処理

引数 (ひきすう) とは, 実行時にそのシェルスクリプトに教えて上げたい追加情報であるとも言えます.

たとえば, コマンド ls は, そのままではカレント・ディレクトリ内のファイルをリストアップしますが, 引数として特定のディレクトリを指定すると, 指定されたディレクトリ内のファイルをリストアップします. これはコマンド ls に, 引数として指定したディレクトリについて知りたいんだよ, と追加情報を与えているわけです.

コマンドやシェルスクリプトに引数を指定することを, "引数を渡す"とも表現します. シェルスクリプトに渡された引数を処理したい場合は, $1, $2, ..., $9 というシェル変数を利用します.

例えば, 引数に渡された単語をそのまま表示するシェルスクリプトを考えてみましょう. 次のようなシェルスクリプト(test.sh)を作成してみてください.

  $ vi test.sh        <-- test.sh というシェルスクリプトを vi で編集する

  #!/bin/bash
  echo $1             <-- シェルスクリプト変数 $1 を表示する

  $ chmod u+x test.sh <-- test.sh に実行権限を与える

  $ ./test.sh Hello!  <-- 引数に Hello! を渡しtest.sh を実行
  Hello!              --> 引数に渡した Hello! を返す


ここでは, 引数の文字を画面に出力するコマンド echo に, シェル変数 $1 を引数として渡しています. このシェル変数 $1 には, シェルスクリプト test.sh に渡された引数 Hello! が入ります. 結果, 画面には Hello! と表示されるでしょう.

引数を参照するシェル変数は9つまで使うことが出来ます. それぞれ, $1, $2, ... $9 です.
※引数のデフォルト値などを設定するには 文字列演算子等を利用します.

== 処理の流れを制御する - 順序構造

より複雑な処理をこなす場合でも, 大抵は次の3つの処理の組み合わせで表現できます.

* 順序構造
* 選択構造
* 繰り返し構造

順序構造は, 順番にコマンドを実行するものですから,
特別の制御文は必要ありません.

選択構造には, if, case を使います.
繰り返し構造には, for, while, until を使います.


== 処理の流れを制御する - 選択構造

例として挙げているシェルスクリプトは, vi で実際に編集 (コピーアンドペースト) して実行してみてください.

=== if

書式

  if テストコマンド 1
     then コマンド 1
     [elif テストコマンド 2
      then コマンド 2]
            ...
     [else
        コマンド 3]
  fi



  #!/bin/bash
  echo a=?                 <-- 'a=' と表示
  read a                   <-- a を読み込む
  echo b=?                 <-- 'b=' と表示
  read b                   <-- b を読み込む
  if [ $a = $b ]           <-- もし, $a と $b が
    then echo same         <-- 等しければ 'same' と表示
    else echo different    <-- そうでなければ(異なっていれば) 'different' と表示
  fi

解説

if 文は if と fi で対になって, いれ子を形成しています. まず, if に続く
「テストコマンド 1 」を実行し, それが「真」 (0) を返したら then に続く
「コマンド 1 」が, 「偽」 (0 以外の値) を返したら elif に続く「テストコマ
ンド 2 」が実行されます. 「テストコマンド 2 」が「真」なら「コマンド 2 」
が, 「偽」なら 次の elif が…と続いていき, 全てのテストコマンドが「偽」
なら 最後に else に続く「コマンド 3 」が実行されます.

elif と「コマンド 2 」, else と「コマンド 3 」の部分は省略可能です. また,
「コマンド 1 」, 「コマンド 2 」…には, 複数のコマンドからなる コマンド
列を記述することも出来ます. if .... fi のいれ子は多重に 重ねることが出
来ます.

if (elif) に続くテストコマンドの文と then に続くコマンドの文は 普通改
行します. どうしても 1 行にまとめて書きたければ,

  if テストコマンド; then コマンド ; fi

のようにセミコロンを挟みます.

=== case

書式

  case 文字 in
      パターン 1)    コマンド 1  ;;
      パターン 2)    コマンド 2  ;;
        ...
        ...
  esac



  #!/bin/bash
  case $# in                   <-- 引数が
    0) echo '引数なし' ;;      <-- 0 個の場合 '引数なし' と表示
    1) echo '引数一つ' ;;      <-- 1 個の場合 '引数一つ' と表示
    2) echo '引数二つ' ;;      <-- 2 個の場合 '引数二つ' と表示
    *) echo '引数三つ以上' ;;  <-- それ以外の場合 '引数三つ以上' と表示
  esac

解説

パターンには, メタキャラクタ ?, *, [-] を使ったパターンマッチング機能
が使えます. case の右に指定される「文字」はパターン 1 から順次比較され,
一致するとそのパターンに対応するコマンド列 (括弧以降) が実行されます. コ
マンド列の終わりは 2 重のセミコロン;;で区切られます.

== 処理の流れを制御する - 繰り返し構造

=== while, until

書式

  while コマンド; do
    コマンド
  done
  


#!/bin/bash
read user                       <-- user を読み込ませる
while [ $user != $USER ]        <-- $user が $USER (ログインしているユーザ) と一致しない限り,
do                                  do と done の間に書かれたコマンドを実行し続ける
   echo 'You are not $USER!'     --> 'You are not $USER!' と表示する
   read user                     <-- 再び user を読み込ませる
done

解説

while は, while の右に書かれたコマンドが真 (0) を返す限り, do と done
の間に書かれたコマンド (列) を実行し続けます.

while の代わりに until と書くと, その右に指定されたコマンドが偽 (0 以外
の値) を返す限り, do と done の間に書かれたコマンド (列) を実行し続けます.

===for

書式

  for 変数 in 引数 ; do
    コマンド
  done



#!/bin/bash
for loop in '*.sh'      <-- 今いるディレクトリにある .sh ファイルの名前を
do                          loop に代入する
   echo $loop            --> $loop, すなわち代入した .sh ファイルの名前を表示
done

解説

for は, 与えられた引数の数だけ処理の繰り返しを行います. in の後に続く引数を変数に代入しながら do と done の間に記述されたコマンドを繰り返し実行します. in の後の引数がなくなり次第ループから抜けます.


== 処理の流れを制御する - 例

=== 引数に指定された複数のファイル名に対し, 実在する物のみファイル名を表示する

  #!/bin/bash
  while test $# -gt 0
  do
    if test -f $1; then
      echo $1
    fi
    shift
  done

※ test は, それに続く式を評価して値を返すコマンドです. 与えるオプションに応じて, 数値に関するテスト, ファイルの型に関するテスト, 文字列に関するテストを行うことが出来ます. 例えば, test -f [ファイル名]とすると, ファイルが存在すれば真, 存在しなければ偽を返します.

=== 数字の1から9を表示する

  #!/bin/bash
  number=1
  while test $number -ne 10
  do
      echo $number
      number=`expr $number + 1`
  done

※expr コマンドはその引数を1つの式として評価し, その結果を表示します. これを使うと UNIX で簡単な計算を行い, シェルスクリプトの中に算術演算処理を含めることが出来ます. なお, number の式の右辺はクオーテーション(')ではありません. 注意して下さい. (次節参照)

== 引用符

シェルスクリプトを作成する際, 引用符を用いることがあります. 引用符には以下の3種類があり, それぞれ意味が異なります(bash).

引用符
  # RT
  記号, 呼称, 意味
  '...', シングルクォーテーション, 内部の文字列をそのままの文字列として返す.
  "...", ダブルクォーテーション, 内部に含まれる変数等 、 `...`、 \ を解釈した結果の文字列を返す
  `...`, バッククォーテーション, 内部にあるコマンド(群)を実行し、 その標準出力を文字列として返す.

実際に, 使ってみて違いを確認してみましょう.

$ var=ls             <-- シェル変数 var に ls を代入

$ echo '$var'
$var                 --> '' 内の文字列をそのまま返す

$ echo "$var"
ls                   --> "" 内の変数の値を返す

$ echo `$var`
sample.sh test.sh    --> `` 内の変数をコマンドとして実行した結果を返す

より詳しい情報は man bash として bash のマニュアルを参照することで得ることも出来ます. 分からないコマンドや単語は, どんどん調べよう.
それでは ((<本日の課題|[Exp2010]シェルスクリプト課題>)) にチャレンジ!!

= 付録 [特殊記号のおさらい]

シェルスクリプト内でも, シェルで用意された特殊な記号を利用することができます. 既に習ったものを含め bash で使用される特殊な記号を以下にまとめました.

== メタキャラクタ

シェルにはファイル名が全部わからなくてもファイル名のパターンで検索できる機能があります. このファイル名とパターンの照合に用いられる特殊な記号を "メタキャラクタ"(ワイルドカード)と言います. メタキャラクタを上手に用いると, タイプする文字数を減らすことができ, 効率的な入力が行えます.

代表的なメタキャラクタ
  # RT
  メタキャラクタ, 意味, 用例, 用例の意味
  *, 任意の文字列を表す., ls /dev/h*, ディレクトリ "/dev/" 以下にある1文字目が"h"で始まるファイル全てを表示せよ.
  ?, 任意の1文字を表す. ?? は任意の2文字になる., ls -d /???, ディレクトリ"/" 以下にある3文字からなるディレクトリ全てを表示せよ.
  [ ], [ ] 内に含まれる文字にマッチする. 例えば [a-c]* は abc のいずれかで始まる任意の文字列を表す. []内の先頭に "^" を付けると "否定" の意味になる., ls -d /[a-c]* ls -d /[^a-c]*, ディレクトリ "/" 以下にある "a、 b、 c" のいずれかで始まる (始まらない) ディレクトリを表示せよ.
  { }, { }内に含まれる文字列にマッチする. 例えば test.{pl、gif、f} は test.pl test.gif test.f と入力したことになる., ls /dev/{hda、hdb}*, ディレクトリ "/dev/" 以下にある "hda、hdb" で始まるファイル全てを表示せよ.

== リダイレクションとパイプ

UNIX のコマンドは必ず標準入力と呼ばれる共通した入力の受け付け口と, 標準出力と呼ばれる共通した出力の生成, 標準エラー出力と呼ばれる共通したエラーメッセージの生成を行う機能が備えられています. 複数のコマンドの標準入出力を繋ぎ合わせたり, ファイルへ/からの出力を行うために使う記号はリダイレクションとパイプ(パイプライン)と呼ばれます.

代表的な入出力リダイレクタ
  # RT
  記号, 意味
  |, パイプと呼ばれる. program1|program2 とすると program1 の標準出力を program2 の標準入力につなぐ.
  >, program >file とすると標準出力を file につなげる.
  >>, >>file とすると標準出力を file の末尾に追加する.
  2>, program 2>file とすると標準エラー出力を file につなげる.
  <, program < file とするとfileを標準入力につなげる.
  << string, ヒア・ドキュメント. string と書かれた行が次に現れるまで、 この後に続く行を標準入力にする.

== その他

入出力リダイレクションとパイプ
  # RT
  記号, 意味
  ;, コマンドの区切り. program1;program2 とすると、 まず program1 が実行され、 次に program2 が実行される.
  &, ;と似ている. ただし program1 の終了を待たない.
  (...), ... のコマンド(群)をサブシェルのなかで実行する.
  {...}, ... のコマンド(群)をカレントシェルのなかで実行する.
  $0, 実行したシェルスクリプトファイルの名前が代入される.
  $1、 $2、 etc, シェルスクリプト実行時の引数が代入される.
  $var, シェル変数 var の値.
  ${var}, シェル変数 var の値. テキスト部分と連結されたときの混乱を防ぐために ${var} を用いる.
  \, エスケープ記号. c がシェルのメタキャラクタである場合、 \c は c の特別な意味を打ち消し、 単なる文字としての c にする.
  #, 単語の先頭に # があると、 その行に書かれた残りの内容はコメントになる.
  var=value, 変数 var への値 value の代入
  p1 && p2, p1 を実行し、 もしうまくいったら p2 を実行する.
  p1 || p2, p1 を実行し、 もしうまくいかなかったら p2 を実行する.

== 文字列演算子

基本的に文字列演算子構文では, 操作を指示する特殊な文字列を変数と右ブレース(})の間に挿入する. 演算子が引数を取る場合は演算子の右側に挿入する. 文字列操作演算子最初のグループは変数の存在を評価し, ある条件のもとでデフォルトの値を置換する.

文字列演算子
  # RT
  使い方, 意味, 目的, 例
  ${varname:-word}, varnameが存在しNULLでない場合、 その値を返す.それ以外の場合はwordを返す., 変数が定義されていない場合にデフォルトの値を返す, ${count:-0}は、 countが定義されていなければ0と評価される.
  ${varname:=word}, varnameが存在しNULLでない場合、 その値を返す. それ以外の場合はvarnameにwordを設定して返す., 変数が定義されていない場合にデフォルトの値を設定する., ${count:=0}は、 countが定義されていなければそれに0を設定する.
  ${varname:?message}, varnameが存在しNULLではない場合、 その値を返す. それ以外の場合はvarnameのあとmessageを出力し、 現在のコマンドあるいはスクリプトを中止する (対話型シェルではない場合). messageを省略するとデフォルトで "parameter null or not set"が 出力される., 変数が定義されていない場合に発生するエラーをキャッチする., ${count:?"undefined!"}は、 countが定義されて いなければ、 "count:undefined" を出力して終了する.
  ${varname:+word}, varnameが存在しNULLでない場合、 wordを返す. それ以外の場合はNULLを返す., 変数の存在を評価する., ${count:+1}は、 countが定義されていなければ 1 ("真"の意味) を返す.
  ${varname:offset} ${varname:offset:length}, サブ文字列を展開する. offsetの位置から length 文字の長さのサブ文字列を $varname の値から取り出す. 文字の位置は 0 からカウントする. lengthが省略された場合、 offsetの位置から $varname の終わりまでのサブ文字列が返される. offsetが0より小さかった場合、 開始位置は $varname の終わりからカウントされる. varname が @ の場合、 length は offset を先頭とする位置パラメータの番号になる., 文字の一部を返す (サブ文字列またはスライスという)., count が frogfootman と設定されている場合、 ${count:4} はfootmanを返し、 ${count:4:4}はfootを返す.

== 算術演算子
bash には, FORTRAN の強力な演算機能には及ばないが, ひととおりの整数演算機能が備わっている.
a=`expr <式>`
あるいは,
a=$(( <式> ))
とすると, 右辺の式を算術演算した結果が, 左辺の変数に代入される. 以下に示す四則演算子とカッコ "(" と ")" を組み合わせることで, 少なくとも有理関数の範囲で計算を行うことができる. ただし, 演算の戻り値は小数点以下切り捨てのため, 小数が計算途中にあらわれるような演算に使用することはできない.

=== 四則演算
四則演算を行う算術演算子. $a や $b には整数しか用いることができず (小数を入れるとエラーとなる), かつ除算の戻り値は整数 (小数点以下切り捨て) となることに注意.
# RT
  演算子, 意味
  $(( $a + $b )), aとbの和
  $(( $a - $b )), aとbの差
  $(( $a * $b )), aとbの積
  $(( $a / $b )), aとbの商
  $(( $a % $b )), aとbの剰余

=== 算術比較
if 文や while 文の条件式に, 大きい, 小さい, のように数値としての比較 (算術比較) を用いることができる.

代表的な算術比較演算子
# RT
  演算子, ,意味
  $a -eq $b, equal, $a と $b が等しい場合に真
  $a -ne $b, not equal, $a と $b が等しくない場合に真
  $a -gt $b, greater than, $a が $b より大きい場合に真
  $a -lt $b, lesser than, $a が $b より小さい場合に真
  $a -ge $b, gt equal, $a が $b より大きいか等しい場合に真
  $a -le $b, lt equal, $a が $b より小さいか等しい場合に真

= 参考資料

このページは
((<"北海道大学 理学院 情報実験 (INEX)"|URL:http://www.ep.sci.hokudai.ac.jp/~inex>))

((<"最低限 UNIX / Linux [III] シェルスクリプト (2007 年度資料)"|URL:http://www.ep.sci.hokudai.ac.jp/~inex/y2007/1026/jitsugi/script.html>))
を基に作成しました.

((<"スケジュール表・各回資料 (06/01)"|[Exp2012]スケジュール表・各回資料#06-2F01>))