Functional Requirements#
[Function similar to git commit -m "msg"]
- Directly print the message when using the -m option; automatically open vim for input when the -m option is not used.
- Detailed Explanation
- ① When there are options and option parameters -m "first commit"
- Directly print the relevant message.
- ② When there is no -m option
- Automatically open vim, the user inputs "second commit" inside, and after saving and exiting, print the relevant message.
- If no valid message is entered in vim [comments do not count as valid messages]
- This operation will fail and provide a friendly prompt to the user.
- ① When there are options and option parameters -m "first commit"
- [PS]
- May require 3 processes: parent process, vim process, and process to delete files generated by vim.
- Need to determine if the message entered in vim is valid.
- The child process runs, the parent process waits, reads data, and prints it out [cat], rm.
Final Effect#
- Input ./git_commit -m "first commit"
- Directly output the message.
- Input ./git_commit
- First pop up vim, allowing any message to be entered.
- ① If multiple lines of messages containing comments are entered
- The final printed result is as follows.
- Multiple lines are combined into one line [same as git design], and comments are suppressed.
- ② If directly exit vim with
- Prompt that the message file cannot be opened.
- ③ If all input is comments
- Prompt that the message is empty.
[PS]
- When vim opens the file, can it specify its TYPE=GITCOMMIT? Currently, TYPE is empty.
- 👉 Change the filename opened by vim to COMMIT_EDITMSG, so that the message code has some coloring.
Implementation Process#
Thought Process Flowchart#
【Process Analysis】
- Since it is necessary to create the rm process later, the parent process cannot be directly replaced by the vim process.
- Otherwise, after exiting vim, the program will end.
- The parent process needs to fork a child process, then exec family to replace that process with the vim process.
- The parent process then forks another child process and replaces it with the rm process.
- Directly replacing the parent process with the rm process is also feasible.
- However, forking a new process allows the parent process to monitor the deletion status and possibly perform more operations.
Getting Command Line Parameters#
Capture the -m option; using this option must include a parameter.
#include "head.h"
int main(int argc, char **argv) {
if (argc == 1) {
// 1. No options: need vim to input msg.
printf("no msg, need vim.\n");
}
int opt;
while ((opt = getopt(argc, argv, "m:")) != -1) {
switch (opt) {
case 'm': {
// 2. There is -m and option parameter: print parameter.
printf("msg: %s\n", optarg);
} break;
default: {
// 3. Invalid option: friendly prompt.
fprintf(stderr, "Usage: %s [-m msg]\n", argv[0]);
exit(1);
}
}
}
return 0;
}
- There are only three cases: ① No options; ② There is -m option and option parameter; ③ Invalid option.
- The header file head.h is at the end.
- Use the test script test.sh [see end] for testing, the effect is as follows:
- Demonstrated the output in three cases.
Input Message#
Use vim to input the message.
- The parent process uses fork to create a child process, then uses execlp to replace it with the vim process.
- The parent process uses wait to monitor the child process status and avoid creating zombie processes.
// Input message
void input_msg() {
pid_t pid;
int status;
// The parent process copies a child process.
if ((pid = fork()) < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
// Replace the child process with the vim process.
execlp("vim", "vim", "COMMIT_MSG", NULL);
} else {
wait(&status); // Monitor child process status.
// The parent process prompts vim error.
if (WEXITSTATUS(status)) printf("vim error!\n");
else printf("input msg completed!\n");
}
return ;
}
- When there are no options, the vim process opens the COMMIT_MSG file.
- The user can input the message.
- After exiting vim, the vim process terminates, and the parent process can sense it.
- The parent process prompts "input msg completed!"
Reading Messages#
The parent process attempts to read the message file.
- 【Friendly Prompt】 The message file may not be able to open [e.g., file does not exist], or it may not contain valid messages [all comments].
- 【Line Parsing】 Determine the existence of line comments; if multiple lines of valid messages are input, print the results combined into one line, with each line of messages separated by a space [same as git design].
#define MAX_LENGTH 512
// Read message
void read_msg() {
int fd, flag = 0; // flag: whether valid message has been read.
char buff[MAX_LENGTH] = {0}; // Read at most MAX_LENGTH bytes of message.
ssize_t nread;
// If unable to open the message file, friendly prompt.
if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
printf("aborting commit due to msg file can't open.\n");
exit(1);
}
// Read the message file.
if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
char *line;
// Line-by-line check for valid messages.
line = strtok(buff, "\n");
while (line != NULL) {
// If it is not an invalid message [not a comment].
if (line[0] != '#') {
flag || printf("msg:"); // Print message prefix "msg:" when printing valid message for the first time.
flag = 1;
printf(" %s", line);
}
line = strtok(NULL, "\n"); // Keep calling until reaching buff's '\0'.
}
flag && printf("\n");
}
// If no valid message is read, friendly prompt.
if (!flag) {
printf("aborting commit due to empty commit msg.\n");
}
close(fd);
return ;
}
- Special case flag outputs message prefix: "msg:"
- Read at most the first 512 bytes of the message file.
- ⭐ String splitting function strtok——cplusplus
- char* strtok(char* str, const char* delimiters)
- It needs to be called repeatedly, from the second time on, the str passed in should be replaced with NULL, each call returns a split string, returning NULL indicates reaching the end of str.
- The effect is as follows:
- When there are no options, input the following message in vim.
- After successful reading, combine into one line for printing.
- Comments will be ignored.
Deleting the Message File#
To allow the parent process to possibly do more things, a child process is forked here and replaced with the rm process.
- The parent process can monitor the status of the rm process, although the rm process will also report failure information.
// Delete message file
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);
// The parent process prompts rm failure [the rm process will also have a prompt].
if (WEXITSTATUS(status)) printf("remove msg file error!\n");
}
return ;
}
- After using the message file, do not leave the COMMIT_MSG file.
Complete Code#
git_commit.c#
#include "head.h"
#define MAX_LENGTH 512
// Input message
void input_msg() {
pid_t pid;
int status;
// The parent process copies a child process.
if ((pid = fork()) < 0) {
perror("fork()");
exit(1);
}
if (pid == 0) {
// Replace the child process with the vim process.
execlp("vim", "vim", "COMMIT_MSG", NULL);
} else {
wait(&status); // Monitor child process status.
// The parent process prompts vim error.
if (WEXITSTATUS(status)) printf("vim error!\n");
}
return ;
}
// Read message
void read_msg() {
int fd, flag = 0; // flag: whether valid message has been read.
char buff[MAX_LENGTH] = {0}; // Read at most MAX_LENGTH bytes of message.
ssize_t nread;
// If unable to open the message file, friendly prompt.
if ((fd = open("COMMIT_MSG", O_RDONLY)) < 0) {
printf("aborting commit due to msg file can't open.\n");
exit(1);
}
// Read the message file.
if ((nread = read(fd, buff, sizeof(buff) - 1)) > 0) {
char *line;
// Line-by-line check for valid messages.
line = strtok(buff, "\n");
while (line != NULL) {
// If it is not an invalid message [not a comment].
if (line[0] != '#') {
flag || printf("msg:"); // Print message prefix "msg:" when printing valid message for the first time.
flag = 1;
printf(" %s", line);
}
line = strtok(NULL, "\n"); // Keep calling until reaching buff's '\0'.
}
flag && printf("\n");
}
// If no valid message is read, friendly prompt.
if (!flag) {
printf("aborting commit due to empty commit msg.\n");
}
close(fd);
return ;
}
// Delete message file
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);
// The parent process prompts rm failure [the rm process will also have a prompt].
if (WEXITSTATUS(status)) printf("remove msg file error!\n");
}
return ;
}
int main(int argc, char **argv) {
if (argc == 1) {
// 1. No options: need vim to input msg.
input_msg();
read_msg();
remove_file(); // If it can reach this step, the message file has been generated.
}
int opt;
while ((opt = getopt(argc, argv, "m:")) != -1) {
switch (opt) {
case 'm': {
// 2. There is -m and option parameter: print parameter.
printf("msg: %s\n", optarg);
} break;
default: {
// 3. Invalid option: friendly prompt.
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 is the executable file name generated by gcc ... -o git_commit.
References#
- Main knowledge points refer to "Network and System Programming"
- 0 Course Introduction and Command Line Parsing Functions——getopt
- 1 File, Directory Operations and Implementation of ls Ideas——open、read
- 3 Multiprocessing——fork、wait、exec family⭐