読者です 読者をやめる 読者になる 読者になる
スポンサーリンク

Linux上でシェルが実行される仕組みを,体系的に理解しよう (bash 中級者への道)

bash linux プログラミング


bash 初級者は,簡単なコマンドが並んだだけの小さなスクリプトを書くことができる。

しかしシェルの動作原理をよく理解しておらず,

一歩進んだことをやろうとするとつまずく。


シェルスクリプトの中級者になるためには,

Linux上でシェルが動作する仕組みを体系的に理解しておく必要がある。


※↑自作の もくじジェネレータ で自動生成


(1)シェルとコマンドについて

シェルがコマンドを受理する仕組みを理解する。


(1−1)シェルとは,OSに命令を出すために,OSを包んでいる外膜である。

OSは,コンピュータ上でさまざまな命令を実行する。例えば

  • ファイル操作
  • プロセス操作

など。


こういったOSの機能は,

OSの中核部分(=カーネル)の中に,特別な関数(=システムコール)として定義されている。


OSの中核部分を直接操作すると,致命的なエラーが発生しやすい。


そのため,OSの中核部分を直接操作するのではなく,

OSの外側を貝殻のように包んだ層(=シェル)を経由し,間接的にOSを操作する。


シェル上では,各種のコマンドを呼び出すことができる。

各コマンドの実体はC言語で書かれたプログラムであり,

そのプログラムの中身でOSのシステムコールが呼び出され,OS上での各種処理が実行される。


※カーネル・シェル・システムコールの関係について:

シェル
http://ja.wikipedia.org/wiki/%E3%82%B...

  • シェル(殻)という名称は、OSの機能を実装している中心核部分(カーネル)の外層として動作することからきている。


カーネル
http://ja.wikipedia.org/wiki/%E3%82%A...

  • 標準CライブラリやAPIが提供され、そこから対応するカーネル機能が呼び出される。(=システムコール)


システムコール
http://ja.wikipedia.org/wiki/%E3%82%B...

  • OSのカーネルの機能を呼び出す


ここで,「カーネルの機能を間接的に呼び出す」のがシェルなわけだが,

シェルには「CUI版シェル」と「GUI版シェル」がある。


Windowsの場合は,

  • 「エクスプローラ」は,GUIのシェルである。(GUI経由でファイル操作できる。グラフィカルシェル。)
  • 「コマンドプロンプト」は,CUIのシェルである。


Linuxの場合,

  • 「Nautilus(ノーチラス)」は,GUIのシェルである。
    • ※Nautilusとはファイルマネージャであり,GNOME(グノ−ム)の上で動作する。
    • ※GNOMEとはデスクトップ環境であり,X Window Systemの上で動作する。
    • ※X Window Systemは,LinuxにGUIを提供するツールである。
  • bashは,CUIのシェルである。


※GUIシェルについて:

GNOME
http://ja.wikipedia.org/wiki/GNOME

  • GNOMEアプリケーション
    • Nautilus - ファイルマネージャ


X Window System
http://ja.wikipedia.org/wiki/X_Window...



(1−2)Linuxログイン時には,そのユーザ用のログインシェルが起動する。

ユーザは,Linuxにログインする。

ログインが成功すると,その直後から,シェルが「面倒を見てくれる」。


つまり,ユーザとシェルとの間で対話が始まり,

ユーザはシェルに対してbashのコマンドを発行可能になる。


ユーザがログインした直後に自動的に起動され,そのユーザの面倒を見てくれるシェルのことを,ログインシェルという。

ログインシェルは,ログイン直後のユーザがコンソール上でタイピングする内容を受け付け,コマンドとして解釈し,実行してくれるのである。


ログインシェルは,ユーザごとに設定ファイル /etc/passwd 内に定義されている。


あるユーザのログインシェルをbashにしたい場合は,

そのユーザのログインシェルの欄に「/bin/bash」と記述する。


つまり,bashの実体は,Linux上の /bin/bash というパスに存在する1つのアプリケーション(バイナリファイル)である。

ログイン・シェルとは
http://itpro.nikkeibp.co.jp/article/K...

  • ログイン・シェルの設定は,ユーザー情報を登録する/etc/passwdファイルに記述
  • ユーザー名で始まる行の末尾に書かれたものがログイン・シェル


Apacheなどのミドルウェアをインストールした場合,

各ミドルウェアに対して,専用のLinuxユーザを作成することが多い。


そういった「ミドルウェア専用のユーザ」に対しては,セキュリティ対策として,

ログインシェルを無効化しておく事が多い。


ログインシェルを無効化するためには,そのユーザのログインシェルとして,無効なファイルを指定する。

そうすれば,そのユーザがログインを試みた場合には「誰も面倒を見てくれない」ので,

ログイン直後に自動的にログアウトされる。

Apacheのセキュリティ
http://imecat.web.fc2.com/apache/apac...

  • もしapacheの権限が奪取された場合、Apacheが使用している他のプロセスに影響を与える可能性がある。
  • ログインシェルを /bin/false にしてシェルの使用およびログイン禁止にしてしまう。



