本文へジャンプ

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

送信されたすべての情報は安全です。

  • 閉じる [x]

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む


送信されたすべての情報は安全です。

  • 閉じる [x]

Linuxコマンド・ライン・ユーティリティーの開発

ベストプラクティスと思慮に富んだコーディングが素晴らしいコマンド・ライン・ツールにつながる

Vasudev Ram (vasudevram@yahoo.com), Independent software consultant
Vasudev Ramは、プロジェクト管理、ソフトウェア開発方法、C、UNIX、Java、およびデータベースの開発と管理の経験を備えた自営のソフトウェア・コンサルタントです。HCL Hewlett-Packard (現在のHCL Infosystems) にシステム・エンジニアおよびコンサルタントとして、Infosys Technologies Limitedにシステム・アナリストおよびプロジェクト・マネージャーとして勤務した経験があります。Vasudevのメール・アドレスは、vasudevram@yahoo.com です。

概要: エンド・ユーザーでも簡単に作成できるLinuxコマンド・ライン・ユーティリティーの書き方を学びます。素晴らしいコマンド・ラインにつながるベストプラクティス (最良の実行原則)の概観から始まり、実用的なページ選択ツールを詳細に解説するところまで、この記事では、みなさんが自分でユーティリティーの作成にとりかかるときに必要な基礎知識を紹介します。

日付:  2002年 6月 01日
レベル:  初級 この記事の原文:  英語
アクティビティー: 3321 ビュー
お気軽にご意見・ご感想をお寄せください: 


本稿では、cat、ls、pr、mvなどの標準的なコマンドと同じような、Linuxコマンド・ライン・ユーティリティーの記述方法を解説します。今回紹介するのはselpgというユーティリティーで、SELect PaGesを意味します。ユーザーは、selpgを使って、入力テキストから取り出すページの範囲を指定します。入力テキストは、ファイルから取り出すことも、別のプロセスから取り出すようにも指定することもできます。以下のように、Linuxでコマンドを作成するときの事実上の規約 (de facto conventions) にしたがったユーティリティーとします。

  • 単独で動作する
  • コマンドのパイプラインの中の1個のコンポーネントとして動作する (標準入力または引数で指定したファイルを読み込み、標準出力および標準エラーに書き出す)
  • 動作に変更を加えるコマンド・ライン・オプションをとる

Selpgは、私がかつて顧客のために作成したもので、その後、UNIXのメーリング・リストにポストしたところ、数多くのメンバーが便利なツールだと言ってくれたものです。

このユーティリティーは、標準入力またはコマンド・ライン引数に指定されたファイルから、テキスト入力を読み込みます。ユーザーが、この入力の出力したい範囲をページで指定すると、その部分が出力されます。たとえば、入力が100ページからなっている場合に、35~65ページだけを印刷せよ、というような指定ができます。これは、実際、指定した部分だけをプリンターに印刷したい場合に、用紙を無駄にせずに済みますので、便利です。他にも、非常に大きなファイルを印刷したのだけれども、紙詰りか何かで、ある部分だけがちゃんと印刷できなかった、というような場合もあります。このようなとき、必要なページだけを印刷したいときに、このツールが役に立ちます。

本稿では、本物のLinuxユーティリティーを紹介するだけでなく、以下のことも説明します。

  • ソフトウェア開発用としてLinux環境が備えている能力。
  • Cで、fopen、fclose、access、setvbuf、perror、strerror、popenなどのシステム・コールやライブラリー関数を正しく使用する方法。
  • 1回きりのプログラムではなく、広く一般に使用されるユーティリティーを目指した場合に行うべき完璧なエラー・チェックの実現方法。
  • Cでプログラミングを行うときに起こる可能性のある、バッファー・オーバーフローなどの問題に対する警告や、そういう問題を起こさないようにするための助言。
  • コマンド・ライン引数を手ずから解析する (hand-coded parsing) 方法。
  • このツールをパイプラインで使用したり、入力、出力、エラー・ストリームのリダイレクションを行う方法。

コマンド・ラインのガイドライン

汎用的なLinuxユーティリティーを作成するときは、ガイドラインにしたがってコーディングを進める必要があります。ガイドラインは、長い時間を経て発展してきた推奨事項であり、ユーティリティーを柔軟に使用できるようにする、とくに、他のコマンド (組込済みのコマンドであれ、ユーザー作成のコマンドであれ) やシェルと連携できるようにする上で、手助けとなるものです。この連携するということが、Linuxの開発環境としての能力にとって、重要な点の1つです。Selpgユーティリティーでは、以下のガイドラインや機能を説明します。(注意: 以下の例で、$ はシェル・プロンプトであり、ユーザー入力ではありません。)

ガイドライン1. 入力

入力は、以下のいずれかの方法で指定できるものとします。

  • コマンド・ラインにファイル名を指定する。例:

    $ command input_file

    この場合、commandはinput_fileを読み出す必要がある。

  • 標準入力 (stdin)。デフォルトで端末 (すなわち、ユーザーのキーボード)。例:

    $ command

    この場合、ユーザーがControl-D (ファイルの終わりの印) を入力するまでにキー入力したものがcommandの入力となる。

ただし、stdinは、シェルの < (標準入力のリダイレクト) 演算子を使って、ファイルから入力されるようにリダイレクトすることもできます。例:

$ command < input_file

この場合、commandは、標準入力を読み出しますが、それは、シェル / カーネルによって、input_fileからリダイレクトされたものです。

Stdinは、また、シェルの | (パイプ) 演算子を使って、別のプログラムの標準出力から入ってくる場合もあります。例:

$ other_command | command

この場合、other_commandの標準出力 (stdout) は、(シェル / カーネルによって) 透過的にcommandの標準入力に渡されます。

ガイドライン2. 出力

出力は、標準出力 (stdout) に出力します。標準出力も、デフォルトで端末 (すなわち、ユーザー画面) です。例:

$ command

この場合、commandの出力は画面に表示されます。

標準出力も、シェルの > (標準出力のリダイレクト) 演算子を使って、ファイルにリダイレクトすることができます。例:

$ command > output_file

この場合、commandは、あくまでもstdoutに書き出しているのですが、それがoutput_fileに書き出されるように、シェル / カーネルがリダイレクトを行います。

また、commandの出力は、やはり | 演算子を使って、別のプログラムの標準入力とすることもできます。例:

$ command | other_command

この場合、シェル / カーネルは、command出力がother_commandの入力となるようにします。

ガイドライン3. エラー出力

エラー出力は、標準エラー (stderr) に出力します。標準エラーも、デフォルトで端末 (すなわち、ユーザー画面) です。例:

$ command

この場合、commandの実行中に何かエラー・メッセージが出される場合、画面に書き出されます。

ただし、stderrのリダイレクションを使えば、エラーがファイルに出力されるようにリダイレクトすることもできます。例:

$ command 2>error_file

この場合、commandの通常の出力は画面に表示され、エラー・メッセージはerror_fileに書き出されます。

stdoutとstdinの両方を、それぞれ別々のファイルにリダイレクトすることもできます。例:

