功能要求#
[類似 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,可輸入任意消息
- ① 若輸入包含註釋的多行消息
- 最終打印結果如下
- 多行整合為一行 [同 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;
}
- 只有三種情況:① 無選項;② 有 - m 選項與選項參數;③ 選項不合法
- 頭文件 head.h 見末尾
- 使用測試腳本 test.sh [見末尾] 測試,效果如下:
- 展示了三種情況下的輸出
輸入消息#
利用 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!"
讀取消息#
父進程嘗試讀取消息文件
- 【友好提示】消息文件可能無法打開 [如文件不存在],也可能不包含有效消息 [全是註釋]
- 【分行解析】判斷行註釋的存在;如果輸入了多行有效消息,打印結果時整合到一行,每行消息用空格隔開 [同 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)
- 需要不斷調用,從第二次起傳入的 str 替換為 NULL,每次調用返回一個被分割的字符串,返回 NULL 表示到達 str 末尾
- 效果如下:
- 命令無選項時,在 vim 中輸入下列消息
- 讀取成功後,整合到一行打印
- 註釋會被忽略
刪除消息文件#
為了讓父進程有可能做更多事,這裡 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 族⭐