(1−3)ユーザが打ち込んだコマンドは,実行前に,bashによって整形される。

ユーザがタイピングした文字列は,bashが受け付ける。

bashは受け付けた文字列を直接OSに渡すのではなく,まずbash自身が文字列を「解釈」し,

特定のルールに従って「変換」した上で「実行」する。


そのため,ユーザが打ち込んだ文字列と,bashが実際に実行する文字列との間には,違いが生じる。


この「違い」(bashによるコマンド文字列の解釈方法)をちゃんと知っておけば,

bashのコーディング時に思わぬ挙動が発生して迷ったりしなくて済む。



一例として,変数の展開がある。

下記のコマンドを実行してみる。

--> echo $LANG

ja_JP.UTF-8

$LANGという環境変数には,端末上で現在使われている文字エンコード方式が格納されている。


この変数の内容は,echoコマンドが展開するのではない。

変数の内容を展開するのは,シェル(bash)である。


実際にシェルがどう変数を展開して動いているのかを確かめるために,下記のコマンドが役に立つ。

# bashによる解釈が済んだコマンド文字列を表示してから,コマンドの内容を実行する。
sh -x -c "コマンド内容"

試してみる:

sh -x -c "echo $LANG"

+echo ja_JP.UTF-8

--> ja_JP.UTF-8

ユーザは「$LANG」とタイピングしたが,

その部分はbashによる実行の直前のタイミングで「ja_JP.UTF-8」に置換されている。

その後で,echoコマンドが実行されている。(echoコマンドは変数の操作は何もしていない。)

ユーザがタイピングした文字列は,まずシェルによって解釈されてからコマンドに渡されるのである。



別の例として,スペースの処理がある。

以下のサンプルのように,ユーザがコマンド・引数・引数の間にスペースを入れても,bashによる実行時点ではそれらのスペースは「詰められて」しまう。

# 複数の半角スペースは,1文字に詰められて出力される。
echo 1 2  3   4    5

--> 1 2 3 4 5


# echoコマンドに引数が渡る時点で,既にそうなっている。
sh -x -c "echo 1 2  3   4    5"

+echo 1 2 3 4 5

--> 1 2 3 4 5

ユーザがタイピングした文字列をbashが受け取った時点で,シェルによってスペースを詰める「整形作業」が行なわれる。

その後,整形済みの文字列がコマンドおよび引数として「実行」される。


もし,シェルによる「スペース詰め」の自動整形を回避したい場合は,

文字列全体を引用符で囲う。

# 引用符で囲むと,一つの文字列としてグルーピングされる。
echo "1 2  3   4    5"

--> 1 2  3   4    5


# echoコマンドには,スペースが整形されていない文字列が渡る。
sh -x -c 'echo "1 2  3   4    5"'

+echo '1 2  3   4    5'

--> 1 2  3   4    5

このように,シェルはユーザから受け付けた文字列を自動的に「解釈」(変換)した上で実行する。



ほかには,アスタリスクの展開がある。

# 普通に実行
ls -l *

   drwxr-xr-x 2 root root 4096 514 11:48 Desktop
   -rw------- 1 root root 1614 514 06:52 anaconda-ks.cfg
   -rw-r--r-- 1 root root 44227 514 06:51 install.log
   -rw-r--r-- 1 root root 6123 514 06:49 install.log.syslog


# シェルによる解釈結果を表示させながら実行
sh -x -c "ls -l *"

+ls -l Desktop anaconda-ks.cfg install.log install.log.syslog

   drwxr-xr-x 2 root root 4096 514 11:48 Desktop
   -rw------- 1 root root 1614 514 06:52 anaconda-ks.cfg
   -rw-r--r-- 1 root root 44227 514 06:51 install.log
   -rw-r--r-- 1 root root 6123 514 06:49 install.log.syslog

シェルがアスタリスクを「カレントディレクトリ上に存在する全ファイル名の並び」に変換した上で,lsコマンドを実行していることがわかる。

lsコマンドそのものは,アスタリスクを処理していない。



この点を理解していないと,いろいろとつまずく。

例えば,findコマンドで「ファイル名が任意のパターンにマッチする」ようなファイルを再帰的に検索したい場合。

下記のようにタイプするとエラーになる(「パスは評価式の前に置かなければならない」と言われる)。

# 正しく動作しない
find . -name * 

この場合,シェルが勝手にアスタリスクを展開してしまうので,findコマンドにアスタリスクが渡らないのである。

シェルによる解釈をスキップして,コマンドにアスタリスクを渡すためには,引用符で囲う。

# 正しく動作する
find . -name "*"



(1−4)コマンドの先頭の文字列は,実行可能ファイルか,またはbashの組み込みコマンドである。

echoコマンドの実体は,echoという名前のプログラムである。

「echo」という名前のファイルがどこかに置いてあり,それを呼び出して実行しているのだ。


