📝

C言語ファイルを監視して自動コンパイルするCプログラムを開発してみた

2022/05/23に公開約7,300字

はじめに

最近、大学の講義でC言語を学び始めました。

C言語はコンパイラ言語であるため、プログラムの実行にやや手間がかかってしまいます。
そこで、今後のC言語プログラミングの効率化のために、特定のディレクトリ内のCファイルを監視して、Cファイルが保存されたタイミングで自動的にコンパイルや実行を行うC言語のプログラムを開発しました。

使用環境

C言語の開発環境には、Windows10のWSL2でセットアップしたUbuntuを用いました。

  • OS: Ubuntu (ver.20.04)
  • エディター: Visual Studio Code (ver. 1.671)
  • コンパイラ: Gcc

特定ディレクトリやファイルの監視方法について

はじめは、タイムスタンプの比較でファイルの保存を検知しようと考えていましたが、Linuxに今回の目的に合ったinotify APIがあったため、これをディレクトリやファイルの監視方法として使用しました。

inotify APIは、Linuxにおいて、ファイルシステムイベントを監視するための機構を提供するAPIです。
今回は、特定の順番でイベントが発生した際にCファイルのコンパイルや実行を行うようにしています。

プログラム

プログラム
auto_compile.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/inotify.h>

#define INOTIFY_EVENT_SIZE  (sizeof(struct inotify_event))
#define BUF_LEN (1024 * (INOTIFY_EVENT_SIZE + 16))
#define ARRAY_LEN 256

/*拡張子が.cのファイルパスを受け取り、拡張子を取り除いた文字列を返す
*filepath:対象のファイルパス
*result:結果の代入先
*/
void cut_extension_c(const char *filepath, char *result)
{
    strcpy(result, filepath);
    result[strlen(result)-2] = '\0';
}

/*受け取ったパスがディレクトリかを判別する
*path:対象のパス
*戻り値:ディレクトリならば1, それ以外ならば0
*/
int is_dirpath(const char *path)
{
    struct stat stat_buf;
    stat(path, &stat_buf);
    return S_ISDIR(stat_buf.st_mode);
}

/*日付と現在時刻の文字列を返す
*result:結果の代入先
*/
void get_date_and_time(char *result)
{
    char tmp[64];
    time_t t = time(NULL);
    strftime(tmp, sizeof(tmp), "%Y/%m/%d %a %H:%M:%S", localtime(&t));
    strcpy(result, tmp);
}

/*配列の先頭をデキューして、新たに値を一つエンキューする
* arr:対象の配列
*item:エンキューする値
*len:配列の長さ
*/
void queue_push(uint32_t *arr, uint32_t item, int len)
{   
    if (len < 2){
        arr[0] = item;
        return;
    }

    for (int i=1; i < len; i++){
        arr[i-1] = arr[i];
    }
    arr[len-1] = item;
}


/*ファイルの変化によって対応した処理を行う
*fd:ファイルディスクリプタ
*target_dirpath:対象のディレクトリパス
*/
void inotify_read_events(int fd, const char *target_dirpath)
{
    int i = 0, length = 0;
    char *p, buffer[BUF_LEN], date[64], filepath[ARRAY_LEN];
    static uint32_t event_hist[3];
    uint32_t cond1[]={IN_OPEN, IN_MODIFY, IN_CLOSE_WRITE}, cond2[]={IN_OPEN , IN_MODIFY, IN_CLOSE_NOWRITE};

    if((length = read(fd, buffer, BUF_LEN)) < 0){
        return;
    }

    while (i < length) {
        struct inotify_event *event = (struct inotify_event *) &buffer[i];
        snprintf(filepath, sizeof filepath, "%s/%s", target_dirpath, event->name);
        p = strrchr(filepath, '.');
        /*拡張子がcか確認*/
        if (p != NULL){
            if(strcmp(p, ".c")==0 && !is_dirpath(filepath)){

                if (event->mask & IN_CREATE){
                    get_date_and_time(date);
                    printf("[%s] %s が作成されました。\n", date, filepath);
                }

                queue_push(event_hist, event->mask, sizeof(event_hist)/sizeof(u_int32_t));
                /*イベント履歴が特定の並びの場合、コンパイルと実行を行う。*/
                if (memcmp(event_hist, cond1, sizeof(event_hist)) == 0 || (memcmp(event_hist, cond2, sizeof(event_hist)) == 0)){
                    memset(event_hist, 0,  sizeof(event_hist));
                                        char command[ARRAY_LEN*2+30], filepath_no_extension[ARRAY_LEN];
                    cut_extension_c(filepath, filepath_no_extension);                        
                    snprintf(command, sizeof command,"gcc -Wall \"%s\" -lm -o \"%s\"",filepath, filepath_no_extension);
                    get_date_and_time(date);
                    printf("[%s] %s をコンパイルします。\n", date, filepath);
                    system(command);

                    get_date_and_time(date);
                    printf("[%s] %s を実行します。\n", date, filepath);
                    printf("<----実行開始---->\n");
                    system(filepath_no_extension);
                    printf("<----実行終了---->\n");
                }
            }
        }
        i += INOTIFY_EVENT_SIZE + event->len;
    }
}

