功能要求#
[类似 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 族⭐