实现的功能
类 UNIX 操作系统控制台的提示信息
- 当前用户信息
- 当前工作目录
获取命令字符串
- 分号分隔
- 多行输入
解析命令
- 分析指令及其参数
- 管道
- 输入输出重定向
- 变量存储和替换
指令执行
- exec 函数族实现前台执行
- 实现部分内建命令
大致框架
我们使用的 shell 一般有以下流程:初始化,打印提示符,解析命令,执行内置命令,执行外部命令,循环上述过程。所以可以根据这个流程来写出大致框架:
int main()
{
while (true)
{
init();
fflush(stdin);
print_prompt();
int len = get_input();
if (len <= 0)
continue;
parse_token();
for (auto i = 0; i < cmdNum; i++)
{
cmd_info* curCmd = cmds + i;
if (exec_inner(curCmd) == 1) // if not shell builtin
{
// execvp will not return if it runs successfully
// so use child proc to execute other command.
pid_t pid = fork();
if (pid < 0)
printf("[Error] Fork error in main.");
else if (pid == 0) {
exec_outer(curCmd);
continue;
}
if ((cmds[i].type & BACKGROUND) == 0)
waitpid(pid, nullptr, 0);
}
// free cmd node
init_node(curCmd);
}
}
}
功能实现
头文件和全局变量等
#include <cstdio>
#include <cstdlib>
#include <cerrno>
#include <cstring>
#include <fcntl.h>
#include <pwd.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#define MAX_CMD 100 // Maximum number of read commands per line
#define MAX_VAR_NUM 100 // Maximum number of variables
#define BUFFER_SIZE 1024 // Maximum buffer size
#define MAX_PATH_LENGTH 1024 // Maximum length of file path
struct cmd_info cmds[MAX_CMD];
char cmdStr[BUFFER_SIZE];
int cmdNum = 0;
char envKey[MAX_VAR_NUM][MAX_PATH_LENGTH];
char envVal[MAX_VAR_NUM][MAX_PATH_LENGTH];
int varNum = 0;
cmd_info 结构体
首先我们需要一个结构体来保存命令的信息。根据要实现的功能,这个结构体应该保存有以下信息:
- 指令名
- 指令参数
- 输入输出重定向
- 管道信息
所以我设计结构体如下:
// type of command, use binary
#define BACKGROUND 1
#define LEFT_REDIRECT 2
#define RIGHT_REDIRECT 4
#define REDIRECT_APPEND 8
struct cmd_info
{
// command and parameters
int argc = 0;
char** argv{};
// Input and output redirection
char* input{};
char* output{};
// next command
struct cmd_info* next = nullptr;
// command attributes
int type = 0;
};
argc 和 argv 记录命令和参数个数;input 和 output 记录输入输出重定向;next 指针记录管道的下一个指令;type 记录重定向类型。
输出提示符
一般入门编程学的就是这个,就不需要讲什么了吧。
void print_prompt()
{
char host_name[MAX_PATH_LENGTH]; // user's hostname
char path_name[MAX_PATH_LENGTH]; // current working path
passwd* pwd = getpwuid(getuid()); // user's information
getcwd(path_name, MAX_PATH_LENGTH);
// get user's hostname
if (gethostname(host_name, MAX_PATH_LENGTH))
strcpy(host_name, "unknown");
// if current directory is not user's home, print it directory
if (strlen ( path_name ) < strlen ( pwd->pw_dir ) || strncmp(path_name , pwd->pw_dir , strlen ( pwd->pw_dir)) != 0)
printf("\033[;32m%s@%s\033[0m:\033[;34m%s\033[0m", pwd->pw_name, host_name, path_name);
else // else use '~' instead of user's home
printf("\033[;32m%s@%s\033[0m:\033[;34m~%s\033[0m", pwd->pw_name, host_name, path_name + strlen(pwd->pw_dir));
// if the current user is root
if (getuid() == 0)
printf("# "); // is root
else
printf("$ "); // isn't root
}
解析命令字符串
这个功能代码量比较大,具体代码见文末吧。
这些函数解析命令字符串, 能支持多个空格,支持多行输入,支持多条命令 ;
,支持了变量$
, 支持引号'
,同时为重定向 <
,>
,<<
,以及管道 |
做好准备。
这一部分有些可能处理不是很恰当,参考一下就好了吧 qwq
内建命令
对于内建命令, 比如 cd, pwd, exit, echo, export, unset
可以直接执行
在代码中, 内建命令的实现都在 exec_inner
函数中, 如果不是内建命令, 则返回 1 , 然后会调用执行外部命令的函数 exec_outer
外部命令
实现的函数是 exec_outer
,里面包括了重定向,管道,下面再介绍。
对于外部命令, 可以 fork 一个子进程,然后使用 exec 函数族让程序在子进程执行并返回。这里我使用 execvp
函数。
如果当前命令 的 next
为 nullptr
,即没有下一条管道命令,那么直接将标准文件描述符传给 setIO
处理好文件 IO,然后调用 execvp
执行外部命令即可。
如果不为nullptr
,说明有管道。建立管道,并用 fork 来新建子进程执行管道命令。这时传递到 setIO
函数的是对应管道文件描述符的输入输出, 然后如果有多个管道,可以递归地调用 execOuter
函数。
void exec_outer(cmd_info* cmd)
{
// the last command runs directly and exit
if (cmd->next == nullptr)
{
setIO(cmd, STDIN_FILENO, STDOUT_FILENO);
execvp(cmd->argv[0], cmd->argv);
printf("[Error] Execute command %s error.\n", cmd->argv[0]);
return;
}
// pipe
int fd[2];
pipe(fd);
pid_t pid = fork();
if (pid < 0)
{
printf("[Error] Fork error in exec_outer.");
return;
}
else if (pid == 0)
{
close(fd[0]);
setIO(cmd, STDIN_FILENO, fd[1]);
execvp(cmd->argv[0], cmd->argv);
// if execute success, the following code will not be executed
printf("[Error] Execute command %s error.\n", cmd->argv[0]);
return;
}
else
{
wait(nullptr);
close(fd[1]);
// execute all commands recursively
cmd = cmd->next;
setIO(cmd, fd[0], STDOUT_FILENO);
exec_outer(cmd);
}
}
管道与重定向
重定向的 I/O 以及管道的 I/O,我都放在 setIO
函数中处理,如下.
这个函数接受的参数包括一个命令指针 cmd
(以;分隔的, 包括管道中的命令), 以及 一个输入文件描述符rfd
,一个输出文件描述符wfd
.
文件重定向
如果命令的 type
属性中的 LEFT_REDIRECTION/ RIGHT_REDIRECTION
标志位设置为 1 的话,就打开重定向的文件得到其文件描述符, 然后将标准输入/输出文件描述符关闭,再修改为此文件描述符。
注意最后用完此该文件描述符要用 close 关闭
管道重定向
分别检查传入的文件描述符参数是否是标准输入,输出。如果不是, 说明传递的是管道的文件描述符, 就将相应的 标准输入/输出 关闭 ,再复制到 rfd/wfd
, 最后`close rfd/wfd
void setIO(cmd_info* cmd, const int rfd, const int wfd)
{
int type = cmd->type;
// > or >>
if ((type & RIGHT_REDIRECT) != 0)
{
int mode = 0;
if ((type & REDIRECT_APPEND) != 0)
mode = O_WRONLY | O_TRUNC | O_CREAT;
else
mode = O_WRONLY | O_APPEND | O_CREAT;
// change output file
int wport = open(cmd->output, mode);
dup2(wport, STDOUT_FILENO);
close(wport);
}
// <
if ((type & LEFT_REDIRECT) != 0)
{
int rport = open(cmd->input, O_RDONLY);
dup2(rport, STDIN_FILENO);
close(rport);
}
// pipe
if (rfd != STDIN_FILENO)
{
dup2(rfd, STDIN_FILENO);
close(rfd);
}
if (wfd != STDOUT_FILENO)
{
dup2(wfd, STDOUT_FILENO);
close(wfd);
}
}