スクリプトやプログラムが使える入力・出力

Ippei Kishida

Last-modified:2016/11/08 07:20:09.

『UNIX今日の技』での記事をまとめたものです。

『入力→(システム)→出力』

コマンドは、入力から出力を生成するシステムに相当する部分だと言えます。 それはユーザが自分で作ったスクリプトについても同様です。 ここでは「スクリプトやプログラムが利用できる入出力にはどのようなものがあるのか」ということについてまとめていきます。 よりよいスクリプトやプログラムを作る為には、これらを整理して理解しておくことが必要だと思います。

「シェルからコマンドを起動する」という UNIX の一般的なスタイルにおいてプログラムがどのように動いているのかということを、(Perl や Ruby、C といった)特定の言語に依存しない部分について書いていきたいと思います。

1 出力

処理の流れとは逆になりますが、目的をはっきりさせた方が分かり易いと思いますので、先に出力の説明をします。

1.1 標準出力

画面への表示です。 ユーザはそれをリダイレクトしてファイルに落とすこともできます。

標準出力への出力は、「原理的にシステムを破壊することがない」というのが魅力です。 後述する「ファイル出力」や「コマンドの実行」では何らかのミスやトラブルでファイルやシステムを破壊することがないとは言い切れません。

プログラム内部でファイルに書き出す仕様にするよりも、標準出力に出力するようにしておいてユーザが必要な時にリダイレクトでファイルに落とすようにしておいた方がラクチンで安全な場合も多いです。

:例:取得した情報を表示、実際に行った(行う予定の)処理を表示

1.2 ファイル出力

プログラムを実行した結果をファイルに記録します。 複数のファイルに対して出力したい場合にはスクリプト内で明示的にファイルを扱う必要があります。

また、ファイルの修正・加工を行うプログラムを作った場合に、標準出力からリダイレクトという方針を取ると、 command some.txt > temp.txt; mv temp.txt some.txt としなければならず、一手間増えます。 ここでうっかり手順を省略してしまって command some.txt > some.file と打ってしまうとファイルが破壊されてしまいます。 (参照:UNIX今日の技/シェル → ストリームという概念(パイプとリダイレクト))

ユーザの負担軽減とミスの防止(フールプルーフ)のために読み込みと書き込みをスクリプトの中でやることは十分に意義のあることだと思います。

:例:ファイルの加工スクリプト, あるいはさらに一度に複数のファイルを変更できるようにする

1.3 コマンドをシェルに投げる

画面でもファイルでもなく、システム自体に投げる……スクリプトからシェルにコマンドラインを投げることもよくあります。 すなわち、スクリプトはコマンドラインの文字列を整形している、ということです。

尤も細かく見ると、そのコマンド自体の作用が目的の場合と、コマンドから返って来るストリーム(文字列)が目的の場合とに分類できます。 が、後者の場合は返って来るストリームを新しい入力として扱うというだけで、「プログラムからシステムへの出力」という観点からは本質的な違いはありません。

UNIX のコマンドは実に高機能な物が多く、そのインターフェイスを整えるためだけにスクリプトを書くということを私はよくやります。

:例:scp や rsync でリモートホストとファイルのコピーや同期を行う, VASP の計算を走らせる

1.4 システムの変更

「ファイルの名前を変更する」なども立派な出力です。

「コマンドをシェルに投げる」に近いのですが、プログラム言語の側で用意されている物はシェルを通さなくても実行できます。 例えばファイルのリネーム・削除、ディレクトリを作成・削除など。 高級なことはあまりできませんが。

シェルによる文字列展開はスクリプトから扱うには不便なことが多いので、言語側で用意されているものはこちらを使った方が良いと思います。

例えば、「’」「“」などが含まれているファイルをシェル経由で扱おうとするならば「シェルで意味を持つ文字」を、例えば「’」→「'」のようにエスケープしてやる必要があります。 そしてその、「エスケープする必要のある文字」はシェルによって異なります。 則ち「zsh を使っている人では問題なく動くけれど、tcsh を使っている人では動かない」という状況が発生し得るということです。 プログラム言語側で用意されている方法を使えば大抵はこのような文字列展開は気にしなくても済みます。

1.5 親プロセス(シェル)への戻り値

「コマンドから別のコマンドへ投げる」という構図は、そのまま「シェルからコマンドを投げる」という構図と完全に一致します。 すなわち、あなたが自作するコマンドも、他のコマンドから利用する可能性があるならば然るべき戻り値を返すようにしてやるのも良いかもしれません。

