前回は行データを分析した結果としてシンプルコマンドに行き着くのではなく、コマンドや制御演算子の組み合わせになっていることがあることを説明した。それらは「複雑なコマンド」と呼ばれ、次のような種類で構成されていることも説明した。

  • シンプルコマンド
  • パイプライン
  • リストまたは複合リスト
  • 複合コマンド
  • 関数定義


前回は、これらの複雑なコマンドの中からパイプラインについて説明した。パイプラインでは複数のコマンド(プロセス)が同時に実行され、マルチコアを有効に利用することができる。まずはマスターしておきたい機能だ。続く今回は、リストと関数定義について取り上げる。

リスト

リストはコマンドが改行、「;(セミコロン)」、「&(アンパサンド)」のいずれかで区切られた0個以上のシーケンスを指している。リスト内のコマンドは書かれた順番で実行されていく。なお、コマンドの後ろがアンパサンドになっている場合、コマンドの終了を待たずにバックグラウンドプロセスとして起動した後、すぐに次のコマンドに処理が移っていく。記述例は以下の通りだ。

コマンド1; コマンド2 & コマンド3; コマンド4; コマンド5

バックグラウンドコマンド

リストにおける区切り記号として&が登場したが、コマンドの後に&が指定されていた場合、シェルはコマンドをサブシェルで実行したのち、コマンドの終了を待たずに次の処理へ移っていく。例えば次のような書き方をした場合、コマンド1はサブシェルで実行され、そのコマンドの動作終了を待たずにコマンド2が実行される。

コマンド1 & コマンド2

バックグラウンドコマンドの終了コードは常に「0」になる。次のサンプルはそれを示すもので、終了コードが「0」のtrueコマンドも、終了コードが「1」のfalseコマンドも、&をしてバックグラウンドコマンドとして実行した場合、終了コードは「0」になっていることがわかる。これは、すぐに処理がシェルに戻ってくるため、コマンドの終了コードを待ってから処理することができないからだ。

# true & echo $?
[1] 76448
0
[1]+  Done                    true
# false & echo $?
[2] 76452
0
[1]   Done(1)                 false
#

また、バックグランドコマンドは標準入力が強制的に「/dev/null」になる。

# dd bs=1024 count=1
a
a
0+1 records in
0+1 records out
2 bytes transferred in 0.789364 secs (3 bytes/sec)
# dd bs=1024 count=1 &
[1] 76474
#

[1]+  Stopped(SIGTTIN)        dd bs=1024 count=1
#

コマンドのグループ化

関数の説明に入る前に、コマンドのグループ化について説明しておこう。そのほうが、関数が理解しやすくなるはずだ。リストは、次のように括弧で囲むことでグループ化することができる。

コマンドをグループ化するには、次の2つの記述方法がある。

■コマンドのグループ化 - サブシェルで実行

( リスト )

■コマンドのグループ化 - そのシェルで実行

{ リスト; }

()で囲った場合、コマンドはサブシェルから実行される。つまり、そのシェルとは別のシェルプロセスが起動され、そちらから実行されるわけだ。()のサブシェルには元のシェルから次のデータがコピーされる。

  • カレントディレクトリ
  • umaskで設定されるファイル作成時マスク
  • ulimitで設定されるリソース上限
  • オープン済みファイルへのディスクリプタ
  • トラップ設定
  • ジョブ
  • 位置パラメータと変数
  • シェルオプション
  • シェル関数
  • シェルエイリアス


{}で囲った場合には、新しくサブシェルを生成するのではなく、そのシェルで実行されるため、()で囲ったときよりも少しだけ処理が軽量になる。と言っても、本当にちょっとだけだ。現在のPCパワーだとほとんどわからないくらいの違いしかないだろう。{}は、主に複数のコマンドの出力をまとめたい場合などに利用できる。

最初は()と{}のグループ化は使い分けが難しいかもしれないが、次のような特徴を抑えておくと、判断しやすくなると思う。

  • ()でグループ化すると元のシェルに影響を及ぼすことがない。例えばカレントディレクトリを移動するとか、環境変数を変更するなど、元のシェルに変更が出てほしくない場合には()で囲むことで影響を遮断できる
  • {}でグループ化しても、それは元のシェルで動作しているので変更の影響はシェルに現れる。複数のコマンドの出力をまとめたいとか、そういった場合に{}を使用する


具体的なサンプルで()と{}の違いを確認していこう。まず、次のように()でリストをグループ化してみよう。

# ( sleep 12; sleep 34; sleep 45 )

この状態でプロセスの親子関係を表示させると、次のようにシェルからサブシェルが生成され、そこからさらに「sleep 12」が実行されていることを確認できる。

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  S    0:00.01 - sh
41280  3  S+   0:00.00 `-- sh
41281  3  SC+  0:00.00   `-- sleep 12
#

