コマンドライン引数と終了ステータス

多くのプログラムに対して、起動時にコマンドライン引数(あるいは短く引数と呼ぶ)という形で情報を渡すことができます。例えば、

逆に、プログラムの終了時にはプログラムから終了ステータスという情報が返され、それをコマンドラインやその他の場所で利用することができます。終了ステータスを調べることにより、プログラムの実行が正常に終了(成功)したか異常終了(失敗)したかを判別できます。また、ソフトウェアによっては、異常終了した場合に異常の種別を知ることができるようになっています。

この章では、コマンドライン引数をプログラム内でどのように利用するか、終了ステータスをどのようにしてプログラムから返すのかについて学びます。

コマンドライン引数の利用

argc と argv

これまで、main 関数は仮引数のない形で定義してきましたが、次のように main には argc, argv という2つの仮引数を持たせることができます:

int main(int argc, char **argv)
{
  …(略)…
}

この argc, argv を利用すると、プログラム中でコマンドライン引数を扱うことができます。

argc, argv というのは仮引数名に過ぎないので、別の名前(例えば ac, av)に変更してもプログラムは動きますが、慣習としてこの名前がプログラマの間で長く用いられてきたので、この名前のままで用いたほうが良いでしょう。

argc は main 関数の第1引数で、int 型です。argc には、プログラムが起動されたときのコマンドライン引数の個数 + 1 が入ります。1 が足されている理由は、あとで見るようにプログラム名もコマンドライン引数といっしょに main に渡されるからです。

argv は main 関数の第2引数で、char ** 型という少し複雑な型を持ちます。 argv はコマンドライン引数が順番に文字列として並んだ配列と考えることができます。 もう少し正確に言うと、プログラムが

プログラム名 引数1 ... 引数n

のような形で起動されたとき、

起動されたプログラムのファイル名がargv[0]に入っているために、argv が指す配列の大きさは実際のコマンドライン引数の個数より1だけ大きいことに注意して下さい。

例として、ls -l /binをコマンドラインから実行したときの argc, argv のようすを下図に示します:

609x336
ls -l /binを実行した時の argc, argv のようす

ただし、図中青色で示したアドレスはあくまで一例であって、いつもこの通りになるわけではありません。

上で、argv の型は char ** であると述べました。それに従うと argv の型宣言は char **argv となりますが、char *argv[] という書き方も認められています。 argv[0], argv[1], ... の型は char * ですから、後者の書き方のほうが わかりやすいと考えて後者を好む人も多くいます。 しかし、char *argv[] のような型の表記はどこでも使えるわけではなく、 関数の仮引数を宣言する時だけに使える特殊なものです。例えば、 ブロック内の局所変数定義で char *a[];などと書くとコンパイラに叱られてしまいます。 そこで、この章では char **argv のほうの書き方を用いることにします。

argc, argv の内容確認

実際に、次のサンプルプログラムで argc, argv の働きを見てみましょう。

#include <stdio.h>

int main(int argc, char **argv)
{
  int i;

  printf("argc = %d\n", argc);
  for (i = 0; i < argc; i++) {
    printf("argv[%d] = %s\n", i, argv[i]);
  }
  return 0;
}

argv[i] が文字列の先頭を指すポインタであることから、printf 関数のフォーマット指定で %s フォーマットを指定していることに注意して下さい。

このプログラムをargs.c というファイルに記述し、コンパイル・実行すると次のようになります:

cc2001(151)% cc -o args args.c
cc2001(152)% ./args abcd 12345 xyz
argc = 4
argv[0] = ./args
argv[1] = abcd
argv[2] = 12345
argv[3] = xyz

コマンドライン引数からデータを読み取る

これまで、数値の計算を行うようなプログラムでは、 数値の入力を scanf を用いて行っていたと思いますが、 コマンドライン引数として数値を与えることも可能ですし、実際そのようなことがしばしば必要になります。 そのような例として、円の半径を引数として与えると、円の面積を求めて出力してくれるプログラムを考えましょう。次のプログラムを circle.c というファイルに記述します。

#include <stdio.h>
#include <math.h>

/* 円の面積を求める。                    *
 * 半径はコマンドラインの第1引数で与える */
int main(int argc, char **argv)
{
  double r;      // 半径
  double area;   // 面積

  if (argc == 2) {
    sscanf(argv[1], "%lf", &r);
    area = M_PI * r * r;
    printf("半径 %f の円の面積 = %f\n", r, area);
  }
  return 0;
}

コンパイル・実行すると以下のようになります。

cc2001(209)% cc -o circle circle.c
cc2001(210)% ./circle 1.0
半径 1.000000 の円の面積 = 3.141593
cc2001(211)% ./circle 2.0
半径 2.000000 の円の面積 = 12.566371

このプログラムでは、コマンドライン引数は1個だけですが、argv にはプログラム名も入っているので、引数が1個の時 argc は 2 になります。そこで、最初に argc が2であるかどうかのチェックをして、その時だけ面積の計算をしています。

