catコマンド自作(標準入出力ライブラリ)

#はじめに
簡単なオプション付きのcatコマンドを自作しました。 
標準入出力ライブラリ(fopen,fwrite,fread,fclose)を使用しています。


#####使用可能オプション

| オプション | 説明 |
|:-:|:-:|
| -n [--number]  | 行番号を表示  |
| -e [--show-ends]  | 各行末に「$」をつける  |
| -h [--help]  | 使い方の説明  |

#ソースコード
```c:my_cat.c
#include<stdio.h>   /* fprintf,snprintf,perror,fopen,fgets,fputs,fclose*/
#include<stdlib.h>  /* exit */
#define _GNU_SOURCE /* getopt_long */
#include<getopt.h>  /* getopt_long */
#include<string.h>  /* strlen */

static void do_cat_ne(FILE *fp);  /* cat -ne */
static void do_cat_n(FILE *fp);   /* cat -n */
static void do_cat_e(FILE *fp);   /* cat -e */
static void do_cat(FILE *fp);     /* cat */
static void die(const char *str);

#define TRUE 1
#define FALSE 0

int main(int argc,char *argv[])
{
	FILE *fp;
	int i;
	int opt;
	int number = FALSE;
	int show_ends = FALSE;

	static struct option const longopts[] = {
  	{"number"   ,no_argument, NULL, 'n'},
		{"show-ends",no_argument, NULL, 'e'},
		{"help"     ,no_argument, NULL, 'h'},
		{NULL       ,0          , NULL,  0 },
	};

	while((opt = getopt_long(argc,argv, "neh", longopts, NULL)) != -1) {
		switch(opt) {
			case 'n':
				number = TRUE;
				break;
			case 'e':
				show_ends = TRUE;
				break;
			case 'h':
				fprintf(stdout, "Usage: %s [-nE] [file ...]\n", argv[0]);
				exit(0);
			case '?':
				fprintf(stderr, "Usage: %s [-nE] [file ...]\n", argv[0]);
				exit(1);
		}
	}

	if(optind == argc) {
		if(number & show_ends)
			do_cat_ne(stdin);
		else if(number)
			do_cat_n(stdin);
		else if(show_ends)
			do_cat_e(stdin);
		else
			do_cat(stdin);
	} else {
		int i;
		for(i = optind ; i < argc ; i++){
			FILE *fp;

			if((fp = fopen(argv[i], "r")) == NULL) die(argv[0]);
			if(number & show_ends)
				do_cat_ne(fp);
			else if(number)
				do_cat_n(fp);
			else if(show_ends)
				do_cat_e(fp);
			else
				do_cat(fp);

			if(fclose(fp) == EOF) die(argv[0]);
		}
	}
	exit(0);
}

#define LINELEN 2048

static void do_cat_ne(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 3];
	unsigned int count = 0;

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		char *newlinep = &tmp_buf[strlen(tmp_buf) - 1];
		if(*newlinep  == '\n'){
			*newlinep = '$';
			snprintf(buf, sizeof buf, "%d\t%s\n", ++count, tmp_buf);
		} else {
			snprintf(buf, sizeof buf, "%d\t%s", ++count, tmp_buf);
	  }
		fputs(buf, stdout);
	}
}

static void do_cat_n(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 2];
	unsigned int count = 0;

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		snprintf(buf, sizeof buf, "%d\t%s", ++count, tmp_buf);
		fputs(buf, stdout);
	}
}
static void do_cat_e(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 1];

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		char *newlinep = &tmp_buf[strlen(tmp_buf) - 1];
		if(*newlinep == '\n') {
			*newlinep = '$';
			snprintf(buf, sizeof buf, "%s\n", tmp_buf);
		} else {
			snprintf(buf, sizeof buf, "%s", tmp_buf);
		}
		fputs(buf,stdout);
	}
}
static void do_cat(FILE *fp)
{
	char buf[LINELEN];
	while(fgets(buf, sizeof buf, fp) != NULL){
		fputs(buf, stdout);
	}
}
static void die(const char *str)
{
	perror(str);
	exit(1);
}

```
#実行

使い方