$ command >output_file 2>error_file

この場合、stdoutはoutput_fileに出力され、stderrへの書き出しはerror_fileに出力されます。

また、stdoutのリダイレクト先と同じところにstderrをリダイレクトすることもできます。例:

$ command 2>&1

この場合、2>&1という表記は、「stderrをstdoutの出力先に送りなさい」という意味で、エラー・メッセージも通常のメッセージも画面に表示されることになります。もちろん、これは、commandを単に

$ command

と起動しても同じことですので冗長ですが、この機能は、同じコマンド・ライン上で、stdoutがすでに別の出力先にリダイレクトされていて、stderrを同じ出力先に出力したい場合に便利です。例:

$ command >output_file 2>&1

この場合、stdoutが、まずoutput_fileにリダイレクトされます。その後、2>&1によって、stderrもoutput_fileにリダイレクトされることになります。

ガイドライン4. 実行

プログラムは、単独で実行することも、上でいくつか例を示したように、パイプラインの中で実行することもできるようにしなければなりません。このことは、入力元 (ファイルか、パイプか、端末) や出力先にかかわらず、プログラムは同じように処理しなければならない、ということを意味します。そうすることで、プログラムの使い方が最大限柔軟なものになります。

ガイドライン5. コマンド・ライン引数

入力やユーザの設定 (user preferences) によってプログラムが違った動作をするようにしたい場合には、オプション と呼ばれるコマンド・ライン引数の指定をプログラムが受け付けるようにする必要があります。それによって、ユーザーは、プログラムの起動時に、どんな動作をさせたいのかを指定できることになります。

オプションのコマンド・ライン引数は、引数の先頭に "-" (ハイフン) を付けて指定します。それ以外の引数は、オプションではありません。そういう引数は、プログラムの動作を変更するためのものではなく、データ名などとなります。必ずそうでなければならないというわけではありませんが、通常、プログラムが処理の対象とするファイル名を表します。引数では、その他、印刷の出力先やジョブID (その例についてはman cancelを参照) などを表すこともあります。

ファイル名などを表すオプション以外の引数 (先頭にハイフンを付けない引数) がある場合には、コマンドの最後に指定します。

通常、ファイル名を引数に指定する場合、プログラムは、それを入力として受け取ります。そうでない場合、プログラムはstdinから入力を読み込みます。

オプションは、必ず、"-" (ハイフン) で始めなければなりません。オプションは、それに付随した引数をとることができます。

Linuxユーティリティーの構文図式は、以下のようなものになります。

$ command mandatory_opts [ optional_opts ] [ other_args ]

ここで、

  • command は、コマンド自体の名前です。
  • mandatory_opts は、このコマンドを実行する上で必要な一連のオプションです。
  • optional_opts は、ユーザーの選択次第で、指定してもしなくてもよい一連のオプションです。ただし、selpgの -fオプションと -lオプションの場合のように (これについては、以下で説明します)、オプション同士が互いに排他的な場合もあります。
  • other_args は、コマンドによって処理すべきその他の一連の引数で、ファイル名だけでなく、何でも指定することができます。

上の定義で、「一連のオプション」という用語は、スペースかタブか、あるいはその両方の組み合わせかによって、区切られたオプションの羅列を意味します。

上の構文で角括弧 (square brackets) に入っている部分は、省くことも可能です (その場合、括弧ごと省きます)。

個々のオプションは、以下のような形式をとります。

-f (オプションのみ)
-s20 (オプションに引数を1個続けたもの)
-e30 (オプションに引数を1個続けたもの)
-l66 (オプションに引数を1個続けたもの)

ユーティリティーによっては、引数付きのオプションに、これとは少し違った形式をとり、-s 20のように、オプションと引数とを空白 (white space) で区切るものもありますが、コーディングが複雑になりますので、私は、この形式は採用しませんでした。この形式の利点は、コマンドが少し判別しやすくなるという点だけです。

上に示したのは、selpgが実際にサポートするオプションです。


Selpgプログラムのロジック

先述のとおり、selpgは、テキスト入力から作成される出力の、ページ範囲を選択するユーティリティーです。この入力は、コマンド・ラインの最後の引数として指定されるファイルから読み出される場合もあれば、ファイル名の引数が指定されずに標準入力から読み出される場合もあります。

Selpgは、最初に、すべてのコマンド・ライン引数を処理します。オプション引数 (ハイフンで始まっている引数) をすべて読み進んだ後、さらに引数があれば、それを入力ファイルの名前であるとみなし、そのファイルをオープンして読み出そうとします。オプション以外の引数がなければ、selpgは、stdinから入力を行うものとみなします。

引数処理

必須オプションの -sNumberと -eNumber
Selpgは、抽出すべきページ範囲の始まりと終わりをユーザーに指定してもらう必要があります。そのために、-sNumber (たとえば -s10は10ページから始めることを意味する) と -eNumber (たとえば -e20は20ページで終わることを意味する) の2つのコマンド・ライン引数を使用します。Selpgは、指定されたページ番号の有効性チェック (sanity checking) を行います。すなわち、両方の数字が有効な正の整数であり、終了ページが開始ページよりも小さくないことをチェックします。この -sNumberと -eNumberの2つのオプションは、必須であり、コマンド・ラインの中で、コマンド名selpgの後にくる最初の2個の引数でなければなりません。

$ selpg -s10 -e20 ...

(... は、以下で説明するコマンドの残りの部分を表します)。

任意オプションの -lNumberと -f
Selpgは、2種類の入力テキストを処理することができます。

タイプ1: これは、ページの行数が一定であるタイプのテキストです。これがデフォルトのタイプで、この場合にはオプションを指定する必要はありません。つまり、-lNumberオプションも -fオプションも指定されなければ、ページの長さは固定で、どのページも72行であるとみなされます。

これをデフォルトとしたのは、これがライン・プリンターでごく一般的なページ長だからです。これは、最も一般的なコマンドの使い方をデフォルトにし、ユーザーが必要以上にオプションをキー入力しなくて済むようにするという意図です。このデフォルトは、以下のように -lNumberオプションで指定変更することができます。

$ selpg -s10 -e20 -l66 ...

これで、ページの長さは固定で、どのページも66行であるという指定になります。

タイプ2: これは、ページがASCIIのフォーム・フィード文字 (10進の12、Cで \fとして示されるコード) で区切られるタイプのテキストです。1ページあたりの行数が固定のフォーマットに比べ、このフォーマットがすぐれている点は、1ページあたりの行数がまちまちで、ファイルにかなりのページ数が含まれている場合に、ディスク・スペースが節約されるという点です。タイプ2のページの場合、ページの終わりは、テキスト行を続けた後に、フォーム・フィード文字を1個入れるだけで指示することができます。プリンターは、フォーム・フィード文字を認識し、次の行のデータが新しいページから始まるように、自動的に必要な行数だけプリント・ヘッドを進めます。