これはある意味、親プロセス(シェル)への出力、ということができます。

2 入力

2.1 標準入力

標準入力は通常、キーボードからの入力に割り当てられます。 [y/n] を尋ねるなどユーザに判断させたり、「英単語の意味を入力」など一回性の高い入力などで使えます。

またパイプから流れ込むストリームは、プログラムからは標準入力として扱われます。 フィルタプログラムを作りたいならば、標準入力から得た文字列を加工して標準出力に出す仕組みにしてやれば良い、ということになります。

キーボードからの入力を要求されるプログラムでも、「毎回キーボードから打ち込むのが面倒」という場合には一度ファイルに保存しておいて、 command < input.txt あるいは cat input.txt | command のようにしてやれば何度でも簡単に再現できます。

2.2 ファイルからの入力

ファイルの内容を読み込んでそれに応じた処理をしたり、加工したりするのに使います。

イメージを把み易いと思うので詳細は省略します。

2.3 他のプロセスを実行したときのストリーム

多くの言語では他のコマンドに投げた時に、標準出力に出力されるべき文字列を取得することができます。

which command の出力でそのコマンドの実際の場所を調べたり、文字列抽出を自分で書くのが面倒なときに grep string file で横着したり、日付を取得するのを date で横着したり。

後述の「プログラム言語で用意されている情報取得手段」ではリモートホストの情報を得ることは殆どできません。 リモートホストの情報を得たい場合は、rsh / ssh から返ってくるストリームを自前で処理してやる必要があります。

2.4 プログラム言語で用意されている情報取得手段

より精度の高い表現をするならば、OS から提供されている情報をプログラム言語を介して取得する、というところでしょうか。 まあプログラムからシステム(OS)の情報を調べられる、くらいの理解で良いと思います。

以下のような目的に使えます。 * ディレクトリツリーを検索する * ファイルのパーミッションを調べる * 時刻を取得する

2.5 コマンド引数

オプションやファイルの指定など、引数でコマンドの動作を制御できるようにしてやると使い勝手が大幅に改善されたりします。

moon1 に投げる時と moon2 に投げる時でスクリプト自体を書換えるのではなく、「throwVasp moon2」みたいにできた方が便利でしょう? コマンドの使い勝手に最もダイナミックに関わって来る部分だと私は思います。

2.6 環境変数

複数のコマンドが使う設定などはシェルの環境変数に指定しておいて、コマンド側でそれを取り出すようにする方法があります。

コマンド内部に情報を持つようにしておくと、将来その情報を変更することがあった場合、その情報を使う全てのコマンドを変更する必要がでてきます。 プログラミングのコツの一つは「変更しうる情報を一箇所にまとめること」です。 しかしコマンド内部にある情報は他のコマンドからは取り出せません。 環境変数とは親プロセスから子プロセスに情報を渡す仕組みです。 コマンドを呼び出す親であるシェルに環境変数として登録しておけば、子プロセス(コマンド)にそれが自動的に与えられる、というわけです。

通常設定される環境変数でよく使うものを挙げると以下のようなものでしょう。

勿論、ユーザが自作コマンド用に環境変数を設定することもできます。

2.7 子プロセスからの戻り値

(関連:コマンドをシェルに投げる)

プログラムは終了したときに「戻り値」という値を返します(※1)。 この値によってプログラムが「どのような状態で終了したか」ということを簡単に判別できます。 端的に言うならば、「そのコマンドが成功したか」を判定できます。

:※1:返り値、終了ステータスとも言います。

例えば、ping は成功した(ping が通った)時に 0 を返します。 則ち ping の戻り値を判定することで「相手のホストが生きているか」を判定することができます。(※2)

:※2:ping が通っても死んでる時や、生きていても ping に返答を返さないこともありますが。まあ九割方正しく判定すると思っておけば良いでしょう。

例えば zsh では、直前のコマンドの戻り値は「$?」に格納されており、「echo $?」で確認できます。

% ping -c 1 notExistHost
ping: cannot resolve notExistHost: Unknown host

% echo $?
68

% ping -c 1 localhost
PING localhost (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.093 ms

--- localhost ping statistics ---
1 packets transmitted, 1 packets received, 0% packet loss
round-trip min/avg/max/stddev = 0.093/0.093/0.093/0.000 ms

% echo $?
0

尤も、どの状況の時にどんな戻り値を返すかというのはコマンド毎に異なっているので細かい制御をするには一々調べなければなりません。 また、「成功のときに0を返す」というのも慣習なので、全てのコマンドがそうなってるとは限りません。