2018年7月19日

Bash スクリプトの基本あれこれ #1: ファイルディスクリプタとリダイレクション

Bash は Linux のシステムにおいて基本的に使うスクリプト環境であるが、癖が強い故に使いこなすにはコツがいる環境でもある。今回は自分自身の復習もかねて、 Bash スクリプトを使用する上での「キモ的なもの」をおさえる記事を書いてみる。なお、ほとんどの仕様は Sh(Bourne Shell)に基づいたものあるが、これから紹介する機能においては Bash の固有の機能も排除できないので Bash スクリプトの機能として統一して表記する。

ファイルディスクリプタ

ファイルディスクリプタ」は、 Bash のスクリプトの入出力を理解する上で覚えるべき基本的な要素だ。パイプのような強力な機能もこれを基にして実装されているので、これを理解しているか否かで Bash というスクリプトに対する理解度はまるで違うといってもよい。

「C 言語」のような「構造化プログラミング」言語では、関数は引数として特定の型を入力として受け取り、戻り値として特定の型を出力する。この考え方に基づき、語弊を恐れずに単純に言うなら、Bash で実行されるコマンドは「ファイルディスクリプタ型の入力(引数)を受け取ってファイルディスクリプタ型の出力(戻り値)を返す」と考えて良い。こういった考え方は構造化プログラミングを理解しているなら理解し易い。単純にコマンドとして使用していると概念として意識しないかもしれないが、スクリプトとして使用するとなるとこのような構造的な考え方が重要になる。

ファイルディスクリプタには POSIX の仕様に基づいて次の定義がなされている(構造化プログラミング的に言うなら、こういった型があらかじめ用意されている)。

0: 標準入力 (stdin)
1: 標準出力 (stdout)
2: 標準エラー出力 (stderr)
3 以降: 新たにファイルディスクリプタが定義されると作られる(下記リダイレクションを参照)

例えば、 ls コマンド(ディレクトリの内容を表示)を実行すると、現在のディレクトリ下の一覧が表示される。

$ ls
(ディレクトリ下の一覧が出力される)

これは ls コマンドの結果として標準出力(1)というファイルディスクリプタで値を出力することで、結果的に CUI(標準出力)に表示がなされている言える。

・パイプ

Bash の機能のひとつであるパイプは実行したコマンドの結果を次のコマンドへ渡すことが出来る Bash スクリプトを支える強力な機能だ。熟練した Linux ユーザなら当たり前のように使用している機能である。だが、 Bash のコマンドの実行の詳細を理解しないまま使用していると、パイプは「そういう結果となる」といったファジーな感覚で利用しがちである。

この機能を構造化プログラミング的な表現において定義するなら、ファイルディスクリプタ型である標準出力(1)を次のコマンドへのファイルディスクリプタ型である標準入力(0)として渡すことが出来る機能である、と言える。

例えば、 ls コマンドの実行結果を grep コマンド(マッチングする文字列を含む行の抽出)にパイプする場合、

$ ls | grep マッチングする文字列
(ls の結果を grep で抽出した結果が表示される)

と出来るが、これは ls の実行結果をファイルディスクリプタである標準出力(1)とし、 grep がそれをファイルディスクリプタである標準入力(0)として受け取ることで実行される結果となる。

「標準出力(1)を出力するコマンドを標準入力(0)を入力としてサポートするコマンドにパイプとして接続できる」と考えると、厳密な意味でパイプを使用する事が可能となる。つまり、標準入力(0)を受け取る事が出来るコマンドはパイプで接続できるコマンドであると考える事が可能となり、それに基づいてパイプ処理を構築出来る。また、標準入力(0)を入力としてサポートしないコマンドにはパイプは使用できない、と考えることも出来るようになる(ただし、こういったコマンドが何であるかは --help に書かれていない場合は経験則で実行するしかないのが現状ではある)。

・リダイレクション

リダイレクション」は、ファイルディスクリプタによる入出力処理を指定した箇所に変更する機能であり、これもファイルディスクリプタを操作する方法として、 Bash スクリプトでは覚えるべき基本的な機能となるものだ。上記した構造化プログラミングの例で言うなら、入力に使用するファイルディスクリプタ型及び出力に使用するファイルディスクリプタ型を変更できる機能である。「パイプ」も、「標準出力(1)を標準入力(0)とする」というリダイレクションの処理に基づいている。