あるコマンドのプログラムがどこのパスに存在するのかを確かめるためには,whichコマンドを使う。

which echo

--> /bin/echo

/bin/echo はC言語でコンパイルされた実行可能ファイルであり,

シェル上でのecho呼び出しはこのファイルの呼び出しである。



シェル上で複数のワードを半角スペース区切りで入力した場合,

シェルは,最初のワードを「コマンド」とみなし,

そのコマンドに相当するプログラムがどこに存在するか探す。


その際の探索範囲は,環境変数 $PATH の中に定義されている。

PATH内には例えば /bin が含まれており,/bin フォルダ上には echo というプログラムが存在する。

それゆえ,シェル上から echo と打ち込むだけで /bin/echo が発見され,実行される。


なお,ユーザごとにPATHは異なり,下記の設定ファイルで定義されている。

/ユーザ名/.bash_proile


これらの事実をまとめて,

  • 「/binディレクトリ内にはechoというコマンドがインストールされている。」
  • 「現時点でログインしているユーザは,/binにPATHが通っている。」
  • 「echoコマンドはPATHから探索されて呼び出されている。」

のように表現する。


なお,コマンドの中にはプログラムファイルの実体が存在せず,

シェルが直接受け付けて実行してくれるものもある。

それらを,シェルの組み込みコマンド(ビルトインコマンド,内部コマンド)という。

それに対し,プログラムファイルとして実体が存在するコマンドのことを外部コマンドという。


ビルトインコマンドは,「何らかの理由で外部コマンドが全く呼び出せなくなったような危機的な状況下」で役立つことがある。

現在ログインしているシェルの組み込みコマンドだけは実行できる状況
http://www.legacyst.com/naotokun/past...

  • 組み込みコマンド exec でピンチを乗り切った


ビルトインコマンドと外部コマンド
http://78tch.blog49.fc2.com/blog-entry-41.html


なお,先頭以外のワードは,コマンドに対する引数とみなされる。


(※ただし先頭のワードが 変数名=値 の形式になっている場合は例外であり,この場合はコマンドの前に変数の代入を行なうことができる。オライリー「入門bash」3版,p80を参照。)



ここまでで,シェルがコマンドを受理する仕組みを理解した。


(2)コマンド間の連携について

コマンド・スクリプト・関数・ファイルなどが,連携しあって,互いに情報をやり取りする仕組みを理解する。


(2−1)コマンド呼び出しとは,サブプロセスの生成である。

現在実行中のプロセス一覧を調べるコマンドとして,ps -ef がある。

これを実行してみると,出力結果には「ps -ef」そのものも含まれる。


そして「PID」の列には,呼び出し元である親プロセスのIDが入っている。

コマンドの実行は,子プロセスの生成であるということがわかる。



プロセスの親子関係を確かめるためには,pstree コマンドを利用する。

このコマンドを実行すると,下記のことがわかる。

  • 全てのプロセスのおおもとの親プロセスをたどると,「init」というプロセスに行き着く。(プロセスIDは1)
  • pstreeコマンドそのものは,bashという名前のプロセスの子プロセスになっている。
    • つまり,シェルからのコマンド呼び出しは,全てbashによる子プロセスの生成である。


子プロセスの生成のために,内部的には,fork() と exec() という2つの関数が活躍している。


※子プロセスの生成について:

exec 現在実行中のシェルに代わり、指定したコマンドを実行する
http://x68000.q-e-d.net/~68user/unix/...

  • シェルからlsコマンドを実行すると、シェルは以下のことを行う。
    • システムコール fork() を呼び、子プロセスを生成する。
    • 子プロセスは ls を exec() する。
    • 親プロセスであるシェルは、ls の実行が完了するのを待つ (wait する)。


コマンドはどのように実行されるのか?
http://ether.dip.jp/linux/command.html

  • プログラムの実行単位であるプロセスはfork()によって生成する
  • プロセスから別のプロセスを実行するシステムコールがexec()
  • exec()系システムコールは現在のプロセスを新たに実行したプロセスで置き換えてしまいます。このため、プロセスからコマンド実行するためには、fork()が必要


このように,あるコマンド(またはプログラムやシェル)から,別のコマンドを呼び出すということは,プロセスの親子関係を生み出すという行為になる。


ちなみに,「プロセス間の親子関係を断ち切る」のがデーモン。

(例えば Apache Webサーバーのhttpデーモンなど)

デーモンを呼び出すと,自分の子プロセスとはならず,initの子プロセスになる。(後述する。)



(2−2)親プロセスから子プロセスに変数を渡すためには,環境変数を使う。

シェルスクリプトで利用する「変数」には,2種類ある。

  • シェル変数。シェルスクリプトの中でだけ有効。サブプロセスでは無効。
  • 環境変数。シェルスクリプトの外部でも有効。サブプロセスでも有効。


サンプル:

parent.sh

#!/bin/sh

hoge="fuga"

# 子プロセスを生成する
sh child.sh

child.sh

#!/bin/sh