/*コマンドライン引数がなければ対象ディレクトリをカレントディレクトリに、
*コマンドライン引数が一つあればその引数のディレクトリを対象に処理を行う
*/
int main( int argc, char **argv ) 
{
    int fd, wd;
    char* target_dirpath = "./";

    if(2 < argc) {
        fprintf(stderr, "引数が多すぎます。\n");
        return 1;
    }else if(argc == 2){
        if(is_dirpath(argv[1])){
            target_dirpath = argv[1];
        }
        else{
            fprintf(stderr, "有効なディレクトリパスではありません。\n");
            return 1;
        } 
    }
    printf("対象ディレクトリ: %s\n終了は ctrl + c\n", target_dirpath);

    fd = inotify_init();
    if(fd == -1) {
        perror("inotify_init");
        return 1;
    }
    
    wd = inotify_add_watch(fd, target_dirpath, 
            IN_MODIFY | IN_OPEN | IN_CLOSE_WRITE | IN_CLOSE_NOWRITE | IN_CREATE | IN_ACCESS | IN_ATTRIB);

    while(1) {
        inotify_read_events(fd, target_dirpath);
    }

    inotify_rm_watch(fd, wd);
    close(fd);

    return 0;
}

使用方法

第二引数に対象にしたいディレクトリの相対パスを入力します。
第二引数が空の場合は、カレントディレクトリが対象になります。

$ ./auto_compile <ディレクトリの相対パス>

解説

inotify APIについて

今回使用したinotify APIについて簡単にまとめました。

詳細はこちらをご覧ください。

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/inotify.7.html
#include <sys/inotify.h>
/*inotifyインスタンスを生成して、対応したfd(ファイルディスクリプタ)を返す。*/
int inotify_init(void);

/*inotifyインスタンスと対象ファイルまたはディレクトリのパス、監視するファイルイベントを決める。*/
int inotify_add_watch(int fd, const char *pathname, uint32_t mask);  

/*監視しているファイルやディレクトリでイベントが発生すると、readでそれらのイベント構造体を読みだす。*/
ssize_t read(int fd, void *buf, size_t count);

/*監視対象から削除する。*/
int inotify_rm_watch(int fd, int wd);  

/*リソースを開放する。*/
int close(int fd);

引数の説明

  • int fd … ファイルディスクリプタ
  • char *pathname … ファイルやディレクトリのパス
  • uint32_t mask … inotifyイベントのビットマスク
  • void *buf … バッファー
  • size_t count … 読み込むデータの最大サイズ
  • int wd … 監視対象ディスクリプタ

ファイルイベントの例

ファイルイベントはビットマスクによって識別されます。
以下はファイルイベントの例です。

  • IN_OPEN … ファイルやディレクトリがオープンされたとき。
  • IN_CLOSE_WRITE … 書き込みのためにオープンされたファイルがクローズされたとき。
  • IN_CLOSE_NOWRITE … 書き込みのためではないオープンされたファイルがクローズされたとき。
  • IN_MODIFY … ファイルの中身が変更されたとき。

Cファイルの識別について

inotify APIでは、指定したディレクトリ内のディレクトリやファイルすべてを監視しますが、今回はCファイルのみを対象とするため、strcmpでファイルの拡張子が.cかを確認したり、statでファイルイベントの発生源がディレクトリかファイルを識別をできるようにしました。

また、プログラム実行時のコマンドライン引数でも、監視対象のパスがディレクトリではない場合はエラーを返すようにしました。

コンパイルを行うタイミング

Cファイルのコンパイルを行うタイミングは、Cファイルが保存されたときと定めました。
実際にファイルの保存を行うと、ファイルイベントが以下のような順番で取得されます。

  1. [IN_MODIFY]
  2. [IN_OPEN]
  3. [IN_MODIFY]
  4. [IN_CLOSE_WRITE] ※ファイルの中身に変更がない場合は [IN_CLOSE_NOWRITE]

はじめは [IN_MODIFY] のイベントを取得した際にコンパイルを行うようにしていましたが、
それでは保存時に2回コンパイルが行われてしまったり、ファイルの保存以外でも反応してしまったため、コンパイルを行うタイミングを以下のような順番でイベントを取得した時と定めました。

  1. [IN_OPEN]
  2. [IN_MODIFY]
  3. [IN_CLOSE_WRITE] または [IN_CLOSE_NOWRITE]

イベント履歴を配列に保存して、上記の順番になった際にコンパイル→実行の流れを行うようにしました。

改善点

  • ファイルパスが長すぎると文字型配列からはみ出てしまう。
  • おそらくもっと効率の良いC言語の記述がある。

おわりに

C言語の勉強の一環として制作したこのプログラムですが、実際にコンパイルや実行の手間が減り、効率が上がったように感じました。個人で軽く使う分にはいい感じだと思います。
C言語をもっとうまく使えるようになったらまた改善していきたいです。

ここまで読んでくださりありがとうございました。

GitHubリポジトリのリンク

※TypeScript/ReactかCで夏休みにバイトさせていただけるところを探してます。
ご用件がございましたらTwitterにDMをいただけると幸いです。
ツイッターリンク

参考文献

開発及び記事を書く際に参考にさせていただいたサイトの一覧です。

https://linuxjm.osdn.jp/html/LDP_man-pages/man7/inotify.7.html
http://www.nminoru.jp/~nminoru/programming/file_change_notification.html
https://qiita.com/hidetzu/items/8cdca20b52144820859f
GitHubで編集を提案

Discussion

ログインするとコメントできます