```bash
$ (my_cat.c実行ファイル) (オプション) (ファイル)
```
簡単なテストファイルの作成

```bash
$ echo -e "apple\norange" > test.txt
```
my_cat.cの動作。オプションなし

```bash
$ gcc my_cat.c
$ ./a.out test.txt
apple
orange
$ 
```
-n[--number]オプション

```bash
$ ./a.out -n test.txt
1    apple
2    orange
$ ./a.out --number test.txt
1    apple
2    orange
$ 
```
-e[show-ends]オプション

```bash
$ ./a.out -e test.txt
apple$ 
orange$ 
$ ./a.out --show-ends test.txt
apple$ 
orange$ 
$ 
```

-hオプション

```bash
$ ./a.out -h
Usage: ./a.out [-ne] [file...]
$ 
```

#解説

最初にmy_cat.cの中核部分の4つの関数をみていきます。ファイルの内容を読み込んだり、画面へ書き込んだりする関数です。オプションごとに関数を用意しています。

#####do_cat()
オプションが何も指定されなかった時の関数です。引数はストリームです。

```c
#define LINELEN 2048

static void do_cat(FILE *fp)
{
	char buf[LINELEN];
	while(fgets(buf, sizeof buf, fp) != NULL){
		fputs(buf, stdout);
	}
}
```
処理の流れ
1. ストリームからのデータを受け入れるためのバッファ(buf)を用意
2. fgets()で１行ずつ読みこみ、データをbufに格納
3. fputs()でbufのデータを標準出力に書き込む
4. 全て読み終わるまで繰り返し

#####do_cat_e()
オプション-e[--show-ends]が指定された時の関数です。引数はストリームです。

```bash
static void do_cat_e(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 1];

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		char *newlinep = &tmp_buf[strlen(tmp_buf) - 1];
		if(*newlinep == '\n') {
			*newlinep = '$';
			snprintf(buf, sizeof buf, "%s\n", tmp_buf);
		} else {
			snprintf(buf, sizeof buf, "%s", tmp_buf);
		}
		fputs(buf,stdout);
	}
}
```
処理の流れ
1. 処理前のデータを格納するtmp_bufと処理後のデータを格納するbufを用意
2. fgets()で１行ずつ読み込み、データをtmp_bufに格納
3. ポインタnewlinepにtmp_bufの行末の改行コードのアドレスを格納 ※1
4. 改行コードの有無で処理を分岐
5. 改行コードがある場合、改行コードを「$」で置き換え、snprintf()でtmp_bufに改行コードを付け加えた文字列をbufに書き込む
6. 改行コードがない場合、snprintf()でtmp_bufをbufに書き込む。
7. fputs()でbufのデータを標準出力に書き込む
8. 全て読み終わるまで繰り返し

※1 : この場合、strlen(tmp_buf)の戻り値は6なので
　　　&tmp_buf[strlen(tmp_buf) - 1] => &tmp_buf[5]はaddressの0xf0e6を指す

adrress|0xf0e1|0xf0e2|0xf0e3|0xf0e4|0xf0e5|0xf0e6|0xf0e7
---|---|---|---|---|---|---|---|
|char配列|' h '|' e '|' l '|' l '|' o '|' \n '|' \0 '|

#####do_cat_n()
オプション-n[--number]が指定された時の関数です。引数はストリームです。

```bash
static void do_cat_n(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 2];
	unsigned int count = 0;

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		snprintf(buf, sizeof buf, "%d\t%s", ++count, tmp_buf);
		fputs(buf, stdout);
	}
}
```
処理の流れ
1. 処理前のデータを格納するtmp_bufと処理後のデータを格納するbufと行数をカウントするcountを用意
2. fgets()で１行ずつ読み込み、データをtmp_bufに格納
3. snprintf()で行数を表すcountとタブの文字コードを付け加えた文字列をbufに書き込む
4. fputs()でbufのデータを標準出力に書き込む
5. 全て読み終わるまで繰り返し

#####do_cat_ne
オプション-eと-nの両方が指定された時の関数です。引数はストリームです。