# 親プロセスで定義された変数を呼び出そうとする
echo $hoge

実行結果

sh parent.sh

  # 何も出力されない
-->

このように,普通の変数(シェル変数)は,プロセスをまたいで存在することはできない

※parent.sh内でのshコマンドの呼び出しは,新規の「子プロセスを生成」することに注意。


しかし環境変数ならプロセスをまたいで存在できる。

サンプル:

parent.sh

#!/bin/sh

# 環境変数を定義する
export hoge="fuga"

# 子プロセスを生成する
sh child.sh

child.sh

#!/bin/sh

# 親プロセスで定義された変数を呼び出そうとする
echo $hoge

実行結果

sh parent.sh

# 変数がプロセスをまたいでいる
-->fuga

環境変数マニュアル
http://x68000.q-e-d.net/~68user/unix/...

  • ただし、子プロセスが設定した環境変数は親プロセスには影響を与えませんし、全く無関係のプロセス(親子の関係にないプロセス)にも反映されません。
  • 常に設定しておきたい環境変数は、.bash_profile などの中で、ログイン時に設定するといいでしょう。


なお上記の例では,親プロセスも子プロセスもシェルスクリプトだが,

子プロセスとして任意のアプリケーションを想定してよい。


例えば,PostgreSQLのユーティリティツールを呼び出す際,

事前に環境変数をセットしておけば,パスワード入力を省略してツールを呼び出せる。

これは,exportされた変数の内容を,呼び出された側の子プロセスからも参照できるためである。

PostgreSQLでスケジュールバックアップCommentsAdd Star
http://d.hatena.ne.jp/obys/20061028

  • export PGUSER PGPASSWORD



(2−3)プロセスは,シグナルをやり取りする。

「プロセス間での情報のやり取り」の方法を考えた場合,

前述のように環境変数を使うと,親プロセスから子プロセスへ情報を渡せる。


もし,プロセスの親子関係を無視し,任意のプロセスに対して情報を渡したい場合,

シグナルをやり取りするという手がある。


シグナルとは,「イベントの発生通知」のようなもの。

あるプロセスが別のプロセスからシグナルを受信することを「トラップ」という。


シグナルの送信には kill コマンド,シグナルの受信には trap コマンドを使う。

また,端末上でCtrl + Cキーを押下すれば,現在フォアグラウンドで実行中のプロセスに対して「INT(interrupt)」シグナルが送信される。


詳細は下記URLを参照。

シグナルと trap コマンド
http://shellscript.sunone.me/signal_a...

  • kill [シグナル番号|シグナル名] PID
  • trap 'コマンド' シグナルリスト


似た概念として,例えばJavaScriptには「イベントハンドラ」がある。

ブラウザ上で特定のイベントが発生したら,特定の処理を実行するようにイベントリスナを設定しておくことができる。


それと同じように,bashのシェルスクリプトも,

特定のシグナルを受信した際の「シグナルハンドラ」を記述することができる。


これは,例えばスクリプトの中止命令を受信した時に,いきなりスクリプトを終了するのではなく,
いったん終了処理(後片付け)を実行した後でスクリプトを終了するようにしたい,などの場合に役立つ。

(要するに,スクリプトの異常な終了・中途半端な終了・尻切れトンボの終了を防止することができる。)



なお,通常のシグナルがプロセスの外部でやり取りされるのに対し,

bashのプロセス内部でやり取りされる「擬似シグナル」というものもある。

これを使えば,「シェルスクリプト内で定義された関数が0以外の値を返した」というイベントをキャッチして,デバッグに役立てることができる。

(※オライリー「入門bash」3版,p241を参照。)

Bashの擬似シグナルを使ったデバッグ方法
http://d.hatena.ne.jp/dharry/20101121...

  • trap 'errtrap $LINENO' ERR

(2−4)標準入力や標準出力とは,プログラムへの入出力を抽象化・一般化したものである。

プログラミング経験者であれば,下記の言葉を目にした経験はあるだろう。

  • 標準入力
  • 標準出力
  • 標準エラー出力

それでも初級者の場合,なぜこれらの語に「標準」という接頭辞が付くのか?を理解していないだろう。


標準入力と標準出力の意味をより深く理解するためには,「標準」という語を,下記のように置き換えて考えてみると良い。

  • 「何にでも使える」入出力。
  • 「一般化された」入出力。
  • 「汎用の」入出力。
  • 「抽象的な意味での」入出力。

といった具合に。

要は,「使い方を自由に決められて,いろいろ転用できる」という意味。



標準出力は,いわば,何にでも使える出力である。

標準出力を「何に使おう」という点で特に指示がなければ,その出力内容は画面上に表示される。


(※ディスプレーが存在しない時代には,パンチカードやプリンタに文字情報が出力されていた。

しかし出力先のハードウェアの種類はどうでもよい。なぜなら,「何にでも使える」出力だから。)


標準出力を「ファイルに書き出すために使おう」と指示すれば(=リダイレクト),その出力内容はファイル内に書き込まれる。

