機能要件#
[git commit -m "msg" に似た機能]
- -m オプションを使用する場合はメッセージを直接表示し、-m オプションを使用しない場合は自動的に vim を開いてメッセージを入力
- 詳細説明
- ① オプションとオプションパラメータ - m "first commit" を含む場合
-
- 関連メッセージを直接表示
-
- ② -m オプションがない場合
- 自動的に vim を開き、ユーザーが "second commit" を入力し、保存して終了した後、関連メッセージを表示
-
- vim で有効なメッセージを入力しなかった場合 [コメントは有効なメッセージに含まれない]
-
- この操作は失敗し、ユーザーに優しく通知
- ① オプションとオプションパラメータ - m "first commit" を含む場合
- [PS]
- 3 つのプロセスが必要な場合があります:親プロセス、vim プロセス、vim が生成したファイルを削除するプロセス
- vim で入力されたメッセージが有効かどうかを判断する必要があります
- 子プロセスが実行され、親プロセスが待機し、データを読み取り、表示 [cat]、rm
最終的な効果#
- 入力./git_commit -m "first commit"
-
- メッセージを直接出力
-
- 入力./git_commit
- まず vim が開き、任意のメッセージを入力できます
- ① コメントを含む複数行のメッセージを入力した場合
- 最終的な印刷結果は以下の通り
-
- 複数行が 1 行に統合され [git の設計に従って]、コメントは無視されます
- ② で vim を終了した場合
-
- メッセージファイルを開けないことを通知
-
- ③ 全てがコメントの場合
-
- メッセージが空であることを通知
-
[PS]
- vim がファイルを開くとき、TYPE=GITCOMMIT を指定できますか?現在 TYPE は空です
- 👉 vim が開くファイル名を COMMIT_EDITMSG に変更すれば、メッセージコードに色付けができます
実装プロセス#
思考フローチャート#
【プロセス分析】
- 後でrmプロセスを作成する必要があるため、親プロセスを直接vimプロセスに置き換えることはできません
- さもなければ、vim を終了するとプログラムが終了します
- 親プロセスは子プロセスを fork し、exec ファミリーでそのプロセスをvimプロセスに置き換えます
- 親プロセスはさらに子プロセスを fork し、rmプロセスに置き換えます
- 親プロセスを直接rmプロセスに置き換えることも可能です
- しかし、新しいプロセスを fork することで、親プロセスが削除状態を監視し、さらに多くの操作を行う可能性があります
コマンドライン引数の取得#
-m オプションをキャッチし、このオプションを使用するにはパラメータが必要です
#include "head.h"
int main(int argc, char **argv) {
if (argc == 1) {
// 1.オプションなし:vimでmsgを入力する必要があります
printf("no msg, need vim.\n");
}
int opt;
while ((opt = getopt(argc, argv, "m:")) != -1) {
switch (opt) {
case 'm': {
// 2.-mオプションとオプションパラメータあり:パラメータを表示
printf("msg: %s\n", optarg);
} break;
default: {
// 3.オプションが不正:優しく通知
fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
exit(1);
}
}
}
return 0;
}
- 3 つのケースのみ:① オプションなし;② -m オプションとオプションパラメータあり;③ オプションが不正
- ヘッダーファイル head.h は末尾にあります
- テストスクリプト test.sh を使用してテストし、結果は以下の通り:
-
- 3 つのケースでの出力を示しています
-
メッセージの入力#
vim を使用してメッセージを入力します
- 親プロセスは fork を使用して子プロセスを作成し、execlp を使用して vim プロセスに置き換えます
- 親プロセスは wait を使用して子プロセスの状態を監視し、ゾンビプロセスの発生を避けます
// メッセージの入力
void input_msg() {
pid_t pid;
int status;
// 親プロセスが子プロセスをコピー
if ((pid = fork()) < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
// 子プロセスをvimプロセスに置き換え
execlp("vim", "vim", "COMMIT_MSG", NULL);
} else {
wait(&status); // 子プロセスの状態を監視
// 親プロセスがvimエラーを通知
if (WEXITSTATUS(status)) printf("vim error!\n");
else printf("input msg completed!\n");
}
return ;
}
- コマンドにオプションがない場合、vim プロセスで COMMIT_MSG ファイルを開きます
- ユーザーはメッセージを入力できます
- vim を終了すると、vim プロセスが終了し、親プロセスが感知できます
-
- 親プロセスが "input msg completed!" を通知
-
メッセージの読み取り#
親プロセスがメッセージファイルを読み取ろうとします
- 【優しい通知】メッセージファイルが開けない可能性があります [ファイルが存在しない場合]、または有効なメッセージが含まれていない可能性があります [全てがコメント]
- 【行解析】行コメントの存在を判断します;複数行の有効メッセージが入力された場合、結果を表示する際に 1 行に統合し、各行のメッセージを空白で区切ります [git の設計に従って]
#define MAX_LENGTH 512
// メッセージを読み取る
void read_msg() {
int fd, flag = 0; // flag:有効なメッセージが読み取られたか
char buff[MAX_LENGTH] = {0}; // 最大MAX_LENGTHバイトのメッセージを読み取る
ssize_t nread;
// メッセージファイルが開けない場合、優しく通知
if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
printf("aborting commit due to msg file can't open.\n");
exit(1);
}
// メッセージファイルを読み取る
if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
char *line;
// 行ごとにメッセージが有効か判断
line = strtok(buff, "\n");
while (line != NULL) {
// 無効なメッセージでない場合 [コメントでない]
if (line[0] != '#') {
flag || printf("msg:"); // 最初の有効メッセージを印刷する際にメッセージプレフィックス:"msg:"を印刷
flag = 1;
printf(" %s", line);
}
line = strtok(NULL, "\n"); // buffの'\0'に達するまで呼び出し続ける
}
flag && printf("\n");
}
// 有効なメッセージが読み取れなかった場合、優しく通知
if (!flag) {
printf("aborting commit due to empty commit msg.\n");
}
close(fd);
return ;
}
- 特別な flag 出力メッセージプレフィックス:"msg:"
- 最大でメッセージファイルの最初の 512 バイトを読み取ります
- ⭐文字列分割関数 strtok——cplusplus
- char* strtok(char* str, const char* delimiters)
-
- 継続的に呼び出す必要があり、2 回目以降は str を NULL に置き換え、毎回呼び出すと分割された文字列が返され、NULL が返されると str の末尾に達します
- 効果は以下の通り:
- コマンドにオプションがない場合、vim で次のメッセージを入力します
- 読み取り成功後、1 行に統合して印刷
-
- コメントは無視されます
メッセージファイルの削除#
親プロセスがさらに多くのことを行えるように、ここで子プロセスを fork し、rm プロセスに置き換えます
- 親プロセスは rm プロセスの状態を監視でき、rm プロセスも失敗メッセージを報告します
// メッセージファイルを削除
void remove_file() {
pid_t pid;
int status;
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
execlp("rm", "rm", "COMMIT_MSG", NULL);
} else {
wait(&status);
// 親プロセスがrm失敗を通知 [rmプロセスも通知があります]
if (WEXITSTATUS(status)) printf("remove msg file error!\n");
}
return ;
}
- メッセージファイルを使用した後、COMMIT_MSG ファイルは残りません
完全なコード#
git_commit.c#
#include "head.h"
#define MAX_LENGTH 512
// メッセージの入力
void input_msg() {
pid_t pid;
int status;
// 親プロセスが子プロセスをコピー
if ((pid = fork()) < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
// 子プロセスをvimプロセスに置き換え
execlp("vim", "vim", "COMMIT_MSG", NULL);
} else {
wait(&status); // 子プロセスの状態を監視
// 親プロセスがvimエラーを通知
if (WEXITSTATUS(status)) printf("vim error!\n");
}
return ;
}
// メッセージを読み取る
void read_msg() {
int fd, flag = 0; // flag:有効なメッセージが読み取られたか
char buff[MAX_LENGTH] = {0}; // 最大MAX_LENGTHバイトのメッセージを読み取る
ssize_t nread;
// メッセージファイルが開けない場合、優しく通知
if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
printf("aborting commit due to msg file can't open.\n");
exit(1);
}
// メッセージファイルを読み取る
if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
char *line;
// 行ごとにメッセージが有効か判断
line = strtok(buff, "\n");
while (line != NULL) {
// 無効なメッセージでない場合 [コメントでない]
if (line[0] != '#') {
flag || printf("msg:"); // 最初の有効メッセージを印刷する際にメッセージプレフィックス:"msg:"を印刷
flag = 1;
printf(" %s", line);
}
line = strtok(NULL, "\n"); // buffの'\0'に達するまで呼び出し続ける
}
flag && printf("\n");
}
// 有効なメッセージが読み取れなかった場合、優しく通知
if (!flag) {
printf("aborting commit due to empty commit msg.\n");
}
close(fd);
return ;
}
// メッセージファイルを削除
void remove_file() {
pid_t pid;
int status;
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) {
execlp("rm", "rm", "COMMIT_MSG", NULL);
} else {
wait(&status);
// 親プロセスがrm失敗を通知 [rmプロセスも通知があります]
if (WEXITSTATUS(status)) printf("remove msg file error!\n");
}
return ;
}
int main(int argc, char **argv) {
if (argc == 1) {
// 1.オプションなし:vimでmsgを入力する必要があります
input_msg();
read_msg();
remove_file(); // ここまで来ることができれば、メッセージファイルは生成されています
}
int opt;
while ((opt = getopt(argc, argv, "m:")) != -1) {
switch (opt) {
case 'm': {
// 2.-mオプションとオプションパラメータあり:パラメータを表示
printf("msg: %s\n", optarg);
} break;
default: {
// 3.オプションが不正:優しく通知
fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
exit(1);
}
}
}
return 0;
}
head.h#
#ifndef _HEAD_H
#define _HEAD_H
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#endif
test.sh#
#!/bin/bash
cmd="./git_commit"
msg="first commit"
echo "1)${cmd}:" | lolcat
${cmd}
echo "2)${cmd} -m \"${msg}\":" | lolcat
${cmd} -m "${msg}"
echo "3)${cmd} -m:" | lolcat
${cmd} -m
echo "3)${cmd} -b:" | lolcat
${cmd} -b
- ./git_commit の git_commit は使用 gcc ... -o git_commit で生成された実行可能ファイル名
参考#
- 主な知識点は『ネットワークとシステムプログラミング』を参考
- 0 コース紹介とコマンドライン解析関数——getopt
- 1 ファイル、ディレクトリ操作と ls の実装の考え方——open、read
- 3 マルチプロセス——fork、wait、exec ファミリー⭐