A闪的 BLOG 技术与人文
这是一个用于练手的使用C编写的shell程序,当然它只包含非常简单的功能。通常情况下,一个shell程序相对复杂,例如很多配置项以改变shell的现实效果或者部分行为。
从shell开始运行,到结束退出,它主要执行三个工作内容:
下面来看一下程序的main函数内容:
int main(int argc, const char * argv[]) {
lsh_loop();
return EXIT_SUCCESS;
}
函数lsh_loop()
将循环执行,解析命令。下面来看一下此函数的实现。
shell在一个循环过程中会执行三个步骤:
代码实现如下:
void lsh_loop(void){
char *line;
char **args;
int status;
do{
printf("> ");
line = lsh_read_line();
args = lsh_split_line(line);
status = lsh_execute(args);
free(line);
free(args);
}while(status);
}
这里涉及到另外三个函数,下面我们来逐一详解。在这之前我们需要注意,指针变量line
和args
这运行时使用动态内存分配,在命令执行后,应该使用free
将其内存释放。
从标准输入读取命令,可以按行进行读取,我们定义一个lsh_read_line()
来实现此功能。我们压力定义一个宏,来表示每次读取的行所占用的内存长度。
#define LSH_RL_BUFSIZE 1024
char *lsh_read_line(void){
int bufsize = LSH_RL_BUFSIZE;
int position = 0;
char *buffer = malloc(sizeof(char) *bufsize);
int c;
if(!buffer){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
while (1) {
c = getchar();
if( c==EOF || c== '\n'){
buffer[position] = '\0';
return buffer;
}else{
buffer[position] = c;
}
position++;
if(position>=bufsize){
bufsize += LSH_RL_BUFSIZE;
buffer = realloc(buffer, bufsize);
if(!buffer){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}
}
}
值得注意的是,通过malloc
或者realloc
申请或者重新申请内存空间后,一定要对结果进行判定,以防止内存分配失败后程序崩溃。
当然,上面的实现未使用新版本中stdio.h
中的getline()
函数,如果使用,则代码可以如下实现:
char *lsh_read_line(void)
{
char *line = NULL;
ssize_t bufsize = 0;
if (getline(&line, &bufsize, stdin) == -1){
if (feof(stdin)) {
exit(EXIT_SUCCESS);
} else {
perror("readline");
exit(EXIT_FAILURE);
}
}
return line;
}
现在我们已经读取了一行的输入,这是一个完整的C-Style字符串,我们要解析它,按照空格将它们切割,得到命令和对应的参数。为了简化我们的工作,此处使用string.h
中的strtok
函数实现。
#define LSH_TOK_BUFSIZE 64
#define LSH_TOK_DELIM " \t\r\n\a"
char **lsh_split_line(char *line){
int bufsize = LSH_TOK_BUFSIZE, position = 0;
char **tokens = malloc(bufsize*sizeof(char*));
char *token;
if(!tokens){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
token = strtok(line, LSH_TOK_DELIM);
while(token!=NULL){
tokens[position] = token;
position++;
if(position >= bufsize){
bufsize += LSH_TOK_BUFSIZE;
tokens = realloc(tokens, bufsize*sizeof(char *));
if(!tokens){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}
token = strtok(NULL,LSH_TOK_DELIM);
}
tokens[position] = NULL;
return tokens;
}
我们将切割后的每一个元素称之为token,将字符串切割完成后,我们就进入到了执行阶段。
如果你了解linux系统进程相关知识,此处应该知道,当前的shell程序将作为父进程,我们需要启动一个新的进程作为shell的子进程。并在它执行完成后得到它的执行结果。
在子进程执行时,我们要保持shell处于等待状态。以确保不会发生其他问题。
int lsh_launch(char **args){
pid_t pid, wpid;
int status;
pid = fork();
if(pid==0){
if(execvp(args[0], args)==-1){
perror("lsh");
}
exit(EXIT_FAILURE);
}else if(pid<0){
perror("lsh");
}else{
do{
wpid = waitpid(pid,&status, WUNTRACED);
}while(!WIFEXITED(status) && !WIFSIGNALED(status));
}
return 1;
}
当我们fork出一个进程后,先判定它的pid是否合法。如果不合法,则告知用户。如果正常,则调用execvp
执行命令,同时等待进程结束。
完成以上工作,我们还希望提供意见默认的命令,比如更改目录,可以使用chdir()
函数,退出shell这样的功能。下面来看一下三个内置命令的实现:
int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);
char *builtin_str[] = {
"cd",
"help",
"exit"
};
int (*builtin_func[])(char **) = {
&lsh_cd,
&lsh_help,
&lsh_exit,
};
int lsh_num_builtins(){
return sizeof(builtin_str) / sizeof(char *);
}
int lsh_cd(char **args){
if(args[1]==NULL){
fprintf(stderr, "lsh: expected argument to \"cd\"\n");
}else{
if(chdir(args[1])!=0){
perror("lsh");
}
}
return 1;
}
int lsh_help(char **args){
int i ;
printf("LSH\n");
printf("Type program names and arguments, and hit enter.\n");
printf("The following are built in:\n");
for (i = 0; i < lsh_num_builtins(); i++) {
printf(" %s\n", builtin_str[i]);
}
printf("Use the man command for information on other programs.\n");
return 1;
}
int lsh_exit(char **args)
{
return 0;
}
此处使用指针函数,将三个命令包裹到一个数组中,方便后续逻辑操作。
当开始执行命令是,首先需要判定当前命令是否合法。然后确认当前命令是否属于内置命令,如果不是,则调用lsh_launch
函数进启动子进程执行。
int lsh_execute(char **args){
int i ;
if(args[0]==NULL){
return 1;
}
for(i=0;i<lsh_num_builtins();i++){
if(strcmp(args[0], builtin_str[i])==0){
return (*builtin_func[i])(args);
}
}
return lsh_launch(args);
}
完成了上面所有的代码,你可以执行
gcc main.c
./main
如果你的代码没有任何错误的话,可以尝试输入命令查看效果。
完整代码如下:
//
// Write a Shell in C
//
// Created by mebius on 2020/12/13.
//
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#define LSH_RL_BUFSIZE 1024
#define LSH_TOK_BUFSIZE 64
#define LSH_TOK_DELIM " \t\r\n\a"
int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);
char *lsh_read_line(void){
int bufsize = LSH_RL_BUFSIZE;
int position = 0;
char *buffer = malloc(sizeof(char) *bufsize);
int c;
if(!buffer){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
while (1) {
c = getchar();
if( c==EOF || c== '\n'){
buffer[position] = '\0';
return buffer;
}else{
buffer[position] = c;
}
position++;
if(position>=bufsize){
bufsize += LSH_RL_BUFSIZE;
buffer = realloc(buffer, bufsize);
if(!buffer){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}
}
}
char **lsh_split_line(char *line){
int bufsize = LSH_TOK_BUFSIZE, position = 0;
char **tokens = malloc(bufsize*sizeof(char*));
char *token;
if(!tokens){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
token = strtok(line, LSH_TOK_DELIM);
while(token!=NULL){
tokens[position] = token;
position++;
if(position >= bufsize){
bufsize += LSH_TOK_BUFSIZE;
tokens = realloc(tokens, bufsize*sizeof(char *));
if(!tokens){
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}
token = strtok(NULL,LSH_TOK_DELIM);
}
tokens[position] = NULL;
return tokens;
}
int lsh_launch(char **args){
pid_t pid, wpid;
int status;
pid = fork();
if(pid==0){
if(execvp(args[0], args)==-1){
perror("lsh");
}
exit(EXIT_FAILURE);
}else if(pid<0){
perror("lsh");
}else{
do{
wpid = waitpid(pid,&status, WUNTRACED);
}while(!WIFEXITED(status) && !WIFSIGNALED(status));
}
return 1;
}
char *builtin_str[] = {
"cd",
"help",
"exit"
};
int (*builtin_func[])(char **) = {
&lsh_cd,
&lsh_help,
&lsh_exit,
};
int lsh_num_builtins(){
return sizeof(builtin_str) / sizeof(char *);
}
int lsh_cd(char **args){
if(args[1]==NULL){
fprintf(stderr, "lsh: expected argument to \"cd\"\n");
}else{
if(chdir(args[1])!=0){
perror("lsh");
}
}
return 1;
}
int lsh_help(char **args){
int i ;
printf("LSH\n");
printf("Type program names and arguments, and hit enter.\n");
printf("The following are built in:\n");
for (i = 0; i < lsh_num_builtins(); i++) {
printf(" %s\n", builtin_str[i]);
}
printf("Use the man command for information on other programs.\n");
return 1;
}
int lsh_exit(char **args)
{
return 0;
}
int lsh_execute(char **args){
int i ;
if(args[0]==NULL){
return 1;
}
for(i=0;i<lsh_num_builtins();i++){
if(strcmp(args[0], builtin_str[i])==0){
return (*builtin_func[i])(args);
}
}
return lsh_launch(args);
}
void lsh_loop(void){
char *line;
char **args;
int status;
do{
printf("> ");
line = lsh_read_line();
args = lsh_split_line(line);
status = lsh_execute(args);
free(line);
free(args);
}while(status);
}
int main(int argc, const char * argv[]) {
lsh_loop();
return EXIT_SUCCESS;
}
如果你想实现一个较为完整并且符合标准的shell程序,可以参考The Single UNIX Specification ,API接口可参考 POSIX规范。