# 引数を標準出力に出力するコマンド
echo Hello

# 特に指定がないので画面に出力される
--> Hello


# 標準出力をファイル書き込みのために使うことにする
echo Hello > hoge.txt

# ファイルに書き込まれる
--> 


リダイレクトする記号(>とか<)のことをリダイレクタという。

リダイレクタは,特定の標準入出力の入力元や出力先の切り替えを行なう。


本来,リダイレクタを使うためには,「どの標準入出力をリダイレクトしたいのか?」を明らかにするために,番号を指定しなければならない。

標準入出力の番号のリストは下記の通り。

  • 0:標準入力
  • 1:標準出力
  • 2:標準エラー出力


これらの情報を踏まえた上で,下記のサンプルを読む。

# 標準出力(1)のリダイレクト先を設定するサンプル。下記の2つは同じ意味。
echo Hello 1> hoge.txt
echo Hello  > hoge.txt


# 標準入力(0)のリダイレクト先を設定するサンプル。下記の2つは同じ意味。
read fuga 0< hoge.txt
read fuga  < hoge.txt


# 標準エラー出力(2)のリダイレクト先を設定するサンプル。
sonzai_shinai_command 2> hoge.txt

0とか1の番号抜きで,リダイレクタ記号を単体で使っている場合,

その記法には本来は標準入出力の番号が付与されるはずだけど,番号を省略して使っているのだという点を覚えておく。



リダイレクタは,上記の使用法のほかに,標準入出力をマージするためにも利用できる。

# 2(標準エラー出力)を1(標準出力)にマージする
some_command  > hoge.txt 2>&1

# 下記と同じ意味
some_command 1> hoge.txt 2>&1

こうすれば,コマンドが出力したエラー情報もログに記録することができる。

逆に,このようにして標準エラー出力(2)を1にマージしない限り,「>」だけではファイルに記録されない。


なぜなら,既に述べたとおり,「>」という記法の本来の意味は「1>」であって,

あくまで標準出力の内容だけを対象としてリダイレクトしているから。

標準エラー出力の出力内容は,そのリダイレクタからは「漏れてしまう」事になる。



理解があやふやな場合は,下記のページを熟読する。

エラー出力を標準出力にマージする
http://flex.ee.uec.ac.jp/texi/sh/node...


UNIX/基礎知識/リダイレクト、パイプ
http://technique.sonots.com/?UNIX%2F%...


入力と出力
http://shellscript.sunone.me/input_ou...


リダイレクトの仕組み
http://ether.dip.jp/linux/redirect.html

  • ファイル記述子はプロセスからファイルを開くときに一意に割り当てられる数値で、どのファイルに対してのアクセスかを特定
  • リダイレクトの機能は内部的にファイル記述子の0、1、2を操作している


ちなみに,「2>&1」というフレーズはteeコマンドと同じぐらい良く使うので,丸暗記する必要がある。

覚え方は下記の通り。

  • 「>」は「だいなり」で,「&」は「アンド」と読む。
  • なので,2つあわせて「だいあん(代案)」と読むことにする。
  • 「2>&1」は,「2の代案を1にする」という風に音読する。(2の出力先として,デフォルトの挙動の代わりに1を用いることにする)


そして,このような「標準入出力のマージ指示」は,ファイルへのリダイレクトの記述よりも後ろに書く,という点も大事。

(「command > filename 2>&1」のように,filenameへのリダイレクトよりも後ろに,マージ指示を記述する。)

どうしてそう書くのかというと,具体的な入出力先が決まった後でないと,内部的にマージ処理のしようもないから,と考えればよい。



(2−5)複数のコマンド間で標準入出力を共有し,連携するためには,パイプを用いる。

前項で取り上げたリダイレクトは,コマンドと各種入出力先を連携させるための機能であった。

ここでは,コマンドとコマンドを連携させるためのパイプ機能について述べる。


パイプ(|,バーティカルバー)とは,

  • 前のコマンドの標準出力を,後ろのコマンドの標準入力に渡すこと

である。

なんとなく「|」記号を使っている場合は,上記の定義をよく覚えておく。


パイプを使うおかげで,2つのコマンド間で情報をやり取りするための「中間ファイル」が不要になる。

標準入出力使用のすすめ / フィルタの書き方
http://www-or.amp.i.kyoto-u.ac.jp/alg...

  • 'command1 | command2'とすると、 'command1'の標準出力が'command2'の標準入力につながります。
  • 2段階で書くと 'command1 > tempfile' 'command2 < tempfile' と同等ですが、パイプした方が簡単に書けます


パイプによって接続された一連のコマンド群のことを,パイプラインという。


パイプライン中の各プロセスは,先頭から順番に生成・終了されるのではなく,

いっぺんに全部のプロセスが立ち上がる

そのため,プロセス立ち上げのための待ち時間が節約できて高速になる。