この点、タイプ1の場合、テキストを次のページに進めるためには、ファイルに(PAGELEN- CURRENTPAGELEN)個の改行を入れておく必要があります。ここで、PAGELENはページの固定サイズを、CURRENTPAGELENは現在のページに含まれるテキストの実際の行数を表します。この場合、プリンターは、次のページの先頭にプリント・ヘッドをもっていくために、その改行の数だけ実際に印刷を行うことになります。これは、ディスク・スペースの観点からだけでなく、印刷速度の点からも非効率的です(実際上、大した差はないかもしれませんが)。

タイプ2のフォーマットは、以下のように、-fオプションで指定します。

$ selpg -s10 -e20 -f ...

これによって、入力中にフォーム・フィード文字を見つけたら、それをページ区切りコードとして処理してほしいということがselpgに指示されます。

注意: -lNumberオプションと -fオプションを両方いっしょに指定することはできません。

任意オプション -dDestination
Selpgでは、また、ユーザーが、-dDestinationオプションを使って、選択したページ範囲をプリンターに直接送出することもできます。ここで、Destinationは、lpコマンドの -dオプションで受け付けられる印刷出力先の名前でなければなりません (man lp参照)。出力先は実際に存在するものでなければなりません (selpgは、このチェックは行いません)。このオプションが実際に働いているかどうかは、-dオプションを付けてselpgコマンドを実行した後、lpstat -tコマンドを実行すればわかります。これで、Destination向けの印刷キューに印刷ジョブが追加されていることがわかるはずです。プリンターが現在その出力先に接続され有効になっている場合、そのプリンターに出力が印刷されるはずです。この機能は、プロセスが別のプロセスへの出力または入力用のパイプをオープンできるようにするためのシステム・コール popen() を使って実現されます。この場合、以下のコマンド

$ lp -dDestination

への出力用のパイプをオープンし、以下のように、stdoutではなく、このパイプに書き出しを行います。

selpg -s10 -e20 -dlp1

これによって、指定のページ範囲が印刷ジョブとして印刷出力先lp1に送出されます。すると、"request id is lp1-6" といったメッセージが表示されるはずです。このメッセージは、lpコマンドから出されるもので、印刷ジョブIDを示しています。selpgコマンドの直後にすぐに lpstat -t | grep lp1 というコマンドを実行すると、lp1のキューにそのジョブが表示されているはずです。ある程度時間が立ってからlpstatコマンドを実行すると、印刷済みとなり、ジョブがキューから消えてしまい、ジョブを確認できないかもしれません。

入力処理

コマンド・ライン引数の処理がすべて終わると、指定されたオプションや入出力先にしたがって、実際の入力処理が始まります。

Selpgは、現在のページ番号を次のようにして保持します。入力が1ページ行数固定のタイプの場合、ページ長の行数に達するまで改行の数をカウントし、ページ長に達したらページ・カウンターを1つ増加させます。入力がフォーム・フィード区切りタイプの場合には、フォーム・フィードの個数をカウントします。いずれの場合も、ページ・カウンターの値が開始ページと終了ページの間にあるかぎりは、テキストを(1行ずつ、あるいは1文字ずつ) 出力していきます。この条件が偽の場合、すなわち、ページ・カウンターが開始ページよりも小さいか終了ページよりも大きい場合には、出力の書き出しは行いません。はい、これで、指定ページの出力ができあがりというわけです。


コードの説明

では、ソースを詳しく見ていくことにしましょう。まだselpg.cをダウンロードしていない方は、よろしかったらここでダウンロードください ( 参考文献 参照)。コードを少しずつ解説していきたいと思います。

コメント ==== includes ===== 以下の部分
この部分には、必要なヘッダー・ファイルを指定しています。

stdio.hは、Cの標準I/Oライブラリーのヘッダーで、ファイルのオープン、クローズ、読み出し、書き込みを行う関数 ( fopen()fclose()fgets()getc() など)、 printf() 系統の関数、および setvbuf() 関数を使うために必要です。

stdlib.hは、文字列を整数に変換する atoi() (ASCII to integer) 関数に必要です。

string.hは、 strcpy()strcmp() などの文字列関数用です。

unistd.hは、 access() 関数に必要です。

limits.hは、みなさんが使用しているコンパイラー / OS / ハードウェア・プラットフォームでintがとることのできる最大値を指定するINT_MAXの定義に必要です。直接コード化した値 (hardcoded value) を使用せず、この定数を使用するようにすることで、コードの移植性が高くなります。

assert.hは、デバッグ用マクロ assert() に必要です。

errno.hは、グローバルなシステム・コールのエラー番号変数であるerrnoの宣言に必要です (これについては、以下でさらに説明します)。

コメント ==== types ===== 以下の部分
型の定義は、 selpg_args 構造体の1つだけです。この型の変数へのポインターが process_args() 関数に渡され、引数処理で得られた値が、この変数に入れて返されます。 typedef を使って、この型にsp_argsという短い名前を付けています。

また、入力を読み込むときにバッファーとして使用される文字配列のサイズを表すINBUFSIZというマクロも定義しています。性能を上げるために、このようにしています。

コメント ==== globals ====== 以下の部分
Progname は、char* 型のグローバル変数で、コマンドの起動に使われた名前を保存します。エラー・メッセージに表示するときに使用されます。こうすれば、selpgコマンドを別の名前に変えたときでも、新しい名前がメッセージに表示されることになり、コードを修正する必要がなくなります。

コメント ==== prototypes === 以下の部分
この部分には、ANSI Cの規約にならって、コード中のすべての関数の関数プロトタイプを宣言します。現在では、これが標準的な方法となっており、これによって関数定義 / 宣言と関数の使い方との間の型の不一致をコンパイラーが検出しやすくします。

main() 関数
簡単なコードです。必要な変数を宣言した後、変数 acav&sa を指定して、関数 process_args() を呼び出します。 ac はargument countの意味で、コマンド名自体を含めてのコマンド・ライン引数の数が入ります。 av はargument vectorの意味で、charへのポインターのポインターです。この変数には、すべてのコマンド・ライン引数が文字列配列として格納されます。 &sa は、 sp_args 型構造体へのポインターです。 process_args() からは、引数について解析された値が構造体 sa に格納されて返されてきます。そこで、この変数を関数 process_input() に渡してやると、この関数が、必要なページの選択と指定された出力先へのページの書き出しを行ってくれます。

処理を続行できないといったエラーがコード中のどこで発生した場合でも、システム・エラー・メッセージを読み出し (そういうメッセージがある場合)、それを、プログラム独自のメッセージといっしょに表示します。そして、エラー・コードを添えて exit() 関数を呼び出します。このユーティリティー・プログラムでは、いろいろなエラー条件ごとに異なる番号を返すようにしています。ただし、これは、絶対こうしないとならないというわけではありません。単にエラー時には1を、正常終了のときには0を返すようにしているユーティリティーもあれば、エラーをたくさんのカテゴリーに分類し、カテゴリーにしたがって、さらに狭い範囲のコード (たとえば、1か2か3) のいずれかを返すようにしているものもあります。約束ごととして規定されているのは、正常時には0を返し、異常時には非ゼロ値を返すということだけです。システム・コールで発生しうるエラーの詳細については、以下の システム・コール・エラー の節を参照してください。