argv[1] に半径が与えられているはずですが、それは半径を文字列として記述したものであって、数値ではありません。そこで、sscanf 関数を用いて文字列から数値への変換を行なっています。sscanf 関数は scanf 関数とよく似ていますが、scanf が標準入力からデータを読み取るのに対し、sscanf 関数は文字列からデータを読み取ります。例えば、

  scanf("%lf", &r);

であれば、標準入力から実数が1個読み取られてそれが double 型の数値となってrに代入されますが、

  sscanf("1.0", "%lf", &r);

の場合は、"1.0" という文字列から実数が読み取られるので、1.0 という実数が double 型の数値として r に代入されます。上のプログラムでは、argv[1]から実数が読み取られて r に代入されるので、結局コマンドラインの第1引数から実数を読み取って r に入れたことになります。

なお、sscanfを使う際にはヘッダファイルとして stdio.hをインクルードしておく必要があります。

課題 1.

コマンドライン引数として与えられた実数値をすべて合計してその結果を出力するプログラム total.c を作りなさい。コマンドライン引数が1つもないときには、合計は 0 と考えること。コンパイル・実行の例を以下に示します。

cc2002(109)% cc -o total total.c
cc2002(110)% ./total 1.0 2.0 3.0 4.0
Total: 10.000000
cc2002(111)% ./total        ← 引数が1つもないとき
Total: 0.000000

終了ステータス

終了ステータスに関する規約

UNIX では、すべてのプログラムは終了時に「終了ステータス」と呼ばれる整数値を返す決まりになっています。この値については重要な約束事があります。それは、

ということです。 「正常終了」は「成功」と呼ばれることもあります。また、「異常終了」は「失敗」と呼ばれることもあります。ユーザは終了ステータスの値を調べることにより、プログラムが正常に終了したかどうかを調べることができます。また、終了ステータスの値に応じてその後のコマンド実行の流れをコントロールすることもできます。

では、終了ステータスはどのようにして調べることができるのでしょうか? 10号館の標準設定では tcsh (ティーシーシェル) と呼ばれるソフトウェアがコマンドラインの世話をしてくれています。コマンドラインから起動されたプログラムが終了するとき、プログラムが返した終了ステータスは tcsh が受け取ります。 tcsh は独自の変数を持っており(注: C言語でいうところの変数とは別のものです)、 終了ステータスを受けとると、status という名前の特殊な変数にその値を格納します。従って、tcsh の変数 status の値を調べればプログラムの終了ステータスを知ることができます。

status 変数の値を取り出したいときは、$status と書きます。(先頭に $ 記号がついていることに注意して下さい。) 従って、例えば echo $status を実行することにより終了ステータスの値を表示できます。

少し試してみましょう。lsコマンドは存在しないファイルを引数に与えられると異常終了します:

cc2001(105)% ls
a.out  xyz.c             ←正常にファイルがリストアップされた
cc2001(106)% echo $status
0                        ←正常終了なので、終了ステータスは 0
cc2001(107)% ls -l abc   ←存在しないファイル abc を指定
ls: abc: そのようなファイルやディレクトリはありません   ←異常終了
cc2001(108)% echo $status
1                        ←異常終了のあとなので終了ステータスは 1

多くの Linux ディストリビューションの標準設定では、終了ステータスを調べるときに $status と書く代りに $? と書くことになっており、 終了ステータスを表示したければ、echo $status ではなく、echo $? と書かなければなりません。 これは、コマンドラインの世話をするソフトウェアとして tcsh ではなく bash (バッシュ)というものが使われているからです。 本学以外の環境で Linux を使うときには注意して下さい。

もう一つ例をあげましょう。grep というプログラムを使うと、ファイルの中から文字列を検索することができます。例えば grep abc file1 を実行すると、file1 というファイルの中から、abc という文字列を含んだ行をすべて探し出してくれます。 そして、

このように、終了ステータスの値を使い分けることで、異常終了の種類を区別できるようにしています。

cc2001(134)% grep abc file1   ← file1 から abc を検索
abcdefg hijklmn               ← abc を含む行が見つかった
xyz 12345 abc
cc2001(135)% echo $status
0                             ← 検索が成功したので正常終了
cc2001(136)% grep ppp file1   ← file1 から ppp という文字列を検索。しかし、見つからないので出力は無し。
cc2001(137)% echo $status
1                             ← 検索で見つからなかったので、終了ステータス 1 で異常終了していた
cc2001(138)% grep abc file2   ← 存在しないファイル file2 を指定してみる
grep: file2: そのようなファイルやディレクトリはありません
cc2001(139)% echo $status
2                             ← ファイルが存在しなかったので、終了ステータス 2 で異常終了していた