パイプ
http://ja.wikipedia.org/wiki/%E3%83%9...

  • パイプを使わないと遅い。
    • 中間ファイルをつくる場合,1 つ目のプログラムがすべてのデータを処理し終えるのを待って 2 つ目のプログラムが動く
  • パイプを使うと高速。
    • パイプを使えば、 3 つのプログラムをマルチタスクにより同時に動かし、待ち時間を有効に使うことができる。
    • データの受け渡しがファイルではなくメモリ上で行われるため、その点でも高速

http://search.luky.org/linux-users.9/...

  • UNIX の pipe() によって実現されているパイプの場合、前段の実行が完了しなくても後段の実行が始まります。
  • 「|」で多段に接続された各々のコマンドは別プロセスとして動きますので、そのいずれのプロセスも親である shell に変数を渡すことは出来ません。

パイプはどのように実現されているのか?
http://ether.dip.jp/linux/pipe.html

  • 内部的には,リダイレクトもパイプもファイル記述子の切り替えである


パイプの前段で出力された結果を、パイプの後段でシェルスクリプトで受け取りたい場合は、cat コマンドを使えばよい。

うまくやれば、「パイプからも引数からも記録できる、汎用のロギング関数」を作ることができる。

その関数のサンプル:

# パイプから受け取った標準入力か、もしくは引数文字列をログに出力します。
function write_log
{
  # ログのファイルパスを定義
  local log_filepath
  log_filepath="/tmp/hoge.log"

  # 第一引数がnullでないことをチェック
  if [ -n "$1" ]
  then
    # 引数に行番号をつけてログに記録
    echo "[${log_line_counter}] $1" >> ${log_filepath}
    # 行番号をインクリメント
    log_line_counter=$((log_line_counter + 1))
  else
    # 標準入力の内容をログに記録
    cat >> ${log_line_counter}
  fi
}
# 行番号を初期化しておく
log_line_counter=1



# 利用のサンプル

# パイプで記録できる
ls -l | write_log

# 引数で記録することもできる
write_log "fuga"

※なお,通常のパイプとは別にbashでは,

リダイレクタに丸括弧を加えることで「名前付きパイプ」という機能も実現できる。

一時ファイルを作成せずに、コマンドの実行結果のdiffを取る (bashのProcess Substitutionで)
http://d.hatena.ne.jp/hogem/20090530/...



(2−6)各コマンドの実行結果は,終了ステータスに格納される。

コマンドやスクリプトは,「終了ステータス」をやり取りすることができる。

  • コマンドから呼び出し元スクリプトに値を返したい
  • スクリプトから呼び出し元スクリプトに値を返したい
  • 関数から呼び出し元スクリプトに値を返したい

などの場合に便利。


コマンドの終了ステータスは,成功時は0,失敗時は1以上の数値。

直前のコマンドの終了ステータスは $? という変数に格納されている。

sonzai_shinai_command

-bash: sonzai_shinai_command : command not found

echo $?

--> 127


$? を使う場合,直前のコマンドしかステータスを拾えない。

下記のような場合は,パイプラインの最後のコマンドの終了ステータスが代入されてしまう。

shippai_suru_command 2>&1 | tee hoge.log

--> 失敗しました。

echo $?

# teeコマンドの終了ステータスを拾ってしまう
--> 0

パイプライン内の任意の位置のコマンドの終了ステータスを拾いたい場合,PIPESTATUS という環境変数を利用する。

この変数は配列で,添え字は 0 始まり。

shippai_suru_command 2>& 1 | tee hoge.log

--> 失敗しました。

echo ${PIPESTATUS[0]}

# パイプラインの最初のコマンドの終了ステータスを拾ってくれる
--> 1

終了ステータス
http://shellscript.sunone.me/exit_sta...


PIPESTATUS変数について
http://d.hatena.ne.jp/shibainu55/2008...

こういった終了ステータスの参照は,他の言語から外部コマンド呼び出しをした場合にも有効。

例えばRubyのサンプル:

# バッククオート記法により,OS上で外部コマンドを実行
`seikou_suru_command`

# 直前のコマンド呼び出しの終了ステータスを参照
p $?.exitstatus
  # --> 0

ここまでは,特定のコマンドの終了結果を判定したい場合。



次に,自分で定義したシェルスクリプトの終了結果を判定したい場合。

呼び出し元のスクリプトに対して自前で終了ステータスを返したい


この場合は,exitコマンドを使う。

exit時はそのスクリプトの実行が終了する。


a.sh

#!/bin/sh

exit 1

端末から

sh a.sh

echo $?

--> 1

exitで引数に渡した「1」が,呼び出し元にも伝わっている。


もしシェルスクリプトのフローの「異常系」を充実させたい場合,

下記のようなif文を大量に書くことになる。

some_command
if [ $? = 0 ] ; then
  echo "some_commandは正常に実行されました。"
else
  echo "some_commandの実行に失敗しました。"
  exit 1
fi


関数から,呼び出し元に対して終了ステータスを返すこともできる。

returnコマンドを使う。

function hoge{
  echo "hogeを実行中"
  return 1
}