エラーが発生すると必ずexitすることになりますので、 process_input() 関数から返ってくるということは、エラーがなかったということを意味しますので、 main() からは0を返します。


システム・コール・エラー

Linuxのシステム・コール (もしくはCのライブラリー関数) を呼び出す場合、独自の関数を呼び出す場合と同様、エラーが起こる可能性があります。Cの関数の規約 (convention) に従えば、成功時には関数の戻り値で結果に関する何らかの情報を示し、失敗時には戻り値で失敗の理由に関する情報を示すのが一般的です。これに合わせて一般的に順守されている規約があります。

関数やシステム・コールが成功したときの戻り値の意味は、ルーチンごとに異なります。たとえば、 read() は、( read() に対する引数の1つで要求されたバイトではなく) 実際に読み出したバイト数を返しますし、 fopen() はstruct FILEへのポインターを返します。

ここで、われわれに関係するのは、成功しなかった呼び出しの戻り値です。

ほとんどの呼び出しは、エラーの意味で、-1かNULLかEOFを返します。(上で見たように、 fgets() はNULLを返すのに対して、 getc() はEOFを返します。)また、これらの呼び出しは、errnoというグローバルなint変数をセットします。この変数は、errno.hでextern int errno; として宣言されています。このintは、 sys_errlist というシステム・エラー・メッセージのテーブル (文字列の配列) へのインデックスとなっています。このテーブルの最後の要素のインデックスは sys_nerr - 1 で、 sys_errlistsys_nerr も、やはりerrno.hで宣言されています。

システム・コールでエラーが発生した場合、2つある方法のうちの少なくともどちらかの方法で、そのエラーに対応するメッセージにアクセスし、表示することができます。すなわち、 perror() 関数を使うか strerror() 関数を使うかです。(selpgでは両方の使い方を示しています。)

perror (man perror参照) は、引数として文字列を1個だけとります。そして、メッセージをカスタマイズするために指定されたこの文字列引数をstderr (標準エラー) に書き出し、その後にコロンとスペースを続け、さらにシステム・エラー・メッセージを続けて、改行を入れます。

strerror() 関数 (man strerror参照) は、少し違います。この関数では、文字列としてのシステム・エラー・メッセージにアクセスすることができます。strerror() は、整数引数の errno をとります。この引数には、グローバル変数 errno の実際の値を指定します。この変数は、システム・コール (あるいはシステム・コールを呼び出すライブラリー関数) を使用するすべてのCのプログラムがインクルードしているはずのヘッダーerrno.hに含まれている実際の名前を使ってアクセスすることができます。 Strerror() は、システム・エラー・メッセージを文字列として返してきます。これらの関数を使用する場合には、守るべき条件がいくつかあります。詳細については、manのページを参照してください。

Selpgは、ユーザーに適切なメッセージを伝えるために、これらの関数を随所で使用しています。なお、システム・エラー・メッセージが確認できるように、showsyserrというちょっとしたユーティリティーも用意しておきました ( 参考文献 参照)。このユーティリティーは、コマンド・ライン引数に errno の値をとる方法と、何も引数をとらない方法の2通りの方法で実行することができます。引数を渡した場合、その引数に対応するメッセージだけを表示します。引数を指定しない場合、定義されているすべてのエラー・メッセージを表示します。

引数を指定せずにこのユーティリティーを実行すれば、どんなエラー・メッセージがあるのかを調べるためのツールとして使用することができます。あるいは、場合によっては、便利なデバッグ・ツールとして使用することもできます。Linux (あるいは、少なくともUNIX) のユーティリティーの中には (デーモンですら)、エラー・メッセージを表示せずに errno の値だけを表示するものがあります。このようなとき、その errno 値を指定してshowsyserrを実行すれば、対応するエラー・メッセージを確認できるというわけです。私は、UNIXで"Panic - error 2" のようなメッセージがただコンソールに繰り返し流れるだけのエラーを解決する際にこのツールを使ってきました。このメッセージの出所(でどころ) は、明らかに出来の悪いモジュールでした -- もっと意味のあるメッセージを出力すべきでしょう-- 引数に2を指定してshowsyserrを実行することでこうした問題もデバッグすることができました。結局、その劣等プロセスがひっきりなしにディスクに書き込みを行っていたため、ファイル・システムが一杯になっていたことが判明しました。

ディスク・スペースの問題であることがわかりましたので、psとかkillといったコマンドを使って、それがどのプロセスなのかを調べ、それを強制終了 (kill) することができました。

process_args() 関数
これは、ある一部の人間だけが使用するユーティリティーではなく、広く一般に使用してもらうためのものですので、できるだけ堅固な (robust) ものとなるように、さまざまなエラー・チェックを行っています。

コメント ==== check the command-line arguments === 以下の部分
少なくとも3個のコマンド・ライン引数が渡されてきているかをチェックします。つまり、以下の3つの引数です。

  • コマンド名そのもの
  • -sNumber オプション
  • -eNumber オプション

引数が3個よりも少ない場合には、メッセージを表示して終了します。ここでは、 usage() 関数を呼び出しています。このユーティリティーの正しい呼び出し方をユーザーに示すための関数です。これも、汎用的なユーティリティーを作成する場合に守るべき約束ごとの1つです。

コメントhandle 1st arg以下の部分
コメント !!! PBOは、Possible Buffer Overflow (バッファー・オーバーフローの可能性) の意味で、Linuxであれ他のプラットフォームであれ、Cでプログラムを書く場合には必ずチェックする必要のある問題です。これは、 s1 にコピーされたきた文字列が s1 に割り当てたサイズ、すなわちBUFSIZ (stdio.hで定義されている定数) よりも長い場合に起こります。誤って起こすか意図的に起こすのでないかぎり、これが起こる可能性は低いのですが、チェックしておいて損はありません。バッファー・オーバーフローは、安全性に関る数々のバグ (security bug) の原因となります。これを正しく処理するには、 strcpy() 関数ではなく strncpy() を使用し、コピーすべき最大バイト数をBUFSIZ - 1にします。さらに、s1にコピーされた最後の文字の後にNULL文字を追加しておくべきです。

PBOコメントを入れてあるところは、すべて、同じ手続きを踏む必要があります。

ここでは、 argno が指している現在の引数を文字列変数 s1 にコピーしています (argno 1に初期化されており、コマンドそのものの後の実際の1番目の引数を指すようにしています)。次に、 s1 の最初の2文字が開始ページ・オプションを示す -sであるかを調べています。そうでなければ、エラー終了します。

-sなら、 s1 のそれ以降の文字を整数に変換します。そして、INT_MAXを使って、その値がこのプラットフォームで有効な整数かどうかをチェックします。(Cのintのサイズは、プラットフォームによって、あるいは同じプラットフォームでもコンパイラーによって異なるため、値を直接コーディングするのではなく、INT_MAXを使用する必要があるということについては、すでに述べたとおりです)。

