从零到一实现一个简易 Shell
这应该是个蛮有趣的话题:“什么是 Shell
”?相信只要摸过计算机,对于操作系统(不论是 Linux
、Unix
或者是 Windows
)有点概念的朋友们大多听过这个名词,因为只要有“操作系统“那么就离不开 Shell
这个东西。不过,在讨论 Shell
之前,我们先来了解一下计算机的运行状况吧!举个例子来说:当你要计算机传输出来“音乐”的时候,你的计算机需要什么东西呢?
- 硬件:当然就是需要你的硬件有“声卡芯片”这个配备,否则怎么会有声音;
- 核心管理:操作系统的核心可以支持这个芯片组,当然还需要提供芯片的驱动程序啰;
- 应用程序:需要使用者(就是你)输入发生声音的指令啰!
这就是基本的一个输出声音所需要的步骤!也就是说,你必须要“输入”一个指令之后,“硬件“才会通过你下达的指令来工作!那么硬件如何知道你下达的指令呢?那就是 kernel
(核心)的控制工作了!也就是说,我们必须要通过“Shell
”将我们输入的指令与 Kernel
沟通,好让 Kernel
可以控制硬件来正确无误的工作!基本上,我们可以通过下面这张图来说明一下:

以上内容摘自《鸟哥的 Linux
私房菜基础学习篇(第四版)》311 页。
1. Shell
的基本功能
一个基本的 Shell
需要具备以下功能:
- 提示符显示:显示当前用户、主机名和工作目录,例如
[user@host ~]#
。
- 命令读取:从标准输入读取用户输入的命令。
- 命令解析:将输入的命令行分割为命令和参数。
- 命令执行:支持内置命令(如
cd
、export
)和外部命令(如 ls
、cat
)。
- 重定向支持:支持输入重定向
<
、输出重定向 >
和追加输出重定向 >>
。
- 环境变量管理:支持查看和设置环境变量。
- 退出机制:支持通过
exit
退出 Shell
。
让我们一步步实现这些功能。
2. 实现 Shell
的提示符
Shell
的提示符是用户交互的起点,通常显示为 [用户名@主机名 当前目录]#
。我们需要获取用户名、主机名和当前工作目录。
1. 获取用户信息
- 用户名:使用
getenv("USER")
获取当前用户名。
- 主机名:使用
getenv("HOSTNAME")
获取主机名。
- 当前目录:使用
getcwd()
获取当前工作目录。
2. 定义提示符格式
我们通过宏定义设置提示符的格式:
1 2 3
| #define LEFT "[" #define RIGHT "]" #define LABLE "#"
|
3. 实现 interact
函数
interact
函数负责显示提示符并读取用户输入:
1 2 3 4 5 6 7 8 9 10
| void interact(char* cline, int size) { getpwd(); printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd); char* s = fgets(cline, size, stdin); assert(s); (void)s; cline[strlen(cline) - 1] = '\0'; check_redir(cline); }
|
getpwd()
调用 getcwd(pwd, sizeof(pwd))
更新全局变量 pwd
。
printf
格式化输出提示符,例如 [user@host /home]#
。
fgets
从标准输入读取命令行。
check_redir
检查是否有重定向符号(稍后实现)。
命令行解析:用户输入的命令行需要被分割成命令和参数。例如,输入 ls -l /home
应分割为 ["ls", "-l", "/home"]
。
3. 使用 strtok
分割字符串
我们使用 strtok
函数按空格或制表符分割命令行:
1 2 3 4 5 6 7 8 9
| #define DELIM " \t"
int splitstring(char cline[], char* _argv[]) { int i = 0; argv[i++] = strtok(cline, DELIM); while (_argv[i++] = strtok(NULL, DELIM)); return i - 1; }
|
strtok(cline, DELIM)
分割第一个 token
(命令)。
- 循环调用
strtok(NULL, DELIM)
获取后续参数。
- 返回值是参数个数
argc
,存储在全局数组 argv
中。
全局变量定义如下:
1 2 3 4
| #define LINE_SIZE 1024 #define ARGC_SIZE 32 char commandline[LINE_SIZE]; char* argv[ARGC_SIZE];
|
4. 命令执行
Shell
需要区分两种命令:
- 内置命令:由
Shell
直接处理,如 cd
、export
、echo
。
- 外部命令:通过
fork
和 exec
执行系统中的可执行文件。
5. 内置命令实现
内置命令在 Shell
进程中直接执行,无需创建子进程。我们在 buildCommand
函数中实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
| int buildCommand(char* _argv[], int _argc) { if (_argc == 2 && strcmp(_argv[0], "cd") == 0) { chdir(argv[1]); getpwd(); sprintf(getenv("PWD"), "%s", pwd); return 1; } else if (_argc == 2 && strcmp(_argv[0], "export") == 0) { strcpy(myenv, _argv[1]); putenv(myenv); return 1; } else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) { if (strcmp(_argv[1], "$?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else if (*_argv[1] == '$') { char* val = getenv(_argv[1] + 1); if (val) { printf("%s\n", val); } } else { printf("%s\n", _argv[1]); } return 1; } if (strcmp(_argv[0], "ls") == 0) { _argv[_argc++] = "--color"; _argv[_argc] = NULL; } return 0; }
|
cd
:使用 chdir
切换目录,并更新 PWD
环境变量。
export
:使用 putenv
设置环境变量,myenv
是全局缓冲区。
echo
:支持打印上一次退出码、环境变量 VAR
或普通字符串。
ls
增强:自动添加 --color
选项以显示彩色输出。
- 返回值:1 表示内置命令已处理,0 表示需要外部执行。
6. 外部命令执行
外部命令通过 fork
创建子进程并使用 execvp
执行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| void NormalExcute(char* _argv[]) { pid_t id = fork(); if (id < 0) { perror("fork"); return; } else if (id == 0) { int fd = 0; if (rdir == IN_RDIR) { fd = open(rdirfilename, O_RDONLY); dup2(fd, 0); } else if (rdir == OUT_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd, 1); } else if (rdir == APPEND_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_APPEND, 0666); dup2(fd, 1); } execvp(_argv[0], _argv); exit(EXIT_CODE); } else { int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { lastcode = WEXITSTATUS(status); } } }
|
fork()
创建子进程。
子进程根据重定向类型(rdir
)打开文件并使用 dup2
重定向。
execvp
执行命令,从 PATH
中查找可执行文件。
父进程使用 waitpid
等待子进程结束,并记录退出码到 lastcode
。
重定向支持,Shell
支持三种重定向:
输入重定向:< filename
输出重定向:> filename
追加输出重定向:>> filename
7. 解析重定向符号
在 check_redir
函数中解析重定向:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
| #define NONE -1 #define IN_RDIR 0 #define OUT_RDIR 1 #define APPEND_RDIR 2
char* rdirfilename = NULL; int rdir = NONE;
void check_redir(char* cmd) { char* pos = cmd; while (*pos) { if (*pos == '>') { if (*(pos + 1) == '>') { *pos++ = '\0'; *pos++ = '\0'; while (isspace(*pos)) pos++; rdirfilename = pos; rdir = APPEND_RDIR; break; } else { *pos = '\0'; pos++; while (isspace(*pos)) pos++; rdirfilename = pos; rdir = OUT_RDIR; break; } } else if (*pos == '<') { *pos = '\0'; pos++; while (isspace(*pos)) pos++; rdirfilename = pos; rdir = IN_RDIR; break; } pos++; } }
|
- 遍历命令行,检测
<
、>
或 >>
。
- 将符号替换为
\0
以分割命令和文件名。
- 设置全局变量
rdir
和 rdirfilename
。
interact
函数调用 check_redir
进行解析。
8. 执行重定向
在 NormalExcute
中根据 rdir
处理重定向:
- 输入重定向:打开文件并重定向到标准输入(文件描述符 0)。
- 输出重定向:创建或截断文件并重定向到标准输出(文件描述符 1)。
- 追加输出重定向:创建或追加文件并重定向到标准输出。
- 环境变量管理:
- 查看:通过
echo $VAR
查看环境变量值。
- 设置:通过
export VAR = VALUE
设置环境变量。
这些功能已在 buildCommand
的 echo
和 export
实现中完成。
9. 主循环
Shell
的主循环负责持续运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| int main() { while (!quit) { rdirfilename = NULL; rdir = NONE; interact(commandline, sizeof(commandline)); int argc = splitstring(commandline, argv); if (argc == 0) continue; int n = buildCommand(argv, argc); if (!n) { NormalExcute(argv); } } return 0; }
|
- 重置重定向状态。
- 获取并解析用户输入。
- 处理内置命令或外部命令。
quit
变量控制退出(当前代码中未实现 exit
命令,可扩展)。
3. 源码一览
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247
| #include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include <ctype.h> #include <fcntl.h>
#define LEFT "[" #define RIGHT "]" #define LABLE "#" #define DELIM " \t" #define LINE_SIZE 1024 #define ARGC_SIZE 32 #define EXIT_CODE 44
#define NONE -1 #define IN_RDIR 0 #define OUT_RDIR 1 #define APPEND_RDIR 2
int lastcode = 0; int quit = 0; extern char** environ; char commandline[LINE_SIZE]; char* argv[ARGC_SIZE]; char pwd[LINE_SIZE]; char* rdirfilename = NULL; int rdir = NONE;
char myenv[LINE_SIZE];
const char* getusername() { return getenv("USER"); }
const char* gethostname1() { return getenv("HOSTNAME"); }
void getpwd() { getcwd(pwd, sizeof(pwd)); }
void check_redir(char* cmd) { char* pos = cmd; while (*pos) { if (*pos == '>') { if (*(pos + 1) == '>') { *pos++ = '\0'; *pos++ = '\0'; while (isspace(*pos)) { pos++; }
rdirfilename = pos; rdir = APPEND_RDIR; break; } else { *pos = '\0'; pos++; while (isspace(*pos)) { pos++; }
rdirfilename = pos; rdir = OUT_RDIR; break; } } else if (*pos == '<') { *pos = '\0'; pos++; while (isspace(*pos)) { pos++; }
rdirfilename = pos; rdir = IN_RDIR; break; } pos++; } }
void interact(char* cline, int size) { getpwd(); printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname1(), pwd); char* s = fgets(cline, size, stdin); assert(s); (void)s;
cline[strlen(cline) - 1] = '\0'; check_redir(cline); }
int splitstring(char cline[], char* _argv[]) { int i = 0; argv[i++] = strtok(cline, DELIM); while (_argv[i++] = strtok(NULL, DELIM));
return i - 1; }
void NormalExcute(char* _argv[]) { pid_t id = fork(); if (id < 0) { perror("fork"); return; } else if (id == 0) { int fd = 0; if (rdir == IN_RDIR) { fd = open(rdirfilename, O_RDONLY); dup2(fd, 0); } else if (rdir == OUT_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_TRUNC, 0666); dup2(fd, 1); } else if (rdir == APPEND_RDIR) { fd = open(rdirfilename, O_CREAT | O_WRONLY | O_APPEND, 0666); dup2(fd, 1); }
execvp(_argv[0], _argv); exit(EXIT_CODE); } else { int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid == id) { lastcode = WEXITSTATUS(status); } } }
int buildCommand(char* _argv[], int _argc) { if (_argc == 2 && strcmp(_argv[0], "cd") == 0) { chdir(argv[1]); getpwd(); sprintf(getenv("PWD"), "%s", pwd); return 1; } else if (_argc == 2 && strcmp(_argv[0], "export") == 0) { strcpy(myenv, _argv[1]); putenv(myenv); return 1; } else if (_argc == 2 && strcmp(_argv[0], "echo") == 0) { if (strcmp(_argv[1], "$?") == 0) { printf("%d\n", lastcode); lastcode = 0; } else if (*_argv[1] == '$') { char* val = getenv(_argv[1] + 1); if (val) { printf("%s\n", val); } } else { printf("%s\n", _argv[1]); } return 1; }
if (strcmp(_argv[0], "ls") == 0) { _argv[_argc++] = "--color"; _argv[_argc] = NULL; } return 0; }
int main() { while (!quit) { rdirfilename = NULL; rdir = NONE;
interact(commandline, sizeof(commandline));
int argc = splitstring(commandline, argv); if (argc == 0) continue;
int n = buildCommand(argv, argc);
if (!n) { NormalExcute(argv); } } return 0; }
|
这个 Shell
虽简单,但展示了 Shell
的相对核心机制。当然还有一些其他功能没有实现,以及代码中还多场景考虑不周到、兼容性、健壮性等问题处理不够完美,还是等以后学深了再完善吧~