にソースコードを貼る際に即席で作った,ファイルを読み込んで行番号をつけてtxtにするプログラムをちゃんと改良し,まとめておこう,という記事です。(今は上記事で行番号くっつけたのは使ってないですが)
行番号をつけるプログラムというのはC言語初心者に課されがちな課題な気がしますので,そういうのに悩んでる人に見て欲しいなぁという気持ちはあります。あと,コマンドライン入力怖いなぁって思っている人にも見て欲しいです。そんなに怖くないということがわかるかと思います。
即席版
即席で作ったものをそのまま載せています。利用は非推奨
あとで見てちょっと恐ろしいことに気がつきました。最後にfclose()
するの忘れています。まあプログラム終了時にクローズされるのであんまり心配しなくてもいいけど,fclose()
した方がいいには決まっています。
改良版
行番号をつけるプログラムと,実行ファイル作成のためのmakefile
ボリュームが増えました。自作関数とかマクロとか増えて,豪華になりました。
動作
単純な実行結果を載せます。使用方法を参考に,ぜひ使ってみてください。コンパイルして実行ファイルpln
があり,同じところに上のmakefile
もあるとして,そのディレクトリで
$ ./pln makefile
と実行すると
output file name(path) makefile.txt start reading makefile end writing makefile.txt
と表示され,makefile.txt
が生成されます。中身は
1 pln: putLineNumber.c 2 gcc -o $@ $^
となります。これの意味はともかく,確かに上のやつに行番号がくっついています。
なお,読み込むファイル名だけ与えた場合は拡張子を書き換えた(元々無い時はくっつける)ものを出力のファイル名とします。
$ ./pln makefile hoge.txt
のように,出力ファイルの名前を指定することもできます。この場合はmakefile.txt
と同じ内容を,hoge.txt
に書き込みます。
解説
プログラムの頭
プログラムの最初はこうなっています。
#include <stdio.h> #include <string.h> #define STR_MAX 256 #define EXTENSION ".txt" /* 拡張子 */ #define THIS_FILE "putLineNumber.c" #define DO_FILE "pln"
各インクルードファイルと,それを必要とする関数の対応は以下の通りです。
stdio.h | string.h |
---|---|
printf | strrchr |
fopen | strlen |
fclose | strcpy |
fprintf | strncpy |
fgets | strcmp |
マクロについて
STR_MAX
: 読み込むファイルの中身の1行の文字数と,読み込み・書き出しファイルの名前(パス)の最大値です。まあ,1行200文字も書いてるのは小説か何かでしょう。利用対象外です(笑)。ファイル名も,パス含めたってそんな長くならないでしょう。もし必要であれば書き換えて長くしてください。最初はmalloc()
で確保しようかなとも思ったけど,複雑になるのでやめました。EXTENSION
: 注釈の通り,拡張子です。これは,実行時に読み込むファイルだけを指定された場合,出力ファイル名生成時にくっつけます。オリジナルのものに書き換えても特には問題ありません(.ohayashi
とか)。THIS_FILE
: このプログラムの名前が書いてありますが,実行時に出力先にこのプログラム自体を選ばれてると破壊されるので,それを防ぐために使います。DO_FILE
: 実行ファイル名です。これも,破壊を防ぐために使います。
main関数
メイン関数はこんな感じです。
int main(int argc, char *argv[]){ FILE *fpread, *fpwrite; char filename[STR_MAX] = {'\0'}; /* コマンドライン引数は2つか3つ */ if(argc != 2 && argc != 3){ printf("!! command ERROR !!\n"); return -1; } /* 書き込むファイル名を決める */ set_filename(argc, argv, filename); printf("output file name(path)\n\t%s\n\n", filename); fpread = fopen(argv[1], "r"); if(fpread == NULL){ printf("!! read file open ERROR !!\n"); return -1; } fpwrite = fopen(filename, "w"); if(fpwrite == NULL){ printf("!! write file open ERROR !!\n"); return -1; } /* 書き込み */ printf("start reading\n\t%s\n\n", argv[1]); read_write(fpread, fpwrite); printf("end writing\n\t%s\n\n", filename); fclose(fpread); fclose(fpwrite); return 0; }
まず,コマンドライン引数(解説)で情報を受け取りますが,その数は
- 2つ : 実行ファイル名と読み込みファイル名
- 3つ : 実行ファイル名と読み込みファイル名と出力ファイル名
のどちらかですので,argc
を検査しておかしい場合はここで終了します。
そのあと,自作関数set_filename
で出力ファイル名を決めたら,入出力それぞれのファイルをオープンします。読み込みファイル名はargv[1]
に,出力ファイル名はfilename
に入っていますので,それを使います。ファイルの取り扱いについて詳しくは参考のページがオススメです。
そして,自作関数read_write
で読み込みと,行番号を付けた出力をして,ファイルクローズしておしまいです。
自作関数 set_filename()
これは,役割としては出力ファイルの名前を決めるだけですが,中身は少々複雑です。引数は,コマンドライン引数argc
とargv[]
,さらにファイル名を入れる文字列へのポインタset
です。動作は,コマンドライン引数の数によって大きく分かれています。
- コマンドライン引数が2つのとき
このときは,出力ファイル名を生成しなければなりません。今回は,拡張子があればそれをEXTENSION
で定義したものに差し替え,拡張子がなければEXTENSION
で定義したものを引っ付けることにしました。
拡張子の位置を割り出すために,入力されたファイル名で最後に.
が出てくるところを探します。最初はループで先頭から探すように書こうとしていましたが,strrchr()
という関数があるのを発見しましたので使ってみます。これは,文字列の中で指定した文字が最後に出てくるところを教えてくれます。先頭から探すstrchr()
は聞いたことある人が多いかもしれません。僕もstrrchr()
というのは初耳でした。
#include <string.h> char* strrchr( const char *s, int c );
文字列s
の中で文字c
が最後に出てくるところを探します。戻り値はc
へのポインタで,c
が見つからなかったときはNULL
です。
if ( (ret = strrchr(argv[1], '.')) != NULL ) { dot = ret - argv[1]; } else { dot = strlen(argv[1]); }
としています。最後の.
を見つけたときはret
にそのアドレスが入るので,argv[1]
との差し引きで.
以前の文字数dot
がわかります。
どういうことでしょうか。
argv[1]
に入った文字列をtest.c
とするとメモリ上ではこうなっています(アドレスは仮のものです)。
アドレス 15 16 17 18 19 20 21 中身 t e s t . c \0
argv[1]
は,そもそもポインタであり文字列の先頭を指しているのでargv[1] = 15
です。ret
はstrrchr()
の戻り値として19
をもつことになります。char型の変数の大きさ(sizeof(char)
)は1
なので,ret - argv[1]
によって拡張子の前の名前(test
)の長さdot = 4
がわかります。ポインタと配列(文字列)の相互関係については奥深いものがあるので,気になった方は調べてみてください。
もし入力されたファイル名に拡張子がなかった場合はret = NULL
となるのでelse
に飛んで,ファイル名の長さをそのままdot
に入れてしまいます。
そして,strncpy()
関数でファイル名をset
に写します。
strncpy(set, argv[1], dot);
strncpy
関数は文字列s2
から文字列s1
に,n
文字だけコピーします。
#include <string.h> char *strncpy( char * restrict s1, const char * restrict s2, size_t n);
これにより,文字列set
には拡張子の手前まで(先述の例にのっとるとtest
)が入ります。
最後に,後ろに拡張子をひっつけておしまいです。
for(i=0; i<strlen(EXTENSION); i++){ filename[dot+i] = plus[i]; }
これはそんなに難しい話ではないと思います。plus
は,この関数の先頭で宣言と同時に拡張子の文字列EXTENSION
で初期化しています。
ちなみに,文字列set
は,ポインタset
が指す文字列のことで,set
にはmain()
内で以下のように初期化したfilename
を入れるので,コピーとか拡張子のくっつけとかをしていない領域は全てナル文字\0
が入っています。
[main] char filename[STR_MAX] = {'\0'};
こういうふうに初期化をしないと,ナル文字ではない『何か』が入っていてたまに文字化けします。右辺は,= ""
や= {}
でも大丈夫なはずですが,念のため= {'\0'}
としています。
- コマンドライン引数が3つのとき
if( strcmp(argv[2], THIS_FILE) == 0 || strcmp(argv[2], DO_FILE) == 0 ){ printf("!! can not set %s as output file name. !!\n", argv[2]); strcpy(set, "output.txt"); }else{ strcpy(set, argv[2]); }
このときは,set
にargv[2]
を放り込むのが基本ですが,もし書き込み先ファイルargv[2]
にこのプログラム自体や実行ファイルが入力されてしまったら,行数をつけたあとの出力先がそれらになり,ファイルを破壊することになってしまいます。それを防ぐためにif
文で検査しています。一致が発生した場合,出力ファイル名はoutput.txt
にしてしまいます。
自作関数 read_write()
こちらはかなり単純です。引数は入出力のファイルを指すポインタ(読み込み)r
,出力w
です。
void read_write(FILE *r, FILE *w){ char str[STR_MAX] = {'\0'}; /* ファイル内容の収納 */ int cnt = 1;/* 行番号は1から */ while(fgets(str, STR_MAX, r) != NULL){/* ここで\nまで読み込んでいるので */ fprintf(w, "%4d %s", cnt, str);/* ここでは\nいらない */ cnt++; } }
一旦文字列str
に読み込んでから,fprintf()
で出力しています。
fgets()
はここでは,r
の指すファイルから改行文字が出るまで最大STR_MAX -1
文字読み取り,str
の指す文字列に入れています。fgets()
は改行記号まで読み込むのが注意点です。
fprintf()
は,おなじみprintf()
の出力先が標準出力からファイルになったバージョンです。引数の先頭にファイルポインタが増えています。
この際,変数cnt
に入れた行数を文字列の前に出力しています。そして,fprintf()
を使うときは最後に改行文字\n
をよく入れますが,今回は不要です。"%4d %s\n"
としてしまうと,出力結果が1行飛ばしのスカスカになってしまいます。
まとめ
長くなりましたが,改良版の解説は以上です。
今回これを書いていて,コマンドライン引数は怖くない,ということがわかったので,うまく使えるところがあれば使ってあげたいなと思いました。また,文字列操作関数や,ポインタと文字列についてなど改めて勉強になりました。
以下に,コマンドライン引数とmakefileついて軽い説明,あとGistから落としてコンパイルする等細かい使い方を書いています
コマンドライン引数について
Cの入門では,メイン関数をint main(){...
とかint main(void){...
にして,プログラムに情報を与えたい時はscanf()
とかやるのが鉄板ですが,そこを
int main(int argc, char *argv[]){...
とすることで,実行時に同時に情報を与えることができます。argc
は,与えられた引数の数,argv[]
は与えられた文字列配列へのポインタになります。先ほどやった
$ ./pln makefile
を入力したときにプログラムへ渡される情報は,
argc = 2; argv[0] = "./pln"; argh[1] = "makefile";
となっています。
さらに詳しいことは,参考に載せたサイトが(立命館大の授業ページですが検索で出ました)すごくわかりやすいです。
makefileについてほんのちょっと
冒頭の方に,makefileというのを載せました。細かいことはわかっていないのですが,簡単にいうと,コンパイルのコマンドがめんどくさい時に省略形を作っておける,そんな感じだと思います。
再掲
[makefile] pln: putLineNumber.c gcc -o $@ $^
本来,putLineNumber.c
をコンパイルして実行ファイルpln
を作るときのコマンドは(GCCがどうとかは多少あれど)
$ gcc -o pln putLineNumber.c
こんな感じになります。putLineNumber.c
がめんどくさいですね。上キーで入力の履歴を出せるとは言いつつ,間に別のコマンドを打っていたりすると探すのも大変になります。そこで,同じディレクトリにこれ(makefileはファイル名が「makefile」で特に拡張子はないです)をおいておくと
$ make
だけでコンパイルできます。
makefileの3行目gcc -o $@ $^
が,gcc -o pln putLineNumber.c
に化けて入力されるようになっています。$@, $^
はマクロで,ここでは$@ = pln, $^ = putLineNumber
といった風になります。
細かいことを全然書いてないので,なんとなく便利そうだなと思ったらいろいろ調べてみてください。
使用方法
ダウンロードとコンパイル
C言語の使える環境でターミナル(Windowsはコマンドプロンプト?)に
$ git clone https://gist.github.com/12c05562f0e746762c304009606eb92b
と入れると,12c05562f0e746762c304009606eb92b
という名前のフォルダができ,その中にmakefile
とputLineNumber.c
が入ります。12c05562f0e746762c304009606eb92b
に移動して
$ make
とやるとコンパイルされ,実行ファイルpln
が生成されます。
なお,makefile:2: *** missing separator. Stop.
のようなエラーが出た場合は,2行目の最初の空白がタブではなくスペースになってしまっている可能性があります。ここの空白はタブでなくてはいけません。
後は
$ ./pln <入力ファイル名> <出力ファイル名(省略可)>
とすると上でに示したような動作をして,行番号が付いたファイルを生成します。
ファイル名は,plnがあるディレクトリからの相対パスや,絶対パスでも構いません。現在のディレクトリにhoge
フォルダがあってその下のfuga.c
をhoge
内gafu.dat
に出力する際は
$ ./pln ./hoge/fuga.c ./hoge/gafu.dat
こんな感じです。最後の引数を省略すれば当然./hoge/fuga.txt
が生成されます。
カスタマイズのしどころ
ここでは,プログラム自体に手を加えて弄れそうなところを紹介します。
#define STR_MAX
をいじる
1行が長かったり,ファイルパスが長かったりするときはここの値を大きくしましょう。#define EXTENSION
をいじる
拡張子をオリジナルにしたいときは,ここを変えましょう。実行ファイル名を変える
実行ファイル名をpln
以外にしたいときは,putLineNumber.c
の#define DO_FILE "pln"
と,makefile
のpln: putLineNumber.c
の該当箇所をそれぞれ書き換えましょう。1000行未満,あるいは1万行以上のファイルを扱う
read_write()
内で書かれている出力の形式はfprintf(w, "%4d %s", cnt, str);
なので行数が綺麗に揃って出るのは9999行までです。そんなにいらないよって人は例えば%2d
にすれば行頭2つの空白を埋めて,99行までを綺麗に出力できます。 あるいは,%5d
にすれば,10000行のところで1文字分段が下がるようなことがなくなります。
こんなもんですかね。お付き合いいただきありがとうございました。 もう一回載せときますが,スロット表示プログラムの話も見ていってください。関連記事も書く予定なので。
参考
- 作者: 柴田望洋
- 出版社/メーカー: SBクリエイティブ
- 発売日: 2014/08/09
- メディア: 単行本
- この商品を含むブログ (1件) を見る