```c
static void do_cat_ne(FILE *fp)
{
	char tmp_buf[LINELEN];
	char buf[LINELEN + 3];
	unsigned int count = 0;

	while(fgets(tmp_buf, sizeof tmp_buf, fp) != NULL){
		char *newlinep = &tmp_buf[strlen(tmp_buf) - 1];
		if(*newlinep  == '\n'){
			*newlinep = '$';
			snprintf(buf, sizeof buf, "%d\t%s\n", ++count, tmp_buf);
		} else {
			snprintf(buf, sizeof buf, "%d\t%s", ++count, tmp_buf);
	  }
		fputs(buf, stdout);
	}
}
```
処理の流れ
1. 処理前のデータを格納するtmp_bufと処理後のデータを格納するbufと行数をカウントするcountを用意
2. fgets()で１行ずつ読み込み、データをtmp_bufに格納
3. ポインタnewlinepにtmp_bufの行末の改行コードのアドレスを格納
4. 改行コードの有無で処理を分岐
5. 改行コードがある場合、改行コードを「$」で置き換え、snprintf()で行数を表すcount、タブの文字コード、tmp_bufに改行コードを付け加えた文字列をbufに書き込む
6. 改行コードがない場合、snprintf()で行数を表すcount、タブの文字コードを付け加えた文字列をbufに書き込む。
7. fputs()でbufのデータを標準出力に書き込む
8. 全て読み終わるまで繰り返し

#####die()
次に、エラーが発生した時にエラーメッセージを出力し、処理を終了する関数です。
引数はエラーメッセージに付け加えるための文字列です。

```c
static void die(const char *str)
{
	perror(str);
	exit(1);
}
```

#####main()
最後に、main()関数です。
引数はコマンドラインです。

```c
#define TRUE 1
#define FALSE 0

int main(int argc,char *argv[])
{
	FILE *fp;
	int i;
	int opt;
	int number = FALSE;
	int show_ends = FALSE;

	static struct option const longopts[] = {
  	    {"number"   ,no_argument, NULL, 'n'},
		{"show-ends",no_argument, NULL, 'e'},
		{"help"     ,no_argument, NULL, 'h'},
		{NULL       ,0          , NULL,  0 },
	};

	while((opt = getopt_long(argc,argv, "neh", longopts, NULL)) != -1) {
		switch(opt) {
			case 'n':
				number = TRUE;
				break;
			case 'e':
				show_ends = TRUE;
				break;
			case 'h':
				fprintf(stderr, "Usage: %s [-nE] [file ...]\n", argv[0]);
				exit(0);
		}
	}

	if(optind == argc) {
		if(number & show_ends) 
			do_cat_ne(stdin);
		else if(number)
			do_cat_n(stdin);
		else if(show_ends)
			do_cat_e(stdin);
		else
			do_cat(stdin);
	} else {
		int i;
		for(i = optind ; i < argc ; i++){
			FILE *fp;

			if((fp = fopen(argv[i], "r")) == NULL) die(argv[0]);
			if(number & show_ends)
				do_cat_ne(fp);
			else if(number)
				do_cat_n(fp);
			else if(show_ends)
				do_cat_e(fp);
			else
				do_cat(fp);

			if(fclose(fp) == EOF) die(argv[0]);
		}
	}
	exit(0);
}
```

処理の流れ
1. オプション解析のための構造体の配列を用意
2. オプションを解析するループ。オプションが指定されていれば対応したフラグ(number,show_ends)がtrueになる
3. optindとargcが等しいかどうかで処理を分岐※1
4. 等しい場合、コマンドライン引数にはファイルが指定されていないので、フラグに合わせて各処理を行う※2
5. 等しくない場合、ファイルが指定されているので順番にfopen()でファイルを開き、フラグに合わせて各処理を行う

詳細
※1 : optindにはオプション解析終了直後のコマンドライン引数の数が格納されているので、argcと等しければファイルがコマンドライン引数として渡されていない。
※2 : do_cat()などの各関数には、stdin(標準入力)ストリームを渡す


#おわりに
前回作成の低水準入出力(open,writeなど)を使用して書いたcatコマンドに比べて、標準入出力ライブラリには読み書きのための関数が多くあって便利でした。