問題なければ、 psa が指している構造体の中のフィールドに開始ページを保存します。

コメントhandle 2nd arg以下の部分
1番目の引数と同様のチェックや処理を行います。余計に行わなければならないことは、指定された終了ページが開始ページよりも小さくないかチェックするという点だけです。

コメントnow handle optional args以下の部分
この部分では、さらに引数が指定されている場合に、それを処理します。ここでは、Linuxによくあるスタイルとして、コーディングの常套手段が使用されている点に注目してください。ループの中で行っていることは、引数リストをたどっていき、それぞれの引数がオプションかどうかをチェックするということです。オプションなら、switch文がどのオプションなのかを判定します。有効なオプションであれば、構造体にフラグをセットします。また、-lオプションのページ長や -fの印刷出力先など、オプションにデータが添えられている場合には、その値を保持しておくために、変数をセットします。有効なオプションでない場合には、エラー・メッセージを表示して終了します。

コメントthere is one more arg以下の部分
- で始まる引数をすべて処理し終えたら、さらに引数があるかどうかをチェックします。selpgの場合、そのような引数は、あっても1個だけで、それは入力に使われるファイル名とみなされます。ここでは、そのファイルが存在するか、そしてそれが読み出し可能であるかを、 access() 関数を2回呼び出すことでチェックしています。これは、2回目の呼び出しで存在するかどうかのチェックも行っているわけですので (存在しないファイルは読み出すことができない)、冗長なのですが、存在するかどうかのチェックだけを単独で行えることを示すために、このようにしました。

コメントcheck some post-conditions以下の部分
ここでは、Cの「アサーション (assertions)」を利用しています。これは、C言語の機能で、Linux自体の機能ではないのですが、信頼性の高いコードにする上で有用な機能ですので、入れておきました。

この機能の詳細については、以下の 契約に基づく設計 を参照してください。

process_input() 関数
最初に、変数をいくつか宣言しています。

