序
本文将介绍一个使用 C 语言编写的简单 Shell,这个 Shell 可以执行一些基本命令,例如 cd 和 exit。此外,Shell 还支持管道和重定向操作。
系统编程需知
1.进程间通信:
程序中使用了管道(pipe)进行两个子进程之间的通信,使用了标准输入输出重定向进行输出到文件的操作。
2.系统调用:
程序中使用了多个系统调用,如gethostname() 、getuid()、getpwuid()、chdir()、execvp()、dup2()、 freopen()等。通过这些系统调用可以实现对进程、文件、目录等的操作。
3.进程控制:
程序中使用了多个进程控制函数,如fork() 、wait() 和 waitpid() 等。通过这些函数可以实现父子进程分离、子进程退出状态的获取等功能。
4.GNU Readline 库:
程序中使用了 GNU Readline 库提供的函数,如readline() 和 add_history()等,实现了命令行的交互式输入和历史命令的保存和显示。
主要框架
1.初始化
main()
在程序开始运行时,首先需要进行一些初始化操作。例如初始化一些变量、启用历史记录、记录上一个目录等。这些初始化操作可以在函数中完成,也可以单独写一个函数来实现。
2.循环读取用户输入并解析命令
在进入循环之前,需要设置一个循环标记,用于控制程序何时结束。接着进入一个无限循环,在每次循环中,首先获取当前工作目录,并根据该目录设置提示符;然后读取用户输入,并将其添加到历史记录中;接着调用 函数来解析并执行命令;最后,调用 函数来初始化各种参数和标志位。
3.解析命令并执行
parse()
在 函数中,首先判断是否需要在后台运行,然后将字符串按照管道符号 “|” 进行分割,如果有多个子命令则创建一个管道;接着使用 函数来处理重定向符号 “<” 和 “>”,确定输入或输出文件。最后,根据用户输入的第一个命令查找对应的可执行文件或内置命令,并调用相应函数来运行命令。
4.执行外部命令
execute() 与 execvp()
在 函数中,首先关闭多余的文件描述符,然后根据输入输出重定向符号 “<” 和 “>” 来打开对应的文件,将标准输入、输出、错误输出重定向到文件或管道,最后使用 函数来执行命令。
5.执行内置命令
execute()
在 函数中,首先判断用户输入的是哪个内置命令,然后调用相应函数来执行。对于 cd 命令,它需要改变当前工作目录,而对于 exit 命令,则直接退出程序。
函数实现细节
在 main 函数中,首先初始化了一些变量,然后进入了一个无限循环。循环体内部分别进行了如下操作:设置提示符、读取用户输入并记录历史、解析用户命令、执行命令或报错、初始化变量。
解析命令主要是为了识别出是否是内置命令(例如 cd 和 exit)、是否需要管道操作和是否需要重定向操作。如果是内置命令则直接调用相应函数处理,否则 fork 一个子进程来执行该命令。如果需要管道,则需要再次 fork 一个子进程,前面的子进程执行第一个命令并把输出发送给后面的子进程,后面的子进程执行第二个命令。如果需要重定向,则关闭标准输出,重新打开一个文件,然后执行命令,最后再关闭文件。
#include <pwd.h>
#include <readline/history.h>
#include <readline/readline.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_LINE_LEN 256
#define MAX_CMD_LEN 1024
#define CMDMAX 256
#define ARGMAX 10
char lastdir[100];
char command[CMDMAX][MAX_CMD_LEN] = {0};
int pipecnt = 0;
int pipe_location;
char **cutline(char *buf, const char *delim);
void initial();
void init_lastdir();
void history_setup();
void loop();
void history_finish();
void set_prompt(char *prompt);
char *get_name();
int parse(char *buf);
void execute(int argcnt);
void commandWithPipe(int index, int n, int input_fd);
// void handle_signal(int sig) { }
int main() {
// 提示符
char prompt[BUFSIZ];
int argcnt = 0;
char *line;
// signal(SIGINT, handle_signal);
// getpwd
init_lastdir();
// gethistory
history_setup();
while (1) {
set_prompt(prompt);
if (!(line = readline(prompt)))
break;
if (*line)
add_history(line);
argcnt = parse(line);
free(line);
execute(argcnt);
initial(); // initial
}
history_finish();
}
/* 输出提示符 */
void set_prompt(char *prompt) {
char hostname[100];
char cwd[100];
char super = '#';
// to cut the cwd by "/"
char delims[] = "/";
struct passwd *pwp;
if (gethostname(hostname, sizeof(hostname)) == -1) {
// get hostname failed
strcpy(hostname, "unknown");
} // if
// getuid() get user id ,then getpwuid get the user information by user id
pwp = getpwuid(getuid());
if (!(getcwd(cwd, sizeof(cwd)))) {
// get cwd failed
strcpy(cwd, "unknown");
} // if
char cwdcopy[100];
strcpy(cwdcopy, cwd);
char *first = strtok(cwdcopy, delims);
char *second = strtok(NULL, delims);
// if at home
if (!(strcmp(first, "home")) && !(strcmp(second, pwp->pw_name))) {
int offset = strlen(first) + strlen(second) + 2;
char newcwd[100];
char *p = cwd;
char *q = newcwd;
p += offset;
while ((*(q++) = *(p++)))
;
char tmp[100];
strcpy(tmp, "~");
strcat(tmp, newcwd);
strcpy(cwd, tmp);
}
if (getuid() == 0) // if super
super = '#';
else
super = '$';
char *name = get_name();
sprintf(
prompt,
"\001\e[1;30;44m[%s]\001\e[1;30;42m\002%s@%s "
"\001\e[0m\002\001\e[1;30;41m\002%s\001\e[0m\002\033[43;1m%c➜ \033[0m",
name, pwp->pw_name, hostname, cwd, super);
}
// 得到系统发行版本
char *get_name() {
FILE *fp;
char line[MAX_LINE_LEN];
char *name = NULL;
fp = fopen("/etc/os-release", "r");
if (fp == NULL) {
printf("Failed to open /etc/os-release\n");
return NULL;
}
while (fgets(line, MAX_LINE_LEN, fp)) {
// Remove newline character at the end of the line
line[strcspn(line, "\n")] = 0;
// Find the first '=' character in the line
char *equals_pos = strchr(line, '=');
if (equals_pos == NULL) {
continue;
}
// Split the line into key and value
*equals_pos = '\0';
char *key = line;
char *value = equals_pos + 1;
// Remove quotes from the value string
size_t value_len = strlen(value);
if (value[0] == '"' && value[value_len - 1] == '"') {
value[value_len - 1] = '\0';
value++;
}
// Check if the key is NAME or VERSION_ID
if (strcmp(key, "NAME") == 0) {
name = strdup(value);
}
}
fclose(fp);
if (name != NULL) {
return name;
} else {
printf("Failed to find NAME in /etc/os-release\n");
return NULL;
}
}
/* 获取历史输入 */
void history_setup() {
using_history();
stifle_history(50);
read_history("/tmp/msh_history");
}
void init_lastdir() { getcwd(lastdir, sizeof(lastdir)); }
void history_finish() {
append_history(history_length, "/tmp/msh_history");
history_truncate_file("/tmp/msh_history", history_max_entries);
}
/* parse */
int parse(char *buf) {
char *token;
int cnt = 0;
int head = 0;
int len = strlen(buf);
for (int i = 0; i < len; i++) {
if (buf[i] == '|') {
if (pipecnt == 0) {
pipe_location = cnt;
}
pipecnt++;
strncpy(command[cnt], buf + head, i - head);
cnt++;
// strcpy(command[cnt], "|");
// cnt++;
head = i + 1;
}
if (buf[i] == '<') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], "<");
cnt++;
head = i + 1;
}
if (buf[i] == '>' && buf[i + 1] != '>') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], ">");
cnt++;
head = i + 1;
} else if (buf[i] == '>' && buf[i + 1] == '>') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], ">>");
cnt++;
head = i + 2;
i++;
}
if (buf[i] == '&') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], "&");
cnt++;
head = i + 1;
}
}
strncpy(command[cnt], buf + head, len - head);
cnt++;
return cnt;
}
/* execute */
void execute(int argcnt) {
int key = 0;
for (int i = 0; i < argcnt; i++) {
if (i == pipe_location) {
key = 1;
}
i += pipecnt;
commandWithPipe(pipe_location, pipecnt, STDIN_FILENO);
if (strcmp(command[i], "<") == 0) {
key = 1;
printf("<");
exit(0);
}
if (strcmp(command[i], ">") == 0) {
key = 1;
printf(">");
exit(0);
}
if (strcmp(command[i], ">>") == 0) {
key = 1;
printf(">>");
exit(0);
}
if (strcmp(command[i], "&") == 0) {
key = 1;
printf("&");
exit(0);
}
if (strcmp(command[i], "exit") == 0) {
key = 1;
exit(0);
}
}
for (int i = 0; key == 0 && i < argcnt; i++) {
char **args;
args = cutline(command[i], " ");
if (strcmp(args[i], "exit") == 0) {
exit(EXIT_SUCCESS);
}
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork error\n");
exit(0);
} else if (pid == 0) {
execvp(args[0], args);
} else if (pid > 0) {
wait(NULL);
}
}
}
/* pipecmd */
void commandWithPipe(int index, int pipecnt, int input_fd) {
// 创建管道
pid_t pid;
int arg_count = pipecnt + 1;
char *args[arg_count][ARGMAX];
int pd[pipecnt][2];
int status;
for (int i = 0; i < pipecnt; i++) {
pipe(pd[i]);
}
// 分割命令
for (int in = index, j = 0; j <= pipecnt; j++, in++) { // command[i]
int k = 0;
char *token = strtok(command[in], " ");
while (token != NULL && arg_count < ARGMAX) {
args[j][k++] = token;
// printf("args[%d][%d]=%s\n", j, k - 1, args[j][k - 1]);
token = strtok(NULL, " ");
}
// printf("args[%d] END\n", j);
args[j][k] = NULL;
}
int i = 0;
// 父进程循环创建多个并列子进程
for (i = 0; i < arg_count; i++) {
pid = fork();
if (pid == 0) // 子进程出口
break;
}
if (pid < 0) {
perror("fork()");
} else if (pid == 0 && pipecnt > 0) {
if (i == 0) {
dup2(pd[0][1], STDOUT_FILENO);
close(pd[0][0]);
for (int j = 1; j < pipecnt; j++) {
close(pd[j][1]);
close(pd[j][0]);
}
} else if (i == pipecnt) // 最后一个进程
{
dup2(pd[i - 1][0], 0); // 打开读端
close(pd[i - 1][1]); // 关闭写端
// 其他进程读写端全部关闭
for (int j = 0; j < pipecnt - 1; j++) {
close(pd[j][1]);
close(pd[j][0]);
}
} else // 中间进程
{
dup2(pd[i - 1][0], 0); // 前一个管道的读端打开
close(pd[i - 1][1]); // 前一个写端关闭
dup2(pd[i][1], 1); // 后一个管道的写端打开
close(pd[i][0]); // 后一个读端关闭
// 其他的全部关闭
for (int j = 0; j < pipecnt; j++) {
if (j != i && j != (i - 1)) {
close(pd[j][0]);
close(pd[j][1]);
}
}
}
}
if (pid == 0) {
execvp(args[i][0], args[i]); // 执行命令
perror("execvp");
exit(1);
}
// 父进程关闭所有的管道
for (i = 0; i < pipecnt; i++) {
close(pd[i][0]);
close(pd[i][1]); // 父进程端口全部关掉
}
for (int j = 0; j < arg_count; j++) // 父进程等待子进程
wait(NULL);
}
char **cutline(char *buf, const char *delim) {
int num_args = 0;
char **args = (char **)malloc(sizeof(char *) * 10); // 最多支持 10 个参数
char *token;
// 解析单个命令及其参数
token = strtok(buf, delim);
while (token != NULL) {
args[num_args++] = token;
token = strtok(NULL, delim);
}
args[num_args] = NULL; // 参数列表以 NULL 结尾
return args;
}
void initial() {
for (int i = 0; i < CMDMAX; i++) {
memset(command[i], '\0', MAX_CMD_LEN);
}
pipecnt = 0;
}#include <pwd.h>
#include <readline/history.h>
#include <readline/readline.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#define MAX_LINE_LEN 256
#define MAX_CMD_LEN 1024
#define CMDMAX 256
#define ARGMAX 10
char lastdir[100];
char command[CMDMAX][MAX_CMD_LEN] = {0};
int pipecnt = 0;
int pipe_location;
char **cutline(char *buf, const char *delim);
void initial();
void init_lastdir();
void history_setup();
void loop();
void history_finish();
void set_prompt(char *prompt);
char *get_name();
int parse(char *buf);
void execute(int argcnt);
void commandWithPipe(int index, int n, int input_fd);
// void handle_signal(int sig) { }
int main() {
// 提示符
char prompt[BUFSIZ];
int argcnt = 0;
char *line;
// signal(SIGINT, handle_signal);
// getpwd
init_lastdir();
// gethistory
history_setup();
while (1) {
set_prompt(prompt);
if (!(line = readline(prompt)))
break;
if (*line)
add_history(line);
argcnt = parse(line);
free(line);
execute(argcnt);
initial(); // initial
}
history_finish();
}
/* 输出提示符 */
void set_prompt(char *prompt) {
char hostname[100];
char cwd[100];
char super = '#';
// to cut the cwd by "/"
char delims[] = "/";
struct passwd *pwp;
if (gethostname(hostname, sizeof(hostname)) == -1) {
// get hostname failed
strcpy(hostname, "unknown");
} // if
// getuid() get user id ,then getpwuid get the user information by user id
pwp = getpwuid(getuid());
if (!(getcwd(cwd, sizeof(cwd)))) {
// get cwd failed
strcpy(cwd, "unknown");
} // if
char cwdcopy[100];
strcpy(cwdcopy, cwd);
char *first = strtok(cwdcopy, delims);
char *second = strtok(NULL, delims);
// if at home
if (!(strcmp(first, "home")) && !(strcmp(second, pwp->pw_name))) {
int offset = strlen(first) + strlen(second) + 2;
char newcwd[100];
char *p = cwd;
char *q = newcwd;
p += offset;
while ((*(q++) = *(p++)))
;
char tmp[100];
strcpy(tmp, "~");
strcat(tmp, newcwd);
strcpy(cwd, tmp);
}
if (getuid() == 0) // if super
super = '#';
else
super = '$';
char *name = get_name();
sprintf(
prompt,
"\001\e[1;30;44m[%s]\001\e[1;30;42m\002%s@%s "
"\001\e[0m\002\001\e[1;30;41m\002%s\001\e[0m\002\033[43;1m%c➜ \033[0m",
name, pwp->pw_name, hostname, cwd, super);
}
// 得到系统发行版本
char *get_name() {
FILE *fp;
char line[MAX_LINE_LEN];
char *name = NULL;
fp = fopen("/etc/os-release", "r");
if (fp == NULL) {
printf("Failed to open /etc/os-release\n");
return NULL;
}
while (fgets(line, MAX_LINE_LEN, fp)) {
// Remove newline character at the end of the line
line[strcspn(line, "\n")] = 0;
// Find the first '=' character in the line
char *equals_pos = strchr(line, '=');
if (equals_pos == NULL) {
continue;
}
// Split the line into key and value
*equals_pos = '\0';
char *key = line;
char *value = equals_pos + 1;
// Remove quotes from the value string
size_t value_len = strlen(value);
if (value[0] == '"' && value[value_len - 1] == '"') {
value[value_len - 1] = '\0';
value++;
}
// Check if the key is NAME or VERSION_ID
if (strcmp(key, "NAME") == 0) {
name = strdup(value);
}
}
fclose(fp);
if (name != NULL) {
return name;
} else {
printf("Failed to find NAME in /etc/os-release\n");
return NULL;
}
}
/* 获取历史输入 */
void history_setup() {
using_history();
stifle_history(50);
read_history("/tmp/msh_history");
}
void init_lastdir() { getcwd(lastdir, sizeof(lastdir)); }
void history_finish() {
append_history(history_length, "/tmp/msh_history");
history_truncate_file("/tmp/msh_history", history_max_entries);
}
/* parse */
int parse(char *buf) {
char *token;
int cnt = 0;
int head = 0;
int len = strlen(buf);
for (int i = 0; i < len; i++) {
if (buf[i] == '|') {
if (pipecnt == 0) {
pipe_location = cnt;
}
pipecnt++;
strncpy(command[cnt], buf + head, i - head);
cnt++;
// strcpy(command[cnt], "|");
// cnt++;
head = i + 1;
}
if (buf[i] == '<') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], "<");
cnt++;
head = i + 1;
}
if (buf[i] == '>' && buf[i + 1] != '>') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], ">");
cnt++;
head = i + 1;
} else if (buf[i] == '>' && buf[i + 1] == '>') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], ">>");
cnt++;
head = i + 2;
i++;
}
if (buf[i] == '&') {
strncpy(command[cnt], buf + head, i - head);
cnt++;
strcpy(command[cnt], "&");
cnt++;
head = i + 1;
}
}
strncpy(command[cnt], buf + head, len - head);
cnt++;
return cnt;
}
/* execute */
void execute(int argcnt) {
int key = 0;
for (int i = 0; i < argcnt; i++) {
if (i == pipe_location) {
key = 1;
}
i += pipecnt;
commandWithPipe(pipe_location, pipecnt, STDIN_FILENO);
if (strcmp(command[i], "<") == 0) {
key = 1;
printf("<");
exit(0);
}
if (strcmp(command[i], ">") == 0) {
key = 1;
printf(">");
exit(0);
}
if (strcmp(command[i], ">>") == 0) {
key = 1;
printf(">>");
exit(0);
}
if (strcmp(command[i], "&") == 0) {
key = 1;
printf("&");
exit(0);
}
if (strcmp(command[i], "exit") == 0) {
key = 1;
exit(0);
}
}
for (int i = 0; key == 0 && i < argcnt; i++) {
char **args;
args = cutline(command[i], " ");
if (strcmp(args[i], "exit") == 0) {
exit(EXIT_SUCCESS);
}
pid_t pid;
pid = fork();
if (pid < 0) {
perror("fork error\n");
exit(0);
} else if (pid == 0) {
execvp(args[0], args);
} else if (pid > 0) {
wait(NULL);
}
}
}
/* pipecmd */
void commandWithPipe(int index, int pipecnt, int input_fd) {
// 创建管道
pid_t pid;
int arg_count = pipecnt + 1;
char *args[arg_count][ARGMAX];
int pd[pipecnt][2];
int status;
for (int i = 0; i < pipecnt; i++) {
pipe(pd[i]);
}
// 分割命令
for (int in = index, j = 0; j <= pipecnt; j++, in++) { // command[i]
int k = 0;
char *token = strtok(command[in], " ");
while (token != NULL && arg_count < ARGMAX) {
args[j][k++] = token;
// printf("args[%d][%d]=%s\n", j, k - 1, args[j][k - 1]);
token = strtok(NULL, " ");
}
// printf("args[%d] END\n", j);
args[j][k] = NULL;
}
int i = 0;
// 父进程循环创建多个并列子进程
for (i = 0; i < arg_count; i++) {
pid = fork();
if (pid == 0) // 子进程出口
break;
}
if (pid < 0) {
perror("fork()");
} else if (pid == 0 && pipecnt > 0) {
if (i == 0) {
dup2(pd[0][1], STDOUT_FILENO);
close(pd[0][0]);
for (int j = 1; j < pipecnt; j++) {
close(pd[j][1]);
close(pd[j][0]);
}
} else if (i == pipecnt) // 最后一个进程
{
dup2(pd[i - 1][0], 0); // 打开读端
close(pd[i - 1][1]); // 关闭写端
// 其他进程读写端全部关闭
for (int j = 0; j < pipecnt - 1; j++) {
close(pd[j][1]);
close(pd[j][0]);
}
} else // 中间进程
{
dup2(pd[i - 1][0], 0); // 前一个管道的读端打开
close(pd[i - 1][1]); // 前一个写端关闭
dup2(pd[i][1], 1); // 后一个管道的写端打开
close(pd[i][0]); // 后一个读端关闭
// 其他的全部关闭
for (int j = 0; j < pipecnt; j++) {
if (j != i && j != (i - 1)) {
close(pd[j][0]);
close(pd[j][1]);
}
}
}
}
if (pid == 0) {
execvp(args[i][0], args[i]); // 执行命令
perror("execvp");
exit(1);
}
// 父进程关闭所有的管道
for (i = 0; i < pipecnt; i++) {
close(pd[i][0]);
close(pd[i][1]); // 父进程端口全部关掉
}
for (int j = 0; j < arg_count; j++) // 父进程等待子进程
wait(NULL);
}
char **cutline(char *buf, const char *delim) {
int num_args = 0;
char **args = (char **)malloc(sizeof(char *) * 10); // 最多支持 10 个参数
char *token;
// 解析单个命令及其参数
token = strtok(buf, delim);
while (token != NULL) {
args[num_args++] = token;
token = strtok(NULL, delim);
}
args[num_args] = NULL; // 参数列表以 NULL 结尾
return args;
}
void initial() {
for (int i = 0; i < CMDMAX; i++) {
memset(command[i], '\0', MAX_CMD_LEN);
}
pipecnt = 0;
}
总的来说,这是一个相对简单的 Shell 程序,通过使用Linux操作系统提供的API和库函数来进行开发,实现了一些基本功能。