12秒経ってからもう一度プロセスの親子関係を表示すると、次のようにリスト内の次のコマンドが実行されることを確認できる。

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  S    0:00.01 - sh
41280  3  S+   0:00.00 `-- sh
41283  3  SC+  0:00.00   `-- sleep 34
#

34秒経ってからもう一度プロセスの親子関係を表示させると、次のようにリスト内の最後のコマンドが実行されることを確認できる。ただし、リストの最後のコマンドはサブシェルがそのコマンドに置き換わっており、今までのsh→sh→コマンド、という関係ではなく、sh→コマンド、の状態で実行されている。

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  I    0:00.01 - sh
41280  3  SC+  0:00.00 `-- sleep 45
#

今度は同じことを{}でやってみよう。まずはグループ化する。

# { sleep 12; sleep 34; sleep 45; }

同じように3回に分けてプロセスの親子関係を表示させると、今度はサブシェルではなく直接シェルから実行されていることがわかる。

■リスト内1つ目のコマンドを実行中

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  S    0:00.02 - sh
41632  3  SC+  0:00.00 `-- sleep 12
#

■リスト内2つ目のコマンドを実行中

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  I    0:00.02 - sh
41635  3  IC+  0:00.00 `-- sleep 34
#

■リスト内3つ目のコマンドを実行中

# ps -d
  PID TT  STAT    TIME COMMAND
  ...
41273  3  S    0:00.02 - sh
41697  3  SC+  0:00.00 `-- sleep 45
#

今度は元のシェルへの影響を確認してみよう。次のように()でグループ化した場合、()の中でカレントディレクトリを移動しても、元のシェルのカレントディレクトリは変化しない。

# pwd
/Users/daichi
# ( cd /; pwd )
/
# pwd
/Users/daichi
#

同じことを{}で実行すると次のようになる。{}の中でカレントディレクトリを変更すると、元のシェルのカレントディレクトリも変更される。同じシェルで実行されているためだ。

# pwd
/Users/daichi
# { cd /; pwd; }
/
# pwd
/
#

なお、()と{}では内部のコマンドの最後に「;」が必要かどうかという違いがある点にも注意しておきたい。また、{}の方は括弧の内側直後に少なくとも1つ以上の空白かタブ、または改行が必要というところにも注意が必要だ。

関数

シェルでは次のようにして関数を定義する。

関数名 () コマンド

なお、通常は次のようにコマンド部分は{}で囲ったものが使われる。つまり実質的には{}によってグループ化したものにコマンド名を与えるようなもの、それが関数ということになる。

関数名()
{
    リスト
}

関数定義そのものもコマンドのようなもので、次のように定義した段階で終了ステータスには「0」が割り当てられている。

# hello()
> {
>     echo Hello World
> }
# echo $?
0
# hello
Hello World
#

関数内部で「return」を使用すると終了ステータスを設定することができる。また、「return」を実行すると関数の処理はそこで終了し、関数を呼び出した部分に処理が戻っていく。

# hello()
> {
>     echo Hello
>     return 1
>     echo World
> }
# hello
Hello
# echo $?
1
#

関数の処理は{}でグループ化されていることからもわかるように、同じシェルで実行されている。つまり、変数の変更などの処理を行うと、その影響は変数を実行したシェルにも出る。

# a=1
# hello()
> {
>     echo $a
>     a=2
> }
# hello
1
# echo $a
2
#

{}によるグループ化では利用できず関数でのみ利用できる機能に「local」がある。これは「変数を関数内部だけで使う」という指定で、関数内で「local」を使った段階でその変数はシェルから関数内専用変数としてコピーして使われることになる。

# a=1
# hello()
> {
>     local a
>     echo $a
>     a=2
> }
# hello
1
# echo $a
1
#

ただし、すでに同名の変数があった場合、その値が初期値として使われる。「local」を指定した段階で関数内変数としてコピーしてから使われる、というように動作を理解しておくとわかりやすいと思う。

リスト、グループ化、関数

リストはすでに気にすることなく使っている方がほとんどではないかと思う。関数は、「知ってはいるが使っていない」という方もいるだろう。インタラクティブにシェルを使う場合も、シェルスクリプトとしてシェルを使う場合も、関数はかならず使わなければならない機能ではないので、使わない方も多いだろう。無理に使う必要はなく、こういった機能があるってことを覚えておいてもらえればよいと思う。

一方、グループ化はよく使う。()と{}は最初は使い分けが難しいかもしれない。ただし、これはよく使う機能なので、少しでもよいので理解を深めていってもらえればと思う。

参考資料