実際に終了ステータスに応じてその後のコマンド実行の流れを変えるような仕事は「シェルスクリプト」と呼ばれる仕組みで使われることが多いでしょう。 興味のある人は調べると良いでしょう。 10 号館環境では tcsh と呼ばれるシェルを常用しています。 多くの Linux 環境では bash と呼ばれるシェルを使っているでしょう。

終了ステータスの返しかた

プログラムから終了ステータスを返すには2つの方法があります。

一つの方法は、main関数から return する際に return文の引数として終了ステータス値を指定する方法です。return 0; とすれば終了ステータスが 0 になるので正常終了、return 1;のように 0 以外を指定すれば異常終了です。

一般に return文は関数から値を返すための構文で、その値は関数の呼出し元に返されますから、main 関数以外の関数で return 文を実行しても、終了ステータスを返すことはできません。しかし、main については、main を終了するとプログラム全体が終了することから、main の返す値は終了ステータスになるという規約があります。

もう一つの方法は、exit 関数を使うことです。 n を整数型の式として exit(n)を実行すると、プログラムは終了し、nの値が終了ステータスになります。exit関数はどんな関数の中でも実行できます。main以外の関数の中で何か異常が起きたような時に即座にプログラムを終了したければ exit を使うのが便利です。

なお、exitを使う際には正式にはヘッダファイルとして stdlib.hをインクルードしておく必要があります。

注意: 終了ステータス値がどのようにして返されるかはシステムに依存しますが、あまり絶対値の大きな値は返せないのが普通です。10号館のLinux環境では、main 関数の return 文や exit 関数の引数に指定された値の最下位8ビットだけが実際に終了ステータスとして返されます。従って、終了ステータス値として使えるのは 0 から 255 までの値に限られます。exit(256)exit(0)と同じになってしまいますし、exit(-1)exit(255)と同じになってしまいます。

mainからのreturnの例

前に、円の半径を引数として与えると、円の面積を求めて出力してくれるプログラムを考えましたが、もしも引数が与えられなかった場合、半径を決めることができませんから、面積の計算は行わずに異常終了するのが自然でしょう。また、引数が多すぎるのも異常と考えることにしましょう。そのようにプログラムを書き換えると以下のようになります:

#include <stdio.h>
#include <math.h>

/* 円の面積を求める。                     *
 * 半径はコマンドラインの第1引数で与える。*
 * 引数が1個でなかったときは、            *
 * ステータス値 1 で異常終了とする。      */
int main(int argc, char **argv)
{
  double r;      // 半径
  double area;   // 面積

  if (argc != 2) {  // 引数の個数が間違っている場合
    return 1;       // 異常終了
  }
  sscanf(argv[1], "%lf", &r);
  area = M_PI * r * r;
  printf("半径 %f の円の面積 = %f\n", r, area);
  return 0;  // 正常終了
}

現実問題としては、circle xyz のように引数として数値を表さない文字列が与えられたときにも異常終了とすべきでしょう。sscanf は実際に読み取ることができたデータの個数を return します。"xyz" という文字列からは数値を読み取れないので、sscanf は 0 を返します。一方、きちんとデータが読み取れた場合には本来のデータの個数(ここでは1)が返るはずなので、sscanf の値を調べて、それが1でなければ異常終了すべきでしょう。

課題 2.

ファイルからの入力課題3.課題4.、または ファイルへの出力課題1.課題2.等のプログラムでは、ファイル名が "a.txt" などとソースコード中に埋め込まれていました。 このような作り方をすると、同じプログラムで別のデータファイルを処理する度にプログラムを修正し、コンパイルし直さなくてはいけません。 それは面倒なので、一般にファイル名などはプログラム中に固定的に記述したりせず、実行時に与えるようにします。

上記の課題プログラムを修正して、ファイル名をプログラム中に直接記載するのではなく、引数として与えられるようにしてください。

修正に当たっては、以下の仕様を満たすようにしてください。

  1. コマンドライン引数が必要数に満たない、または過剰な場合(例えば課題2.はファイル名を二つ必要としますが、ファイル名を全く与えなかった場合、一つの場合、三つ以上与えた場合など)に、「ファイル名を二つ指定して下さい」などと適切なメッセージを表示して終了すること。終了ステータスとして 1 を返す。
  2. 指定したファイルを読み込みモードでオープンしたが、ファイルが存在せずエラーになった場合は「ファイル名 ****** が存在しません」と表示して終了する。終了ステータスとして 2 を返す。
  3. 指定したファイルを書き込みモードでオープンしたが、ファイル名やディレクトリ指定がおかしい等でエラーになった場合は「ファイル名 ******* を作成できません」と表示して終了する。終了ステータスとして 3 を返す。
  4. エラーメッセージは標準エラー出力に出すように。
  5. 終了ステイタスの設定は exit() を使う。(実際のプログラムでは return を使うことも可能だろうし、どちらを使っても良いが、どうせなら exit() を試してみましょう)

当然ながら ***** には実際に指定したファイル名が表示されるように。 どうしていいのかピンと来ない人は教員や補助員に尋ねること。