hoge

echo $?

--> 1

exitとreturnの違い:

  • returnは,その関数の実行を終了する。
  • exitは,関数がどれだけネストされていても,そのスクリプト全体の実行を終了する。


ここまでで,コマンドなどの様々なもの同士が情報をやり取りする仕組みを理解した。



(3)シェルと端末について

シェルが手作業で実行される際の仕組みを理解する。

これを理解すれば,シェルが「手作業ではない状況下」で自動的に実行されるケースも理解できる。


(3−1)手作業でコマンドを打つ場合,「端末(tty)」という概念が関係する。

UNIXやLinuxは,マルチユーザ・マルチタスクのOSである。

つまり,同一マシン上に同時に複数のユーザがログインして,並行して作業できる。


Windows上などからネットワーク経由でLinux上にログインしているような場合,

puttyやteratermなどの「SSHクライアントソフト」を使うことだろう。


Linuxマシン内には,複数のクライアントがログインできるように,複数の「端末」が準備されている。

クライアントがマシンにログインすると,そのユーザに対して,マシン内の特定の端末が割り当てられる。

どのような端末が割り当てられたのか確認するためには,ttyコマンドを使う。

tty

# 特定の端末に対応するデバイスファイルのパスが返る
--> /dev/pts/1

TTYとは
http://e-words.jp/w/TTY.html

  • テレ・タイプライターの略
  • キャラクタ端末のこと


あるマシンが複数の端末によって利用されている場合,

端末間で「通信」(メッセージをやり取り)することが可能である。

下記のURLを参照。

ttyについて ttyやptsってなんぞ?
http://d.hatena.ne.jp/takuya_1st/2010...


手作業で端末上でコマンドを打ち込む場合,

それらのコマンドが「どの端末上で実行されたか」を確認できる。


ps -ef | less として,出力結果の「TTY」の列を見てみよう。

今打ち込んだばかりの「ps -ef」と「less」の各プロセスが,

「pts/1」のような端末上で実行されていることが確認できる。


端末を終了する際には,その端末にひもづいた全てのプロセスが終了される。



(3−2)デーモンは,端末に属さない。

場合によっては,特定の端末が終了しても,実行し続けたいプロセスがある。


例えば,端末でrootユーザでログインして,Apacheを起動したとする。

その後rootはログアウトするわけだが,ログアウトと同時にApacheが終了してしまっては困る。

端末からの接続が終わった後でも,そのプロセスは生き続けてほしい。


また,誰一人マシンにログインしていない状況下であっても,「sshd」は動作している必要がある。

そうでないと,誰もSSHクライアントから接続できないから。


だから,「特定の端末に依存せずに,影でずっと動作し続ける」プロセスが必要である。

こういうプロセスの事をデーモン(daemon)という。

※Windowsでは「サービス」という。


「httpd」とか「sshd」の末尾の「d」は,デーモンを指す。

つまり,プロセスとして特定のTTYに所属しておらず,起動元の端末が接続を終了しても生き続けるアプリケーションである。


前述のとおり,普通,プロセスを起動すると「子プロセス」になり親子関係が生まれる。

しかし,デーモンを起動すると,起動元プロセスの子プロセスにはならない。

おかげで,起動元プロセスが終了した後でもデーモンは動き続ける。

Linux リテラシ - 第4回 デーモン
http://rat.cis.k.hosei.ac.jp/article/...

  • デーモンとは悪魔の事を指すdemonではなくdaemon(守護神)
  • バックグラウンドで動くサービスのこと
  • 端末がないのでユーザに直接エラー通知できないため,ログが大事になる


プロセスの親子関係を含めて表示する
http://www.linuxmaster.jp/linux_skill...

  • デーモンプロセスとは,制御端末を持たないプロセスのことである。


Re: daemon コマンドの機能??
http://search.luky.org/fol.2001/msg03...

  • RedHat系ならdaemonは /etc/rc.d/init.d/functions で定義されているシェル関数であり,コマンドではない。
  • デーモンはふつう端末から切り離して、メッセージは端末がないので標準出力ではなくsyslogに出力する。
  • デーモンはpstreeでinitの直下に来る。


デーモンプロセスの構造
http://ether.dip.jp/linux/daemon.html#

  • デーモンプロセスが端末セッションに関連づいたままでは、端末を終了した際に、デーモンプロセスも終了してしまいます。
    • これは, 端末セッションに関連づいたプロセスグループ全体に終了シグナルが送信されてしまうため。


ところで,デーモンが端末に所属しないという事は,

デーモンからシェルを普通に呼び出して実行する事は可能なのだろうか?


実は,端末がないために不都合が生じるという場合がある。


例えば,Apacheが httpd ユーザ権限で起動しているとしよう。

サーバサイドのスクリプト言語から,外部プロセス呼び出しでシェルのコマンドを実行したいとする。


この場合,su コマンドなどは実行できない。Rubyのコードの例:

