おはやし日記

テーマ……バイク←プログラミング←旅

コマンドライン入力と,行番号を付加するプログラム [C言語]

これは,nurcesでスロットの記事↓

o-treetree.hatenablog.com

ソースコードを貼る際に即席で作った,ファイルを読み込んで行番号をつけて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()

これは,役割としては出力ファイルの名前を決めるだけですが,中身は少々複雑です。引数は,コマンドライン引数argcargv[],さらにファイル名を入れる文字列へのポインタsetです。動作は,コマンドライン引数の数によって大きく分かれています。

このときは,出力ファイル名を生成しなければなりません。今回は,拡張子があればそれを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です。retstrrchr()の戻り値として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'}としています。

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]);
}

このときは,setargv[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という名前のフォルダができ,その中にmakefileputLineNumber.cが入ります。12c05562f0e746762c304009606eb92bに移動して

$ make

とやるとコンパイルされ,実行ファイルplnが生成されます。

なお,makefile:2: *** missing separator. Stop.のようなエラーが出た場合は,2行目の最初の空白がタブではなくスペースになってしまっている可能性があります。ここの空白はタブでなくてはいけません。

後は

$ ./pln <入力ファイル名> <出力ファイル名(省略可)>

とすると上でに示したような動作をして,行番号が付いたファイルを生成します。

ファイル名は,plnがあるディレクトリからの相対パスや,絶対パスでも構いません。現在のディレクトリにhogeフォルダがあってその下のfuga.chogegafu.datに出力する際は

$ ./pln ./hoge/fuga.c ./hoge/gafu.dat

こんな感じです。最後の引数を省略すれば当然./hoge/fuga.txtが生成されます。

カスタマイズのしどころ

ここでは,プログラム自体に手を加えて弄れそうなところを紹介します。

  1. #define STR_MAXをいじる
    1行が長かったり,ファイルパスが長かったりするときはここの値を大きくしましょう。

  2. #define EXTENSIONをいじる
    拡張子をオリジナルにしたいときは,ここを変えましょう。

  3. 実行ファイル名を変える
    実行ファイル名をpln以外にしたいときは,putLineNumber.c#define DO_FILE "pln"と,makefilepln: putLineNumber.cの該当箇所をそれぞれ書き換えましょう。

  4. 1000行未満,あるいは1万行以上のファイルを扱う
    read_write()内で書かれている出力の形式はfprintf(w, "%4d %s", cnt, str);なので行数が綺麗に揃って出るのは9999行までです。そんなにいらないよって人は例えば%2dにすれば行頭2つの空白を埋めて,99行までを綺麗に出力できます。 あるいは,%5dにすれば,10000行のところで1文字分段が下がるようなことがなくなります。

こんなもんですかね。お付き合いいただきありがとうございました。 もう一回載せときますが,スロット表示プログラムの話も見ていってください。関連記事も書く予定なので。

o-treetree.hatenablog.com

参考

  1. コマンドライン引数

  2. 納得C言語 [第16回]ファイル入出力 - ほぷしぃ

  3. C言語関数辞典 - C言語Tips集 文字列の最後から文字を検索する

  4. C言語関数辞典 - C言語Tips集 文字列をコピーする

トップに戻る

にほんブログ村 IT技術ブログ IT技術メモへ
にほんブログ村

プライバシーポリシー ・お問い合わせはこちら