コメントset the input source以下の部分
ファイル名の引数がコマンド・ラインに指定されなかった場合には、stdinが使用されます。それが指定された場合には、そのファイルを読み出し用としてオープンします。オープンできない場合には、エラー終了します。オープンできた場合には、 setvbuf() 関数を呼び出して、 fin からの読み出し用に、サイズがINBUFSIZ (先に16Kバイトに #defineしてある) のバッファーをセットします。これは、入力を高速に読み出すための方法です。サイズを16Kバイトにしたのは、1Kバイトから64Kバイトまでの間でサイズをいろいろ変えてみて実験してみた結果、1Kバイトから16Kバイトまでバッファー・サイズを少しずつ変えていくと、性能がある程度改善されていくが、それ以上増やしてもあまり改善がないことがわかったためです。みなさんのシステムでのサイズ効率 (mileage) は、違ったものになるかもしれません。バッファー・サイズをいろいろ変化させてみて、みなさんのLinuxシステムでどのサイズが最善なのかを調べてみるとよいでしょう。練習として、バッファー・サイズを指定する -bNumberのような名前の引数をとることができるように、selpgのコードを修正してもよいでしょう。そうすれば、いちいち #defineマクロで値を変更して、コードをコンパイルし直さなくても、実験ができることになります。この引数は、任意のオプションということになりますので、オプションが指定されなかった場合には、「-l数」のオプションでそうしているように、デフォルトのバッファー・サイズとして適当な値を使用するようにすることができます。


契約に基づく設計

テストやデバッグを進める上で、アサーションは、非常に役に立ちます。assertマクロの結果は、そのBoolean引数が真 (すなわち非ゼロ) であるかどうかをチェックすることにあります。真なら、何も行われません。偽なら、プログラムは、アサーションが失敗したことを示すメッセージを表示して終了します。アサーションを失敗させたBooleanの状態も表示されます。アサーションは、NDEBUGマクロを定義することで、無効にすることができます。その1つの方法は、ソースに #define NDEBUGという内容の1行を追加すことで (#defineに値を指定する必要はありません)、もう1つの方法はコンパイル時に、以下のように、ccまたはgccコンパイラーのコマンド・ラインに、コマンド・ライン・スイッチ -DNDEBUGを含めるやり方です。

$ cc -DNDEBUG -o selpg selpg.c

アサーションは、コード中の任意の場所で使用することができますが、関数の入り口と出口に配置して、コードが正しい場合に存在するはずの事前条件と事後条件をチェックするのが上手いやり方です。すなわち、関数の引数に有効な値の組み合わせや範囲が存在する場合には、その旨のアサーションを関数内のコードの1行目に配置すればよいでしょう。たとえば、平方根を計算する関数が非負値だけを扱えるものとすると、入力引数 >= 0というアサーションを設けることができます。つまり、非負の引数だけが渡されるようにするのは、呼び出し側の関数の責任であるというわけです。負の値が指定された場合には、アサーションが発動され、入力引数 >= 0という条件が失敗したというメッセージを表示して、プログラムをアボートします。同様に、関数の最後の部分でも、事後条件が成立することがわかっているのであれば、それぞれの条件に合わせてアサーションを記述します。このようにすれば、この関数内のコードにバグがあるために、出口の部分で真となるはずの条件がそうならなかったのかどうかを知ることができることになります。

アサーションの上手い使い方として、最終的なアプリケーションに2つのバージョンを設けることがあります。1つは、アサーションを無効にしたバージョン (リリース・バージョン) とアサーションを有効にしたバージョン (デバッグ・バージョン) の2つです。最初は、ユーザーにリリース・バージョンを提供します。ユーザーが離れた場所にいる場合、デバッグ・バージョンもいっしょに提供してもよいでしょうが、ディレクトリーは別々にしておきます。アプリケーションに何か問題が発生した場合には、ユーザーに一時的にリリース・バージョンとデバッグ・バージョンを入れ替えてもらい、問題を再現してもらうようにします。そうすると、アサーションがメッセージを表示することになるでしょうから、バグの在処 (ありか) が突き止めやすくなります。

デバッグ・バージョンとリリース・バージョンの作成は、makeユーティリティーで自動化できます。いっしょに提供しているmakefile (稿末の 参考文献 参照) が、それを示す簡単な例となっています。makeとは、1個または複数のファイルを基にアプリケーションを構築する作業を自動化するためのユーティリティーのことです。Makeは、ソース・ファイルが修正された場合には、ソースをコンパイルし直して、対応するオブジェクト・ファイルも作成し直す必要があるといったルールなどの依存関係を処理することができます。その他にも、makeは、いろいろなことを行うことができます。

コメントset the output destination以下の行
-dDestinationオプションが指定されなかった場合には、stdoutに書き込みを行います。-dDestinationオプションが指定された場合には、lp -dDestinationというコマンド文字列を使って、lpコマンドへのパイプのオープンを試みます。これには、別のプロセスへのパイプを読み出しまたは書き込み用としてオープンするためのシステム・コール popen() を使用します。ここでは、lpへのパイプを書き込みモードでオープンします。このことは、selpgプロセスからの標準出力が、すべて、lpプロセスの標準入力に送られることを意味します。 popen() が失敗に終わった場合には、エラー終了します。

コメントbegin one of two main loops以下の部分
入力のタイプにしたがって、2つのループのいずれかを実行します。

ページ・タイプが1ページ行数固定である場合、入力の読み出しは、ライブラリー関数の fgets() を使って1行 ずつ行います。(fgets() は、エラーかEOF [end-of-file] を検出すると、stdio.hで定義されているNULLを返します。ループ終了時に、どちらが検出されたのかをチェックします。)NULLでなければ、行カウンターを1つ増加させます。そして、行数がページ長を超えていないかチェックします。行数がページ長を超えていれば、ページ・カウンターを1増やしてやり、行カウンターを0にリセットします。そして、ページ・カウンターが指定されたページの範囲内であるかどうかを調べ、そうであれば、現在の行を書き出します。そうでなければ、書き出しは行いません。 fgets() がNULLを返すまで、このループを繰り返します。注意: fgets() は、最大BUFSIZ - 1個の文字を fin から読み込み、それをchar型配列lineに格納します。 fgets() は、最後にNULL文字を追加して、lineが正しくゼロ終端されたCの文字列となるようにします。

フォーム・フィード区切りタイプのページに対するロジックもだいたい同じですが、行カウンターを設ける必要がないため、少し簡単になっています。ライブラリー関数 getc() を使って、1文字ずつ読み込みを行います。 getc() は、エラー時にはEOFすなわちend-of-file (ファイルの終わり) を返してきます。1文字読んでは、フォーム・フィードかどうかをチェックし、そうであれば、ページ・カウンターを1つ増加させます。1ページ行数固定の場合と同様、ページが指定の範囲内にある場合にのみ、出力を書き出します。 getc() がEOFを返すまで、このループを繰り返します。

1行ずつあるいは1文字ずつ読み込みを行う場合に、 setvbuf 呼び出しがどういう点で役に立つのだろうかと思われる方もいるのではないでしょうか。それは、こういうことです。ここではstdioライブラリーを使用していますので、ライブラリー内の下位レベルのルーチンは、ディスクからデータをブロック単位で読み込み (ブロックのサイズは、プログラムで指定したバッファー・サイズにしたがって決まる)、それを、バッファーinbufに入れます。そこで、 fgets()getc() を呼び出すと、そのつど、データがプログラムで定義した変数 ( line または c ) に渡されます。ディスクからデータを読み込む場合、大きなブロック単位で読み込むほうが効率的であり、また、メモリー内のある場所 ( inbuf ) から別の場所 (プログラムで定義した変数) にデータを移すのは、I/Oに比べ非常に高速ですので、このプログラムのI/Oは、少なくとも理論上は、高速になっているはずです。ただ実際には、ハードディスク自体とか、コントローラーとか、カーネルのディスク・ドライブ・デバイス・ドライバーなど、いろいろなレベルでバッファリングが行われている可能性がありますので、このプログラムで行っているバッファリングに大した効果がない可能性もあります。プログラムで setvbuf() を使用すると、むしろコードの実行を遅くしている可能性すらあります。結局ここでの教訓は、性能を改善する作業は、一筋縄ではいかない複雑な問題であり、変更してみた場合には、必ずテストして、効果があるかどうか確認する必要があるということです。

コメントend main loop以下の部分
開始ページまたは終了ページが、実際の総ページ数よりも大きくなかったか確認し、もしそうなら、その旨のメッセージを表示します。また、入力ストリームにエラーがなかったかチェックし、もしそうなら、メッセージを表示します。最後に、入力ストリームをクローズし、出力ストリームをフラッシュし、出力ストリームがパイプだった場合には、 pclose() 関数を使って、パイプをクローズします。パイプをクローズするとlpプロセスにEOFが送られることになり、それによってlpプロセスは終了します。

出力ストリームがstdoutで、それがファイルまたはパイプにリダイレクトされていなかった場合には、指定されたページ範囲は画面に表示されることになります。

出力ストリームがstdoutで、それがファイルにリダイレクトされていた場合には、指定されたページ範囲は、そのファイルに書き出されることになります。

出力ストリームがstdoutで、それが (コマンド・ライン・レベルで) 別のプロセスにパイプされた場合、指定されたページ範囲は、そのプロセスへの入力となります。たとえば、出力を1ページずつ表示し、前後にスクロールすることが可能なlessというページ表示ツール (pager) に出力をパイプすることも可能です。

出力ストリームが (プログラムの中から -dDestinationオプションと popen() を使って) lpにパイプされていた場合、指定されたページ範囲は、指定の印刷出力先に出力されることになります (その出力先が有効になっているものとして)。


Selpgの使い方

以下には、実際的な selpg コマンド文字列の例を挙げておきます。上でこれまで解説してきた原則が、エンド・ユーザーによってどのように実現されるのかを示すものです。

  1. $ selpg -s1 -e1 input_file

    input_fileの第1ページをstdoutに書き出します。リダイレクションもパイプも使われていませんので、stdoutは画面になります。

  2. $ selpg -s1 -e1 < input_file

    結果は、例1と同じになります。ただし、この例では、selpgは、明示的に指定されたファイル名の引数からではなく、シェル / カーネルによってinput_fileから読み込むようにリダイレクトされたstdinから読み込みを行います。入力の第1ページが画面に書き出されます。

  3. $ other_command | selpg -s10 -e20

    other_commandのstdoutが、シェル / カーネルによって、selpgのstdinにリダイレクトされます。ページ10~20がselpgのstdout、すなわち画面に書き出されます。

  4. $ selpg -s10 -e20 input_file >output_file

    Selpgは、ページ10~20を、そのstdoutに書き出します。このstdoutは、シェル / カーネルによってoutput_fileにリダイレクトされます。

  5. $ selpg -s10 -e20 input_file 2>error_file

    Selpgは、ページ10~20を、そのstdout、すなわち画面に書き出します。エラー・メッセージは、シェル / カーネルによってerror_fileにリダイレクトされます。2と > の間に空白を入れてはならないことに注意してください。これは、シェルの構文の問題です (man bashまたはman sh参照)。

  6. $ selpg -s10 -e20 input_file >output_file 2>error_file

    Selpgは、ページ10~20を、そのstdoutに書き出します。stdoutは、output_fileにリダイレクトされます。selpgがstderrに書き出すものは、すべて、error_fileにリダイレクトされます。この起動方法は、input_fileが大きい場合に有効です。selpgが完了するまで、ただ待っているのが嫌で、出力もエラーも保存したいという場合です。

  7. $ selpg -s10 -e20 input_file >output_file 2>/dev/null

    Selpgは、ページ10~20をそのstdoutに書き出します。stdoutは、output_fileにリダイレクトされます。selpgがstderrに書き出すものは、すべて、/dev/null、すなわちnullデバイスにリダイレクトされます。つまり、エラー・メッセージは棄てられることになります。デバイス・ファイル /dev/nullは、それに対して書き出されたすべての出力を破棄し、それから読み出しを行った場合には、即座にEOFを返します。

  8. $ selpg -s10 -e20 input_file >/dev/null

    Selpgは、ページ10~20をそのstdoutに書き出しますが、書き出されたものは棄てられます。エラー・メッセージは画面に表示されます。selpgをテストしたいときに、(何らかの理由で) 通常の出力ではなくエラー・メッセージだけを見たいというような場合に、こういう使い方をすることがあります。

  9. $ selpg -s10 -e20 input_file | other_command

    selpgのstdoutは、シェル / カーネルによって透過的にリダイレクトされてother_commandのstdinとなり、そこにページ10~20が書き出されます。other_commandは、たとえばlpとすることができ、その場合、出力はシステムのデフォルトのプリンターに印刷されることになります。あるいは、other_commandをwcとした場合には、指定されたページ範囲に含まれる行数、単語数、文字数が表示されることになります。stdinから読み込みを行うことのできるコマンドなら、どんなコマンドでもother_commandに使うことができます。その場合でも、エラー・メッセージは、画面に表示されます。

  10. $ selpg -s10 -e20 input_file 2>error_file | other_command

    エラー・メッセージがerror_fileに表示される点が違うだけで、他は上の例9と同じです。

上で、stdoutまたはstderrのリダイレクションを使っている例では、> を >> に置き換えると、ターゲット・ファイルは、上書きされたり (ファイルが存在する場合)、新規に作成される (ファイルが存在しない場合) のではなく、(出力やエラー・データが) アペンドされるようになります。

以下に示す例も、(例外が1つある以外) すべて、上に示したリダイレクションやパイプと組み合わせることができます。以下の例でリダイレクションやパイプの機能を使用していないのは、すでに上で、充分紹介したと考えたからです。例外は、-dDestinationオプションを指定してselpgを起動する場合には、出力のリダイレクションまたはパイプは利用できない、という点です。この場合でもstderrのリダイレクトまたはパイプは行うことができるのですが、標準出力がなくなってしまうため (標準出力は popen() 関数を使って内部的にlpプロセスにパイプされる)、stdoutに対してリダイレクトまたはパイプを使用することはできません。

  1. $ selpg -s10 -e20 -l66 input_file

    ページ長を66行に設定していますので、selpgは、66行単位でページ分割されているかのように、入力を処理することになります。ページ10~20がselpgのstdout、すなわち画面に書き出されます。

  2. $ selpg -s10 -e20 -f input_file

    ページがフォーム・フィードで区切られているものとされます。ページ10~20がselpgのstdout、すなわち画面に書き出されます。

  3. $ selpg -s10 -e20 -dlp1 input_file

    ページ10~20がコマンドlp -dlp1にパイプされます。その結果、出力がプリンターlp1に印刷されることになります。

    最後に、Linuxシェルの別の機能を示す例を紹介します。

  4. $ selpg -s10 -e20 input_file > output_file 2>error_file &

    この例では、プロセスを「バックグラウンド」で実行できるというLinuxの強力な機能を利用しています。上のように起動した場合、まず1234などのプロセスID (pid) が表示され、その後少ししてからシェル・プロンプトが現れ、シェルに次のコマンドをタイプできるようになります。同時に、selpgプロセスはバックグラウンドで実行され、stdoutとstderrは両方ともそれぞれのファイルにリダイレクトされます。こうすることで、selpgを実行している間も他の作業を進めることができるという利点が得られます。

    プロセスがまだ実行中なのか完了したのかは、psコマンド (Process Statusという意味) を実行することで確認できます。このコマンドを実行すると、シェル自身を含め、このシェル・セッションで始動されたプロセス1個につき1行ずつ情報が表示されます。selpgがまだ実行中なら、selpgについての行が確認できるはずです。実行中のselpgプロセスは、コマンドkill -15 1234によって、強制終了することもできます。これでうまくいかない場合は、kill -9 1234を試してみてください。警告: 重要なプロセスにこのコマンドを実行するときには、まずman killを読んでからにしてください。


システム・コールとライブラリー関数 -- 歴史的なことも少し

Linuxのシステム・コールとCのライブラリー関数にはどんな違いがあるのでしょうか。以下、少し解説してみたいと思います。

まず、両方とも、もともとのCにおける意味として「関数」です。すなわち、個別に定義されるコードの断片で、どこかで定義され、名前で呼び出すことができ ( スコープの中にあるコードのどこか他の箇所から何度でも呼び出せる)、引数を渡すことができ、値を返すことができる、という意味においてです。

歴史的なことについての興味深いメモ: Linux OSのほとんどの部分はCで記述されており、(カーネルやデバイス・ドライバーを記述する場合のような) OSレベルのプログラミングに関しても、システム・コールを利用するアプリケーション (RDBMSやネットワーク・ツールやビジネス用のカスタム・アプリケーションなど) を作成する場合のLinuxシステム・プログラミングに関しても、CはLinuxの「ネイティブな」言語だと言えます。

Linuxが強力なOSであり開発環境であるのは、このことが1つの理由となっています。開発者であるみなさんは、メモリー管理、プロセス管理、ファイル / ディレクトリー管理、デバイス管理、ネットワーキング、スレッドなど、OSのさまざまな機能に、ただで、かつ透明な形でアクセスすることができます。みなさんが行うべきことは、使いたい呼び出しを宣言しているヘッダー・ファイルをインクルードして、(場合によっては) それらの呼び出しを実装しているライブラリーをコードにリンクすることだけです。

これは、UNIXとCが、もともと、ベル研究所にいたほとんど同じグループの人々によって開発されたものであるということで、部分的には、歴史的な偶然によるものです。実際、Cは、システム・プログラミング用の高級言語として、とりわけオペレーティング・システム作成用に開発されたものです。それ以前のオペレーティング・システムは、ほとんどが、アセンブリー言語で記述されてきていたのに対して、UNIXの2番目 (かそのあたり) のバージョン以降は、ターゲット・プラットフォームのアセンブリー言語で記述するしかない、ハードウェアに依存するごくわずかな比率のコードを除き、ほとんどの部分がCで記述されました。

OSのほとんどを高級言語で記述する能力が、UNIX/Linuxがかくも普及し、成功を収めた理由の1つでした。研究によると、他の条件がまったく同じなら、プログラマーの生産性は、コーディングに使っている言語のレベルには関係なく、すなわちアセンブリー言語であろうが高級言語であろうが、1日あたりのコードの行数に関して、だいたい同じだとされています。平均して、高級言語の1行は、アセンブリー言語のコード換算でかなりの行数になります。したがって、同じ量の機能を記述する場合、高級言語で記述するほうがアセンブリー言語よりも少ない時間で済みます。もちろん、高級言語で記述したコードのほうが、デバッグや保守がはるかに簡単であることは言うまでもありません。

第2に、Linuxのシステム・コールは、基本的に、1個の関数であり、オペレーティング・システムのコードの一部となっているということです。システム・コールは、カーネルのコアそのものの一部になっていることもあれば、カーネルに静的あるいは動的にリンクされるデバイス・ドライバーの一部となっていることもあります。したがって、みなさんのコードから、たとえば、みなさんのプログラムの中の foo() という関数からシステム・コールを呼び出す場合、実際には、カーネル内のルーチンを呼び出しているわけです。その結果、「ユーザー・スペース」から「カーネル・スペース」へのコンテキスト・スイッチというものが起こることになります。これは、単なるご参考までの情報ですが。呼び出され方に関して言えば、システム・コールとライブラリー関数に違いはありません。Cのライブラリー関数は、それ自体はOSコードの一部ではありません。ライブラリー関数は、基本的にはユーザー・スペースで実行されますが、その実装部分の一部がカーネル・スペースで実行されていることもあります。これについては、次の点とも関係してきます。

第3に、Cのライブラリー関数は、下支えとなるシステム・コールと連携していることもあれば、そうでない場合もあります。関数の中には、その機能を果たすためにシステム・コールを使用する必要のあるものもあれば、そうでないものもあります。

システム・コールとは関係のないライブラリー関数の例としては、string.hで宣言されている strlen() があります。OSの機能は何も必要としません。この関数の処理内容は、単に、文字列の終わりを示す終端文字NULLに達するまで文字列をたどっていき、1文字ごとにカウンターを1つ増加することです。Cの文字列は、終端文字NULL (ASCII 0) を添えた文字の配列にすぎませんでした。そして最後に、 strlen() は、カウンターの値を返します。これは、まったくOSとは独立なコードで、Linuxであれ何であれ、Cのすべてのプラットフォームで同じように機能します。

システム・コールと連携するライブラリー関数の例としては、stdio.hで宣言されている fopen() 関数があります。この関数は、ファイルをオープンする上級レベルの方法です。ただし、Linuxでファイルをオープンするにはシステム・コール open() を使うより他にありませんので、 fopen() は、その実装部分で open() を利用します。同様に、 printf()fprintf() などの関数も、それぞれの機能を果たすために、システム・コール write() を呼び出します。


まとめ

今回の記事は、Linuxコマンド・ライン・ユーティリティーの自作にチャレンジするための充分な内容になっているはずですが、さらに研究を深めたい方は、以下の 参考文献 に示した参考意見を参照してください。


参考文献

  • 本稿で言及したソース・コードはダウンロードできます。
    • selpg.c : selpgのCのコード
    • showsyserr.c : ヘルパー・ユーティリティーのCのコード
    • makefile : selpg構築用のメイクファイル
    • mk : makefileを実行するためのスクリプト。 chmod +rx mk として実行可能なファイルにした後であれば、 mk とタイプするだけで実行できます


  • stdiot 、および本稿で使用したシステム・コールや関数については、manページを参照してください。その他のことについても、各manページのSEE ALSOの項目に示されている他のmanページで調べることができます。

  • getopt のmanページ、 man 3 getopt も参照してください。この3は、manページのセクションを指します。セクション3は、Cのライブラリー関数についての説明です。3を指定しなければならないのは、manページのセクション1 (コマンド) にもgetoptがあり、セクションを指定しないと、デフォルトで第1セクションが表示されるためです。本稿でコマンド・ライン引数を手作業で解析するようにしたのは、今回のものが比較的単純だったことと、そうした手法を紹介したかったためです。しかし、もっと複雑な引数の解析を行う場合には、 getopt() 関数を調べてみるとよいかもしれません。ある程度作業が楽になるかもしれません。

  • assert()lpcancellpstatps 、および kill のmanページを参照してください。

  • pipe()dup()open()close()exec() 、および fork() のmanページを参照してください。 popen() は、 pipe()fork() 、および exec() を使って実装されています。注意: pipe() の使い方は、 popen() に比べ複雑です。Marc J. Rochkind著のAdvanced Unix Programming (Prentice-Hall, Inc., 1986) など、pipe() の正しい使い方を説明している良い参考書を参考にしてください。

  • bash、cshなど、自分の使用しているシェルのmanページを参照してください。コマンド・ラインやスクリプトで、あるいはユーザー・プログラムとの間のやりとりの中で、シェルの強力な機能を利用する方法を研究してください。そうすれば、みなさんのソフトウェア開発の生産性は多いに高まるはずです。

  • 今回紹介したプログラム以外にも、いろいろなLinuxユーティリティーのソース・コードをダウンロードして調べてみてください。

  • Brian W. Kernighan、Rob Pike著のThe UNIX Programming Environment (Prentice-Hall, Inc., 1984) を読むとよいでしょう。少し古い本ですが、ユーザーにとっても開発者にとっても、UNIXやLinuxの入門書として最もすぐれた本の1つです。この本は、これまで私が目にしてきた他の本とは違い、UNIX / Linuxのパワーを解説しています。UNIXの基本的な使い方、コマンド、フィルター (コマンド・ライン・ユーティリティーに対する別の呼び方)、sed、awk、grep、シェル・プログラミング、およびCプログラミングが解説されています。傑作であり、必読の書です。

  • Brian W. Kernighan、Dennis M. Ritchie著のThe C Programming Language (Prentice-Hall, Inc., 1988)、(邦訳「プログラミング言語C」) をまだ読まれていない方は、読むとよいでしょう。C言語のバイブルです。内容が難しいと思われる方は、初心者向けの本を読むのもよいでしょう。ただし、後で必ずこの本を読むようにしてください。この本を読めば、Cを縦横無尽に使用できるようになるはずです。CとUNIXの統合について1章か2章設けられています。また、コマンド・ライン・ユーティリティーや引数処理などの例も数多く示されています。この本も、傑作であり、必読書です。

  • developerWorks のLinuxゾーンには、他にも Linux関係の記事が多数 掲載されています。

著者について

Vasudev Ramは、プロジェクト管理、ソフトウェア開発方法、C、UNIX、Java、およびデータベースの開発と管理の経験を備えた自営のソフトウェア・コンサルタントです。HCL Hewlett-Packard (現在のHCL Infosystems) にシステム・エンジニアおよびコンサルタントとして、Infosys Technologies Limitedにシステム・アナリストおよびプロジェクト・マネージャーとして勤務した経験があります。Vasudevのメール・アドレスは、vasudevram@yahoo.com です。

不正使用の報告のヘルプ

不正使用の報告

ありがとうございます。 このエントリーは、モデレーターの注目フラグが設定されました。


不正使用の報告のヘルプ

不正使用の報告

不正使用の報告の送信に失敗しました。


developerWorks: サイン・イン


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。 プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。 お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

表示名をお選びください

developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

(半角英数字で3文字以上31文字以下にする必要があります)


「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 利用条件

 


この記事を評価する

コメント

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Linux
ArticleID=253111
ArticleTitle=Linuxコマンド・ライン・ユーティリティーの開発
publish-date=06012002
author1-email=vasudevram@yahoo.com
author1-email-cc=

タグ

Help
このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。

スライダーバーを使用することで、より多く(少なく)タグを表示します。

人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。

マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。

このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。