# Webサーバ上で,Rubyのプログラムからシェルを呼び出す
p `su - postgres -c "psql db_name < database.out" 2>&1`
  # --> 「standard in must be a tty」 のエラーになる。

このエラーメッセージを翻訳すると「標準入力は端末(tty)である必要があります」となる。

suコマンドはパスワードの入力が必要になるので,端末(TTY)が存在しない状況下では実行不可能なコマンドなのだ。

つまり,CGIとかサーバサイド言語の中からの呼び出しは不可能。

standard in must be a tty
http://x68000.q-e-d.net/~68user/cgi-b...

  • su は端末 (tty) からのパスワード入力を求めているのに、端末がないよ、という意味です。
  • 端末というのは、キーボードで入力ができて、文字が表示されるもの、たとえば kterm がそれです

この制限を回避して su コマンドをWebサーバ上から実行するためには,

あらかじめroot権限でsudoをインストールし,sudo自体もTTY無しで動くように設定しておく必要がある。

sudoが「sudo: sorry, you must have a tty to run sudo」と文句を言うときは
http://blog.cles.jp/item/2919

  • Defaults requirettyをコメントアウト

Ruby on Railsで,DBへの全接続を強制的に切断したい (Webアプリから,sudo経由で任意のコマンドを実行可能にする方法)
http://language-and-engineering.hatenablog.jp/entry/20110606/p1


なお,既に述べたとおり,httpd のログインシェルは無効なファイル( /sbin/nologinとか )にしておいて,セキュリティ対策をする。

だから,httpdでログインしようとしてもそれはできず,httpdユーザとして端末上でbashを使うことはできない


それなのに上のソースコードなどを見ると,どうしてhttpdというデーモンは普通にbashを実行できているのだろうか?


その理由は,Ruby on RailsをLinux上で走らせている場合に限って言えば,

httpdのログインシェルに関わらず,mod_railsプラグインが「$SHELL」環境変数にに「/bin/bash」を代入し,

Rubyのバッククオート記法内でbash構文を利用可能にしているようだ。


試しに,下記のコードをRails上で走らせてみればわかる。

<%= `echo $SHELL` %>

この環境変数は,現在実行中のシェルのパスを返す。

httpdのログインシェルとして何が設定されていようと,上のコードは「/bin/bash」を出力する。

だから,Ruby on Railsのソースコード中からbashのコマンドを利用できているのである。



(3−3)端末には,制御コードを出力できる。

デーモンの存在を考えればわかるとおり,

あるプロセスにとって「自分の所属する端末が存在する」というのは,恵まれた状況である。


端末が存在していれば,プロセスが出力したメッセ―ジを,即時的にユーザに直接,端末を通して表示する事ができる。


それだけではない。

端末には,文字だけでなく,制御コード(制御命令)も出力できる。例えば

  • テキスト色の変更命令
  • カーソル位置の変更命令

など。


サンプルとして,青色で文字を表示したい場合:

# HOGEと表示
echo -e "\033[1;34mHOGE\033[0m"

「\033[●●m」というのは,文字色を特定のカラーコードに設定するための制御コード。

●●の部分に「1;34」と入れれば,色は「Light Blue」が設定される。

「0」と入れれば元の色に戻る。

(元の色に戻さないと,端末上の出力結果の色が変わったままになってしまう。)


文字の色と対応するコードについては,下記ページを参照。

5. ANSI エスケープシーケンス: 色とカーソル操作
http://archive.linux.or.jp/JF/JFdocs/...


シェル - echoで文字に色をつける その1
http://www.m-bsys.com/linux/echo-color-1

  • ESCはエスケープ文字です。ESCには実際には「\e」か「\033」

こういった制御コードを活用できるのは,ひとえに,

シェルの実行媒体として「端末」が存在してくれているおかげである。



ここまでで,手作業で端末上でコマンド操作する場合の動作原理を理解した。


復習の質問

  • (1)ユーザによるコマンドを,シェルが解釈して実行する流れを説明しなさい。シェルは,どのような3つの物を加工するか。
  • (2)実行コマンドの実体がプロセスである,という点を説明しなさい。プロセスは,どのような4つの手段で互いに情報をやり取りするか。各々の特徴は何か。
  • (3)実行コマンドがttyに依存する場合と,依存しない場合の違いを説明しなさい。


結び

以上で,Linux上でbashのコマンドが動作する仕組みを体系的に理解することができた。


細かいプログラミングテクニックについては触れてなかったが,

こういった背景の骨組みとなる知識は,トラブルシュートなどで大いに役立つ。



関連する記事:

ユーザ配布用のbashシェルを作成するための 17 のコマンド
http://language-and-engineering.hatenablog.jp/entry/20101028/p1


bashでcronジョブを自動登録する (バッチでcrontabを編集)
http://language-and-engineering.hatenablog.jp/entry/20101210/p1


コマンドラインからプロセスを起動・終了する方法 (環境変数とレジストリについて)
http://language-and-engineering.hatenablog.jp/entry/20081028/1225160338