设计自定义Shell程序
一、初始Shell程序
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
int main() {
/* 输入的命令行 */
char cmd[256];
/* 命令行拆解成的各部分,以空指针结尾 */
char *args[128];
while (1) {
/* 提示符 */
printf("# ");
fflush(stdin);
fgets(cmd, 256, stdin);
/* 清理结尾的换行符 */
int i;
for (i = 0; cmd[i] != '\n'; i++)
;
cmd[i] = '\0';
/* 拆解命令行 */
args[0] = cmd;
for (i = 0; *args[i]; i++)
for (args[i+1] = args[i] + 1; *args[i+1]; args[i+1]++)
if (*args[i+1] == ' ') {
*args[i+1] = '\0';
args[i+1]++;
break;
}
args[i] = NULL;
/* 没有输入命令 */
if (!args[0])
continue;
/* 内建命令 */
if (strcmp(args[0], "cd") == 0) {
if (args[1])
chdir(args[1]);
continue;
}
if (strcmp(args[0], "pwd") == 0) {
char wd[4096];
puts(getcwd(wd, 4096));
continue;
}
if (strcmp(args[0], "exit") == 0)
return 0;
/* 外部命令 */
pid_t pid = fork();
if (pid == 0) {
/* 子进程 */
execvp(args[0], args);
/* execvp失败 */
return 255;
}
/* 父进程 */
wait(NULL);
}
}
代码功能分析:
- line 9-31 :从命令行输入流将字符串读入数组中并拆分为命令部分args[0]和参数部分args[1]
- line 38-49:内部命令的判断和执行:
对args[0]进行匹配,若为内建命令,则执行系统调用实现相关功能。
— 内部命令:直接在shell中实现,例如cd, pwd, exit等简单的系统命令。Shell启动时,内部命令也被加载进内存中,因此执行速度更快。执行内部命令无需创建子进程。 - line 52-60:外部命令的执行:
创建子进程并用于执行外部命令。
— 外部命令:通常是一些实用程序,其包含的程序量较大,故不会随shell加载,只有当调用这些命令时才装载进内存中。外部命令需存放在环境变量目录中,如/bin,/sbin等。当使用外部命令时,shell会fork一个子进程并调用execvp找到这个外部命令并执行。
Q:为什么cd命令必须做成shell程序的功能,而不能编译为外部程序,通过调用来运行呢?
A: 如果cd作为外部程序执行,则必须fork一个子进程来执行,而这个子进程运行结束后,回到父进程,父进程的环境目录并未发生改变。故cd无法通过外部命令来实现。
二、改进的Shell程序
改进后的shell程序将拆分命令模块保留在main()函数中,而执行命令模块封装在函数execute()中以便递归调用。递归的思想有利于多重管道、复杂命令的实现。
健壮性:
- 无输入:判断args[0],若为换行符,则不执行、进入下一次循环
- cd:判断拆分的args[1]之后的每个一维数组,有空格则忽略;如果该目录下没有匹配到的子目录,输出“ERROR!”
- fork():如果没有创建子进程(即pid < 0),则输出"ERROR!"后强制退出
- 括号匹配:如果没有找到相应的反括号")",输出"Match ERROR!"后退出
- echo输出环境变量:若环境变量表中未匹配到参数,输出"NOT FOUND!"后退出
- exec():如果该函数没有自行退出进程,说明调用失败,退出并返回-1
支持管道:
调用pipe函数创建管道并分别与标准输入输出端口相连,利用dup()、dup2()函数进行参数的传递拷贝。
支持重定向:
调用open函数设置文件指针,并同样使用dup()、dup2()函数进行参数拷贝。
支持修改环境变量:
在内建命令中添加对字符串"export"的匹配,然后使用系统调用setenv()修改环境变量。
支持echo:
同样在内建命令中添加对字符串"echo"的匹配,然后使用系统调用获取变量并输出即可。
支持alias:
定义一个全局的alias_array数组(下文记为数组a),规定映射关系 f : a[i] = a[i+100]。
每次识别到alias stringA=“stringB” 命令后,将stringA存在a[i]中,stringB存在a[i+100]中。
每次进入循环,在数组a[0]~a[99]中匹配字符串args[0]。如果匹配成功a[k],则将a[k+100]作为cmd执行。
支持后台运行:(sleep 3 ; echo USTC_CS) &
从外到内处理 &、()、; 每一步都将命令拆分为子命令后作为参数送进execute()递归即可。
源代码:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
char alias_array[200][256] ;
int alias_top = 0 ;
int main() {
int execute (char *args[]) ;
/* 输入的命令行 */
char cmd[256];
/* 命令行拆解成的各部分,以空指针结尾 */
char *args[128];
while (1) {
/* 提示符 */
printf("# ");
fflush(stdin);
fgets(cmd, 256, stdin);
if(cmd[0] == '\n')
continue ;
/* 清理结尾的换行符 */
int i;
for (i = 0; cmd[i] != '\n'; i++)
;
cmd[i] = '\0';
/* 拆解命令行 */
args[0] = cmd;
for (i = 0; *args[i]; i++)
for (args[i+1] = args[i] + 1; *args[i+1]; args[i+1]++)
if (*args[i+1] == ' ') {
*args[i+1] = '\0';
args[i+1]++;
break;
}
args[i] = NULL;
//for (int i = 0 ; strcmp(args[i] , "\0") != 0 ; i ++)
// puts(args[i]) ;
if (strcmp(args[0] , "exit") == 0)
return 0 ;
execute(args) ;
}
}
int execute(char *args[])
{
int i ;
/* & */
for (i = 0 ; args[i] != 0 && strcmp(args[i] , "&") != 0 ; i++) ;
if (args[i] != 0)
{
args[i] = NULL ;
pid_t pid = fork() ;
if (pid < 0)
{
puts("ERROR!") ;
exit(-1) ;
}
if (pid == 0)
{
execute(args) ;
exit(0) ;
}
return 0 ;
}
/* 括号 */
for (i = 0 ; args[i] != 0 && args[i][0] != '(' ; i++) ;
if (args[i] != 0)
{
int j , k;
for (k = 0 ; args[i][k] != '\0' ; k++)
args[i][k] = args[i][k+1] ;
for(j = i ; args[j] != NULL ; j++)
for(k = 0 ; args[j][k] != '\0' ; k ++)
if (args[j][k] == ')')
{
args[j][k] = '\0' ;
args[j+1] = NULL ;
execute(args + i) ;
return 0 ;
}
puts("Match ERROR!") ;
return -1 ;
}
/* 分号 */
for (i = 0 ; args[i] != 0 && args[i][0] != ';' ; i ++) ;
if (args[i] != 0)
{
args[i] = NULL ;
execute(args) ;
execute(args+i+1) ;
return 0 ;
}
/* 管道 */
for (i = 0 ; args[i] != 0 && strcmp(args[i] , "|") != 0 ; i ++) ;
if (args[i] != 0) // 未到末尾,存在管道
{
int filedes[2] ;
pipe(filedes) ;
pid_t pid = fork() ;
if (pid < 0)
{
puts("Error!") ;
exit(-1) ;
}
if (pid == 0) // 子进程
{
close(filedes[0]) ; // 关闭读入端
dup2(filedes[1] , 1) ; // stdout标准输出拷贝至管道写入端
args[i] = NULL;
execute(args) ;
close(filedes[1]) ; // 递归执行
exit(0) ;
}
// 父进程
close(filedes[1]) ;
int buffer = dup(0) ; // 备份
dup2(filedes[0] , 0) ; // 管道读取端拷贝至标准输入
execute(args+i+1) ;
close(filedes[0]) ;
dup2(buffer , 0) ;
waitpid(pid , NULL , 0) ;
return 0 ;
}
/* 重定向 */
for (i = 0 ; args[i] != 0 && (strcmp(args[i] , ">") != 0 && strcmp(args[i] , ">>") != 0) ; i ++) ;
if (args[i] != 0)
{
int flag = 0 ; // 0为覆盖,1为追加
if (strcmp(args[i],">>") == 0)
flag = 1 ;
int buffer = dup(1) ;
int filedes ;
if (flag == 0) // >
filedes = open(args[i+1] , O_WRONLY|O_CREAT|O_TRUNC) ;
else // >>
filedes = open(args[i+1] , O_WRONLY|O_CREAT|O_APPEND) ;
for (i ; args[i+2] != NULL ; i++)
args[i] = args[i+2] ;
args[i] = NULL ;
dup2(filedes , 1) ;
execute(args) ;
close(filedes) ;
dup2(buffer , 1) ;
return 0 ;
}
// <
for (i = 0 ; args[i] != 0 && strcmp(args[i] , "<") != 0 ; i++) ;
if (args[i] != 0)
{
int buffer = dup(0) ;
int filedes ;
filedes = open(args[i+1] , O_RDONLY) ;
for (i ; args[i+2] != NULL ; i++)
args[i] = args[i+2] ;
args[i] = NULL ;
dup2(filedes , 0) ;
execute(args) ;
close(filedes) ;
dup2(buffer , 0) ;
return 0 ;
}
// alias判断
for (i = 0 ; i < 100 ; i ++)
{
if (strcmp(args[0], alias_array[i]) == 0)
{
pid_t pid = fork() ;
if (pid < 0)
{
puts("ERROR!") ;
exit(-1) ;
}
if (pid == 0)
{
// 拆解新的命令
char cmd[256] ;
char *args[128] ;
strcpy(cmd , alias_array[i+100]) ;
int j ;
for (j = 0 ; cmd[j] != '\0' && cmd[j] != '\n' ; j++) ;
if (cmd[j] == '\n')
cmd[j] == '\0' ;
args[0] = cmd ;
for (j = 0 ; *args[j] ; j++)
for (args[j+1] = args[j] + 1; *args[j+1]; args[j+1]++)
if (*args[i+1] == ' ')
{
*args[i+1] = '\0' ;
args[i+1] ++ ;
break ;
}
args[j] = NULL ;
// 用新的命令递归
execute(args) ;
return 0 ;
}
wait(NULL) ;
return 0 ;
}
}
// 无输入
if (args[0] == NULL)
return 0 ;
/* 内建命令 */
if (strcmp(args[0], "cd") == 0) {
int i ;
//for (i = 1 ; args[i] != NULL ; i++)
// puts(args[i]) ;
for (i = 1 ; strcmp(args[i] , " ") == 0 || strcmp(args[i] , "\0") == 0 ; i++) ;
if (args[i])
{
if (args[i][0] == ' ')
for (int k = 0 ; args[i][k] != '\0' ; k ++)
args[i][k] = args[i][k+1] ;
int error ;
error = chdir(args[i]);
if (error == -1)
{
puts("ERROR!") ;
return 0 ;
}
}
return 0 ;
}
if (strcmp(args[0], "pwd") == 0) {
char wd[4096];
puts(getcwd(wd, 4096));
return 0 ;
}
if (strcmp(args[0], "exit") == 0)
exit(0) ;
/* 修改环境变量 */
if (strcmp(args[0], "export") == 0)
{
int i ;
for (i = 0 ; args[1][i] != '=' && args[1][i] != '\0' ; i ++) ;
if (args[1][i] == '\0')
return 0;
args[1][i] = '\0' ;
setenv(args[1] , args[1]+i+1 , 1) ;
return 0;
}
/* 附加功能 */
//echo
if (strcmp(args[0], "echo") == 0)
{
if (strcmp(args[1], "~") == 0)
{
char *pathvar ;
pathvar = getenv("HOME") ;
printf("%s\n" , pathvar) ;
return 0 ;
}
if (args[1][0] == '$')
{
char name[256] ;
strcpy(name , args[1]+1) ;
char *pathvar ;
pathvar = getenv(name) ;
if (pathvar == NULL)
{
puts("NOT FOUND!") ;
return 0 ;
}
printf("%s\n" , pathvar) ;
return 0 ;
}
//else
// puts(args[1]) ;
}
// alias
if (strcmp(args[0], "alias") == 0)
{
int j ;
char name[256] ;
char command[256] ;
for (j = 0 ; args[1][j] != '=' ; j ++)
name[j] = args[1][j] ;
name[j] = '\0' ;
strcpy(alias_array[alias_top] , name) ;
j += 2 ; // 跳过= 和 '
int k ;
for (k = j ; args[1][k] != '\'' ; k++)
command[k-j] = args[1][k] ;
command[k] = '\0' ;
strcpy(alias_array[alias_top+100] , command) ;
alias_top ++ ;
return 0 ;
}
/* 外部命令 */
pid_t pid = fork();
if (pid == 0) {
/* 子进程 */
execvp(args[0], args);
/* execvp失败 */
return 255;
}
/* 父进程 */
wait(NULL);
return 0 ;
}