青木 峰郎 SBクリエイティブ 2017-09-22
こんにちは。
最近、ふつうのLinuxプログラミング 第2版を読んでいるので知識の定着のために学んだ内容を要約したメモを書きます。
このエントリは完全な個人のメモです。
お勉強のためにこの本を読んでいるので、内容を覚えるためにSummarizing(サマライジング)を行います。
6章の内容はこちら。
amistad06-a.hatenablog.com
第7章 headコマンドを作る
この章で説明すること
headコマンドの作成を通して、コマンドライン引数の取り扱いやgdbを使用したデバッグ手法、ストリームを扱うAPIなどを説明する。
atoi(3), atol(3)
文字列を数値型に変換する。
atoiはint型に、atolはlong型に変換する。
文字列内に整数が含まれていない場合や、エラーが発生した場合は0を返す。
簡単なheadコマンドを作る
以下にソースコードを記載します。
本の中に書いてあるソースをそのまま写経しています。
このheadコマンドの仕様は以下の通り。
・処理対象は標準入力のみ
・表示したい行数は最初のコマンドライン引数で指定する。
#include <stdio.h>
#include <stdlib.h>
static void do_head(FILE *f, long nlines);
int main(int argc, char *argv[]){
// コマンドライン引数で行数指定がされていない場合エラー
if(argc != 2){
fprintf(stderr, "Usage: %s n\n", argv[0]);
exit(1);
}
do_head(stdin, atol(argv[1]));
exit(0);
}
static void do_head(FILE *f, long nlines){
int c;
// 表示する行数が0以下ならリターン
if(nlines <= 0){
return;
}
while((c = getc(f)) != EOF){
if(putchar(c) < 0){
exit(1);
}
if(c == '\n'){
nlines--;
if(nlines == 0){
return;
}
}
}
}
詳細な処理の流れは割愛します。
main関数の中ではdo_head関数にストリームと表示したい行数を渡しています。
do_head関数の中では指定された行数だけストリームから標準出力に出力します。
行の判定は'\n'で行っています。
※プロトタイプ宣言をせずにmain関数の前に関数を書く方がいいという人もいる。ファイルを上から読んでいったときに、読みやすい。プロトタイプ宣言をできるだけしないというルールでプログラムを書くと結果読みやすくなる。
※putcharの戻り値判定でなぜEOFを使わないのかわからない。
APIの選択
headコマンドは行を扱うコマンドなのに、上記のサンプルプログラムでは何故fgets()を使わずにgetc()を使うのか。
著者は以下の3つの理由を挙げている。
・getc()ならバッファを用意する必要がない
・fgets()を使うと、相当工夫がない限り行の長さが制限される
・getc()でも困らない
この本はこういったプログラムを書く時の思考プロセスなんかも書いてあって本当に勉強になります。
※ちなみに、getc()とputchar()を使った場合処理が遅くなるデメリットがある。
ファイルも扱えるようにする
先ほどのプログラムを改良し、ファイルも扱えるようにします。
#include <stdio.h>
#include <stdlib.h>
static void do_head(FILE *f, long nlines);
int main(int argc, char *argv[]){
long nlines;
if(argc < 2){
fprintf(stderr, "Usage: %s n [file file...]\n", argv[0]);
exit(1);
}
nlines = atol(argv[1]);
if(argc == 2){
do_head(stdin, nlines);
}else{
int i;
// argv[0]は実行ファイル名
// argv[1]は行数指定
// argv[2]以降がファイル名
for(i = 2; i < argc; i++){
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
do_head(f, nlines);
fclose(f);
}
}
exit(0);
}
static void do_head(FILE *f, long nlines){
int c;
// 表示する行数が0以下ならリターン
if(nlines <= 0){
return;
}
while((c = getc(f)) != EOF){
if(putchar(c) < 0){
exit(1);
}
if(c == '\n'){
nlines--;
if(nlines == 0){
return;
}
}
}
}
コマンドライン引数の数が2以下の場合は先ほどのプログラムと同等です。
コマンドライン引数が3以上の場合(コマンドライン引数でファイル名が与えられた場合)、ファイルに繋がるストリームをopenし、そのストリームから出力するようになっています。
オプションとは
コマンドのオプションにはパラメータを取るオプションと取らないオプションがある。
パラメータを取らないオプションは
ls -ltr
のようにまとめて記載することができる。
パラメータを取るオプションはオプションとパラメータをくっ付けて書くことが可能。
例えば、
head -n5
と書くことができる。
また、以下のようなものをロングオプションと呼ぶ。
--version
また、オプション以外にも「-」や「--」は特別扱いされることが多い。
「-」は標準入力から入力、「--」はオプションの解析を終了という意味がある。
オプションの解析をすべて自分で解析するのは大変なので、オプション解析用のAPIを使用する。
getopt(3)
ショートオプションだけを認識するAPI。
以下のプログラムで使い方を説明する。
#include <stdio.h>
#include <unistd.h>
int main(int argc, char *argv[]){
int opt;
// getopt()は常にループと一緒に使用する
// getopt()は呼び出すたびに「次の」オプションを返す
// オプションがなくなったら-1を返す
// 第三引数には解析したいオプションを渡す
// パラメータを取るオプションは後ろに「:」をつける
// パラメータはグローバル変数char *optargで取得できる
// 詳細はmanを参照
while((opt = getopt(argc, argv, "af:tx")) != -1){
switch(opt){
case 'a':
// process for option '-a'
break;
case 'f':
// process for option '-f'
break;
//
// continue if necessary
//
case '?':
// process for invalid option
break;
}
// main program
}
}
※上のプログラムでtかxを渡したらどうなるのか気になるところ。
getopt_long(3)
この関数はgetopt()の全ての機能に加えて、ロングオプションを解析することができる。
基本的な使い方はgetopt()とほぼ同じ。
第4引数でロングオプションの定義を渡す。
詳細な使用方法はmanを参照。
オプションを扱うheadコマンド
先ほど作ったheadコマンドでオプションを扱えるようにしたプログラムが以下。
仕様は以下の通り。
・-nで行数の指定が可能
・-nのロングオプション--lines
・簡単なヘルプメッセージを表示するオプション--help
#include <stdio.h>
#include <stdlib.h>
// getopt_longを使用するためのdefineとinclude
#define _GNU_SOURCE
#include <getopt.h>
static void do_head(FILE *f, long nlines);
#define DEFAULT_N_LINES 10
// long option定義の配列
// 最後は全て0にする必要がある
static struct option longopts[] = {
{"lines", required_argument, NULL, 'n'},
{"help", no_argument, NULL, 'h'},
{0, 0, 0, 0}
};
int main(int argc, char *argv[]){
int opt;
long nlines = DEFAULT_N_LINES;
while((opt = getopt_long(argc, argv, "n:", longopts, NULL)) != -1){
switch(opt){
case 'n':
// グローバル変数からパラメータを取得
nlines = atol(optarg);
break;
case 'h':
fprintf(stdout, "Usage: %s [-n LINES] [FILE ....]\n", argv[0]);
exit(0);
case '?':
// hオプションのときとは出力先を変えている
fprintf(stderr, "Usage: %s [-n LINES] [FILE ....]\n", argv[0]);
exit(0);
}
}
// オプションの解析が終わった時点ではグローバル変数optindは
// オプションではない最初のコマンドライン引数を指している
if(optind == argc){
do_head(stdin, nlines);
}else{
int i;
for(i = optind; i < argc; i++){
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
do_head(f, nlines);
fclose(f);
}
}
exit(0);
}
static void do_head(FILE *f, long nlines){
int c;
// 表示する行数が0以下ならリターン
if(nlines <= 0){
return;
}
while((c = getc(f)) != EOF){
if(putchar(c) < 0){
exit(1);
}
if(c == '\n'){
nlines--;
if(nlines == 0){
return;
}
}
}
}
説明はプログラム内のコメントを参照。
gdbを使ったデバッグ
gdbというデバッガの説明。
私は結構使ったことがあるので詳細は割愛。
以下のgdbコマンドの説明と、デバッグ手順の説明がありました。
・breakpoint(b)
・run(r)
・backtrace(bt)
・frame(f)
・list(l)
・print(p)
・quit(q)
※括弧の中のは省略形
詳細はマニュアルを参照。
練習問題
1. 第6章の練習問題で作った'\t'や'\n'を可視化する機能を、catコマンドのオプションとして使えるようにしなさい。
サポートサイトの解説を見る前に書いたソースコードを載せます。
#include <stdio.h>
#include <stdlib.h>
// difine and include to use getopt_long()
#define _GNU_SOURCE
#include <getopt.h>
// long optionの設定
static struct option longopts[] = {
// --print は -p と同じ動作
{"print", no_argument, NULL, 'p'},
{0, 0, 0, 0}
};
int main(int argc, char *argv[]){
int opt;
int flg = 0;
// long option の解析
while((opt = getopt_long(argc, argv, "p", longopts, NULL)) != -1){
switch(opt){
case 'p':
// p optionがついてたときの処理
flg = 1;
break;
case '?':
printf("unknown option.\n");
exit(1);
default:
break;
}
}
for(int i = optind; i < argc; i++){
int c;
FILE *f;
f = fopen(argv[i], "r");
if(!f){
perror(argv[i]);
exit(1);
}
while((c = fgetc(f)) != EOF){
if(flg){
// p optionがついてた場合、tabや改行を可視化
switch(c){
case '\t':
if(putchar('\\') < 0){
exit(1);
}
if(putchar('t') < 0){
exit(1);
}
break;
case '\n':
if(putchar('$') < 0){
exit(1);
}
default:
if(putchar(c) < 0){
exit(1);
}
}
}else{
if(putchar(c) < 0){
exit(1);
}
}
}
fclose(f);
}
exit(0);
}
※ネストが深いので適切に関数を分割するべき。その際に適切な関数名を付けるとコメント書くよりも読みやすくなる。
2. ファイルの最後の数行を出力するtailコマンドの実走を考えなさい。ただし、出力する行数は固定で構いません。(少し難しい)
これは少しカンニングしました。
以下、書いたソースコードを載せます。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define LINES 10
#define BUFSIZE 100
static void do_tail(char *filename){
int i, lines = 0;
char buf[LINES][BUFSIZE];
FILE *f;
// fprintf(stdout, "top of do_tail\n");
f = fopen(filename, "r");
if(!f){
// ファイルを開くのに失敗したらプログラムを終了
exit(1);
}
// fprintf(stdout, "fopen() success.\n");
while(fgets(buf[lines % LINES], BUFSIZE, f)){
lines++;
}
// for debug
//for(int i = 0; i < LINES; i++){
// fprintf(stdout, "%s", buf[i]);
//}
if(lines < LINES){
// ファイルが10行以下の場合
for(i = 0; i < lines; i++){
fprintf(stdout, "%s", buf[i]);
}
}else{
// ファイルが10行以上の場合
for(i = 0; i < LINES; i++){
fprintf(stdout, "%s", buf[lines++ % LINES]);
}
}
fclose(f);
return;
}
int main(int argc, char *argv[]){
// コマンドライン引数でファイルが指定されていない場合エラー
if(argc != 2){
// fprintf(stderr, "Usage: %s filename.\n", argv[0]);
exit(1);
}
// fprintf(stdout, "main before call do_tail().\n");
do_tail(argv[1]);
exit(0);
}
※カンニング前は10行以下のファイルを読んだ時の動作を失念していた。
※ロジックの前提(今回の場合は10行のリングバッファに使うアイディア)を崩したときの動作を想定する癖をつけるべき。
7章の内容はこんな感じです。
8章以降はこちら。
amistad06-a.hatenablog.com
おわり。
青木 峰郎 SBクリエイティブ 2017-09-22