はじめての C

C programming note*1
「小文字を 大文字に 変換する」 コードを プログラム cat3 に 組み込んでいきます。

この 2つを 1つの プログラムに まとめて、入力元の ファイルは コマンドで 複数指定でき、かつ オプションスイッチを つけた 場合には 大文字変換機能が 有効になるような プログラムを つくることに しよう。(p103)

そのため、今回は flag という 小さなパーツを 用意します。
まず、値を 2つしか もたない 変数 c_flag を 外部宣言しておきます。 値は 1 と 0 の2つなので、これも 前もって マクロで 定義します。

#define YES 1
#define NO 0

int c_flag = NO;

"YESか NOか" もしくは "ONか OFFか" のように 2つの 値しか とる 可能性のない 変数だ ということを 強調するため #define で 定義した 値を 使うようにする。(p105)

つぎに、コマンドラインを チェックし、オプションが ついた場合には 必要な 処理をする コードを 追加します。

if (argc > 0)
if (strcmp(*argv, "-c") == 0) {
c_flag = YES; /* flag を 1 に */
--argc; /* コマンドラインから "-c" を スキップ */
++argv;
}
}

このやり方では
・ファイル名は -c という名前であっては いけない。
・オプション指定は どのファイル名よりも 前になければ いけない。
という 制限付きだ。 しかし、その制限は それほど 不便では ないだろう。

strcmp() という 標準ライブラリ関数が 使われている。 これは 2つの 文字列が 等しいか どうかを 検査する 関数だ。 等しければ 0 を 返し、等しくなければ 0 以外の 値を 返す。

(ここでは) *argc が "-c" という 長さ 2文字の 文字列と 等しいか どうかを 比べている。(p105)

なお、strcmp() には ヘッダファイル string.h が 必要です。
そして、do_one() に 文字列変換の機能を 組み込んでいきます。 ここで 変数 c_flag が 役にたってきます。

void do_one(FILE *fp)
{
int c;

while )((c = fgetc(fp))( != EOF) {
if (iskanji(c)) {
fputc(c, stdout);
fputc(fget(fp), stdout);
}
else if (islower(c)) {
if (c_flag == YES) /* オプションが あるなら */
fputc(toupper(c), stdout); /* 小文字を 大文字に 変換 */
else
fputc(c, stdout);
}
else
fputc(c, stdout); /* ここは 文字以外の 記号 */
}
}

では、「大文字を 小文字に 変換する」 オプションも 付け加えたいときには、どうしたら いいのか ?
まず、追加する オプションの flag を 外部変数として 宣言します。
int s_flag = NO;
次に、while の ループを 使って オプションを 続けて 指定しても プログラムが 動くように しておきます。
char *s; /* コマンドラインの パラメータ */

while (--argc > 0 && **++argv == '-') {
for (s = *argv + 1; *s != '\0'; s++) {
switch (*s) {
case 'c':
c_flag = YES;
break;
case 's':
s_flag = YES:
break;
}
}
}

この switch文を 含む while の ループでは、コマンドラインの パラメータを 1つ目から 順に 調べ、'-' で 始まる パラメータを 見つけたら、それは ファイル名でなく オプション指定だと 解釈し、中の for文のところへ いく。

'-' で 始まる パラメータでなく 普通の ファイル名を 意味する パラメータに 出会った時点で、この while文の 実行は 終了する。(p117)

(上のコードの)
**++argv == '-'
という部分が 目についたのでは ないだろうか ? 一見 複雑そうに 見えるが 何のことは ない。 2つに 分けて 順を追って 考えれば 納得できるだろう。

1) ++argv .......... argv が 次の要素を 指すようにする。

2) **argv == '-' ... argv が 指す先の ポインタが 指す内容が '-' かどうか。

**argv が むずかしければ、*(*argv) だといえば わかるだろうか。(p117)

また argv++ ではなくて ++argv なのは、最初の 1つ (argv[0]) は コマンド名なので、argv[1] 以降の パラメータを 対象に したいからだ。

つまりは、コマンドラインの 各パラメータの 1文字目を 「'-' かどうか ?」と 順番に 検査するための 記述だ。

これは よく使われる 書き方なので、この while文ごと 覚えてしまったほうが よいかもしれない。(p114)

この while文は とても うまく つくられてますね。
cat3 プログラムだと、main() 関数の 始めのほうで コマンドラインから コマンド名を スキップさせるため、

 --argc;
++argv;
と 書いておかないと ダメだったのを、その部分を 条件文に とり入れて、プログラム自体を 短くしています。
それと、while文を 抜けた時点では 既に コマンドラインから オプション分の パラメータは 除かれていて、後に続く 式や関数を いじる必要が ありません。

if文や この switch文のように、条件によって 実行する 制御の流れを 変えるものを、C では Selection Statement という。 if文は 条件が 「0 である (偽) か、それ以外 (真) か」 で 2通りに 分岐するが、switch文は ケースラベル case label を 使って3つ以上の 場合分けを プログラム上で 表現できる*2

case ラベルは switch文の ブロック部分に 複数 使われる。 case ラベルの 一般形は
case 定数式:
というように、定数式と その後ろに コロン ( : ) を 伴う。 定数式は 整数値である 必要がある。 switch という キーワードの 後ろに カッコで くくって 書いてある 「条件式」の 値が、どの case ラベルの 整数値と 一致するかによって 3つ以上への 場合分け分岐が 行なわれる。

プログラムは 次のとおり、

/* upper3r.c */
#include
#include
#include
#include

#define iskanji(c) (0xa1 <= ((c) & 0xff) && ((c) & 0xff) <= 0xfe)
#define YES 1
#define NO 0

void do_one();
void cant();
int c_flag = NO;
int s_flag = NO;

main(int argc, char **argv)
{
FILE *fp;
char *s;

while (--argc > 0 && **++argv == '-') {
for (s = *argv + 1; *s != '\0'; s++) {
switch (*s) {
case 'c':
c_flag = YES;
break;
case 's':
s_flag = YES;
break;
}
}
}

if (argc == 0)
do_one(stdin);
else {
while (argc--) {
if )((fp = fopen(*argv, "r"))( == NULL)
cant(*argv);
do_one(fp);
fclose(fp);
argv++;
}
}

return 0;
}

void do_one(FILE *fp)
{
int c;

while )((c = fgetc(fp))( != EOF) {
if (iskanji(c)) {
fputc(c, stdout);
fputc(fgetc(fp), stdout);
}
else if (islower(c)) {
if (c_flag == YES && s_flag == NO)
fputc(toupper(c), stdout);
else
fputc(c, stdout);
}
else if (isupper(c)) {
if (c_flag == NO && s_flag == YES)
fputc(tolower(c), stdout);
else
fputc(c, stdout);
}
else
fputc(c, stdout);
}
}

void cant(char *name)
{
fprintf(stderr, "can't open %s\n", name);

exit(1);
}

(両方の オプションを 一度に 使うと、2つとも 無効になります)

*1:「作ってわかる Cプログラミング」

*2:今回は 2つだけ