例えば、 ls コマンドは標準として標準出力(1)に結果を表示するコマンドであるが、この標準出力の位置を別の場所に変更する事で結果を「ファイルに保存」とすることも可能である。この場合新たに「ファイル」に対するアクセスを実現するファイルディスクリプタが定義される。

$ ls > list
(このコマンドは ls の結果をカレントディレクトリ下の list ファイルに保存する。つまり、ファイルディスクリプタが標準出力(1)から list ファイルへのファイルディスクリプタ( 3 以降)に変更されている)

0~2 番のファイルディスクリプタは標準で定義されているが、新たにファイルを開くとなると新たに 3 番以降のファイルディスクリプタが定義される(この例では list ファイルに対して新たにファイルディスクリプタが定義されており、これはシステムによって自動的になされる)。

リダイレクションの処理はファイルディスクリプタの番号を指定することで詳細に指定する事が出来る。例えば、

$ command 2>&1 
(このコマンド(command)を実行すると実行結果の標準エラー出力(2)を標準出力(1)に向け、結果的にコマンドライン上の標準出力(1)に標準エラー出力(2)も表示する)

こういったリダイレクションの処理を応用すれは Bash スクリプトにおける入出力を自由自在に操れるようになる。 Bash スクリプトはこういったファイルディスクリプタの操作を行うコマンドを多数定義しているが、ここでは使用頻度が高いであろうコマンドを紹介しようと思う(詳細はこういったリダイレクションの詳細機能の説明しているサイトを参照)。

$ command > file
( command の実行結果(標準出力(1))を file に対するファイルディスクリプタを使って( 3 以降の番号が自動的に割り当てられている)file への出力として保存する。この場合はファイルに上書きはされず、新たにファイルが作られる)

$ command >> file 
( command の実行結果(標準出力(1))を file に対するファイルディスクリプタを使って( 3 以降の番号が自動的に割り当てられている)file への出力として保存する。この場合は file が存在している場合は上書きして追記する)

$ command < file
( file をあらわすファイルディスクリプタからの入力(3 以降)を標準入力(0)として読み込む。コマンドが標準入力(0)からの入力をサポートしているなら、パイプと同じ処理がなされる)

つまり、機能的に次のコマンドは同一の結果となる。

$ ls | grep 抽出する文字列
( ls を実行した結果を grep した結果を表示。 ls の結果をパイプにより grep に接続する。 ls の結果を標準出力(1)とし、それを grep の標準入力(0)として処理する。結果は grep の標準出力(1)としてコマンドライン上に表示される)

$ ls > temp
$ grep ファイル名 < temp
( ls を実行した結果を grep した結果を表示。 ls の結果(標準出力(1))を temp に対するファイルへの出力(3 以降)とする。その結果を読み込む形で temp からの標準入力(0)として grep を評価して標準出力(1)が出力され、結果がコマンドライン上に表示される)

・ヒアドキュメント

リダイレクションの操作の応用として、「ヒアドキュメント」がある。 Bash スクリプトにおいては「実行の際に独自のプロンプト上で動作し、 対話モードとして動作を停止するまでユーザの入力を待つコマンド」が存在する。そういったコマンドに対してコマンドを注入する場合にヒアドキュメントは使用できる。

$ bc << CALC
> x = 1 + 2
> x = x + 3
> x
> CALC
6
(結果として 6 が表示される。 bc コマンド(演算処理を行うインタプリタ)を実行して開始位置として定義された CALC の位置から終了位置として定義された CALC までの文字列を bc コマンドに注入して評価する)

特定の文字列をファイルに保存する場合もヒアドキュメントは利用できる。

$ cat << FILE > file
> hello world
> hello world !
> FILE

$ cat file
hello world
hello world !
( FILE ヒアドキュメントで指定した文字列が cat コマンド(ファイルの内容を標準出力とする)の標準出力(1)となり、それをファイルへの出力(3 以降)として file に保存する)

次: Bash スクリプトの基本あれこれ #2: 区切る、まとめる、無効にする

0 件のコメント:

コメントを投稿