運用環境 シェル(その7)

【連載】

にわか管理者のためのLinux運用入門

【第187回】運用環境 シェル(その7)

[2019/07/16 08:00]後藤大地 ブックマーク ブックマーク

サーバ/ストレージ

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

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


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

リスト

リストはコマンドが改行、「;(セミコロン)」、「&(アンパサンド)」のいずれかで区切られた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」を指定した段階で関数内変数としてコピーしてから使われる、というように動作を理解しておくとわかりやすいと思う。

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

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

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

参考資料

※ 本記事は掲載時点の情報であり、最新のものとは異なる場合がございます。予めご了承ください。

一覧はこちら

連載目次

もっと知りたい!こちらもオススメ

なぜ今、統合システムなのか? 押さえておくべき「3つのインパクト」

なぜ今、統合システムなのか? 押さえておくべき「3つのインパクト」

ガートナー ジャパンは10月31日~11月2日、都内で「Gartner Symposium/ITxpo 2017」を開催。11月1日には同社 主席アナリストの青山浩子氏が登壇し「CIOが理解すべき統合システムの3大インパクト」と題する講演を行った。本稿では、講演の内容をダイジェストでお届けする。

関連リンク

この記事に興味を持ったら"いいね!"を Click
Facebook で IT Search+ の人気記事をお届けします

会員登録(無料)

注目の特集/連載
[解説動画] Googleアナリティクス分析&活用講座 - Webサイト改善の正しい考え方
知りたい! カナコさん 皆で話そうAIのコト
教えてカナコさん! これならわかるAI入門
対話システムをつくろう! Python超入門
Kubernetes入門
AWSで作るクラウドネイティブアプリケーションの基本
ソフトウェア開発自動化入門
PowerShell Core入門
徹底研究! ハイブリッドクラウド
マイナビニュース スペシャルセミナー 講演レポート/当日講演資料 まとめ
セキュリティアワード特設ページ

一覧はこちら

今注目のIT用語の意味を事典でチェック!

一覧はこちら

ページの先頭に戻る