前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >UNIX高级环境编程 第三次实验 实现带参数的简单Shell

UNIX高级环境编程 第三次实验 实现带参数的简单Shell

作者头像
glm233
发布2020-11-26 16:40:23
8870
发布2020-11-26 16:40:23
举报

实验三 实现带参数的简单Shell

1. 实验内容

利用课本第9页程序1-5的框架,实现允许输入命令带参数的简单shell。原来的实现是不能够带参数的。输入命令所能带的参数个数,只受

到系统键盘输入缓冲区长度(以及shell输入缓冲区长度)的限制,该缓冲区的缺省长度是4096个字节。

实现时要解决的主要问题有:

**1.1正确理解并使用系统调用fork(),execve()和waitpid(),特别是execve()函数。**fork()函数创建一个新的进程。新进程就是所谓的子

进程,它是执行fork()函数的进程(父进程)的“克隆”,也就是说,子进程执行的程序与父进程的完全一样。当fork()函数返回值为0时表示处

于子进程中;而返回值大于0时表示处于父进程中,此时的返回值是子进程的进程id。因此,fork()的返回值可以用来划分仅仅适合父进程

和子进程执行的程序段。fork()函数返回值为-1时表示出错。

如果子进程只是运行与父进程完全一样的程序,那用处是很有限的。要让子进程运行不同于父进程的程序,就必须调用execve函数,它是

所有其他exec函数的基础。execve函数把调用它的进程的程序,替换成execve函数的参数所指定的程序。运行execve函数成功后,进程

将开始运行新的程序,也就是execve函数的参数所指定的程序。

execve函数原型:int execve(const char *path, const char *argv[],const char *envp[]);

其中:

  • path:要执行的程序路径名,比如“/bin/ls”,“cd”,“/usr/bin/gcc”等等。
  • argv:参数表,比如ls命令中可带的命令行参数-l,-a等。注意,argv的第一个元素必须是要执行的程序(命令)的路径名。
  • envp:环境变量表,供要执行的命令使用。实参数用NULL或系统环境变量environ均可。注意,因为environ由系统提供,属于外部变量,所以说明时必须用“extern”修饰。

例子:

代码语言:javascript
复制
char *argv[] = {“gcc”, “-g”, “-c”, “hello.c”, NULL};// 编译程序“hello.c”execve(“/bin/ls”, argv1, NULL);  
char *argv1[] = {“/bin/ls”, “-l”, “-a”, NULL};  // 执行命令“ls –l –a”execve(“/usr/ls”, argv1, NULL);
execve(“/usr/bin/gcc”, argv, environ);    // 出错,因为目录/usr/下没有ls程序。// 注意,在argv1 的第一个字符串“/bin/ls”中,只有ls是有用的。

系统调用waitpid()用于等待子进程结束、获取子进程的运行状态,详细说明在第八章。本实验仅仅用它使父进程等待子进程结束,因此维持程序1-5的用法即可。

1.2 根据简单shell的输入,构造execve函数的参数。 根据程序1-5,数组buf保存用户的输入,包括命令和参数。由于shell命令的命令名和各参数之间是用空格分开,因此可以用空格作为分界符。通过一个循环可以把buf数组中的命令和各个参数依次分离开来,并赋给数组argv的各元素适当的指针值。argv数组的最后一个指针必须是NULL。接着就可以调用execve(argv[0],argv, environ)来执行用户输入的命令。

提示:argv数组中各指针所指向的字符串,可以直接利用buf的存储空间,不需要另外分配内存。

2. 实验设计与实现

2.1功能概述

​ 支持常用unix环境下命令**(pwd,ls,cd,vi,touch,rm,构成简单的shell)**

​ 支持带任意参数命令如ls -lh ,rm -rf,cd ~ ,文件重定向等等

2.2代码框架

头文件:

代码语言:javascript
复制
#include "apue.h"
#include <errno.h>
#include <fcntl.h>

apue.h: strcmp字符串比较、strlen取字符串长度、strtok字符串分割函数;

​ 基本io函数以及dup2closechdirgetcwd获得当前目录、execvp进程运行参数替换程序、fork 创建进程函

fgetswaitpid(系统调用,用于等待子进程结束、获取子进程的运行状态,本实验仅仅用它使父进程等待子进程结束)exit退出函数

fcntl.h: 用到文件权限位、文件打开函数

errno.h:

​ 用到errnostrerror出错函数、出错标志处理;

  • 缓冲区长度(也可以有sysconf函数得出),最大参数个数,提示符最大长度按实验要求,均设置为MAX=4096

2.3提示字:

image-20201108221216379
image-20201108221216379

[Testshell 绝对路径]$,仿unix shell风格

组织提示字函数:

代码语言:javascript
复制
char pre[MAX+10]="[Testshell ";
void printpre(char* s)
{
    if(getcwd(s+11,MAX)==NULL)
    {
        printf("getcwd error: %s\n", strerror(errno));
        exit(1);
    }
    strcat(s, "]$");
}

很简单的思路,调用getcwd得到当前目录存在s+11位置之后,("[Testshell "是11个字符),最后再使用strcat将后半部分提示字拼接到后面~

2.4 构建argv
ISO C标准规定的string.h头文件中,包括了strtok这一函数,因此我们完全可以直接调用该函数对字符串进行切分,而无需手动操作,但如果不调用strtok函数,需要进行复杂的字符串模拟,可以编写如下:
代码语言:javascript
复制
//Filter commands for extra Spaces 
int argc=0,len,i;
while(cmd[i] == ' ')i++;//Remove the leading space
argv[argc++]=cmd+i;  //first position of space 
for(len=strlen(cmd),i=0;i<len;i++) 
{
  if (cmd[i]==' ')cmd[i] = 0;
	else 
  {
    //Fill in the command parameters 
    if((i-1>=0)&&(!buf[i-1]==0)) argv[argc++] = cmd+i;
  }
}
argv[argc] = NULL;//last argv is NULL

调用strtok,很方便就可以提取出输入指令参数:

代码语言:javascript
复制
char *token = strtok(cmd, " ");
while (token != NULL)
{
    ...
    token = strtok(NULL, " ");
}

由于本Shell还具有输入输出重定向功能,因此需要处理< filename> filename的情况。因此在token<>时,设置相应的flag:0为正常参数,1代表输入重定向,2代表输出重定向。之后,下一个读入的参数token会根据flag的值设置重定向的输入文件名rfile和重定向的输出文件名wfile

将上述功能封装成construct_argv函数,完整代码如下:

代码语言:javascript
复制
void construct_argv(char *cmd, char **argv, char **rfile, char **wfile)
{
    int flag = 0; // 1 for rfile, 2 for wfile
    *rfile = NULL;
    *wfile = NULL;

    cmd[strlen(cmd) - 1] = '\0';
    int argc = 0;
    char *token = strtok(cmd, " ");
    while (token != NULL)
    {
        if (flag == 1)
        {
            *rfile = token;
            flag = 0;
        }
        else if (flag == 2)
        {
            *wfile = token;
            flag = 0;
        }
        else if (strcmp(token, "<") == 0)
            flag = 1;
        else if (strcmp(token, ">") == 0)
            flag = 2;
        else
            argv[argc++] = token;
        token = strtok(NULL, " ");
    }
    argv[argc] = NULL;
}

2.5 exec函数族:

代码语言:javascript
复制
1.execl   int execl(const char *path, const char *arg, ...);
2.execlp  int execlp(const char *file, const char *arg, ...);
3.execv   int execv(const char *path, char *const argv[]);
4.execvp  int execvp(const char *file, char *const argv[]);

5.execle  int execle(const char *path, const char *arg,
                  ..., char * const envp[]);
6.execvpe int execvpe(const char *file, char *const argv[],
                  char *const envp[]);         

**path:**可执行文件的路径名字

**arg:**可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束。

**file:**如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件

excel 、execv都是需要给出可执行文件名的绝对路径,execlp、execvp则不需要,它们两者的区别是execvp函数参数是一个argv参数表,而execlp是一项一项给出参数,这些函数族最终都是调用execve系统调用。

后缀名总结:

l:表示list,即每个命令行参数都说明为一个单独的参数

v:表示vector,命令行参数放在数组中

e:调用者提供环境表

p:表示通过环境变量PATH,查找执行文件

f:表示以文件描述符为第一个参数

image-20201109154833086
image-20201109154833086

一般命令都可以由fork+execvp执行,由fork创建一个子进程,调用一种exec函数时,该进程执行的程序完全替换为新程序

而新程序则从其main函数开始执行; 但要注意exec并不创建新进程,所以前后的进程ID并未改变,exec只是用一个全新的程序替换了

当前进程的正文、数据、堆和栈段。

因此,我们可以使用execvp函数,仅需传入之前构造的argv参数,从而间接执行系统调用execve

代码语言:javascript
复制
if ((pid=fork())<0)printf("fork error: %s\n", strerror(errno));
else if(pid==0) // child
{
  redirect_stdin(rfile);
  redirect_stdout(wfile);
  execvp(argv[0], argv);
  printf("execvp error: %s\n", strerror(errno));
  exit(1);
}
else if((pid=waitpid(pid,&status,0))<0) // parent
	printf("waitpid error: %s\n", strerror(errno));

代码中先调用fork创建子进程若出错则打印出错信息,pid=0表示在子进程中,若有重定向输入输出,则在redirect_stdin或

redirect_stdout中处理,execvp填入可执行文件参数,子进程开始执行,若出错才会执行下面的execvp error打印错误语句,waitpid等

待特定fork后子进程号结束,若出错则同样做出错打印信息处理

2.6 cd命令

对于一个自制shell如果没有cd命令就不算一个合格的shell,因为cd是shell内部命令,如果用execve系统调用,fork出子进程改变的是子

进程的目录,父进程的目录仍然没有发生改变。所以本实验中如果不做特殊处理,cd命令不会成功运行,需要手动编写一个简单函数,思

路也很简单,对于一般的cd 路径名,我们可以采用chdir函数切换到相应目录,注意到一般shell有cdcd ~,两种形式,我们可以特

判将参数argv[1]等于使用getenv("HOME")获取家目录的环境变量:

代码语言:javascript
复制
if (!strcmp(argv[0],"cd")) // cd command
{
  if (argv[1]==NULL||!strcmp(argv[1],"~"))argv[1]=getenv("HOME");
  if(chdir(argv[1])<0)printf("chdir error: %s\n", strerror(errno));
}
2.7 输入输出重定向

在执行其他命令时,调用了自己写的redirect_stdinredirect_stdout两个函数。这两个函数通过open命令,将之前获取的rfile

wfile文件打开,获取File descriptor后,再使用dup2函数重定向STDIN_FILENOSTDOUT_FILENO,open函数采用权限位为644:

代码语言:javascript
复制
void redirect_stdin(char *rfile)
{
    int readfd;
    if (rfile!=NULL)
    {
        if((readfd=open(rfile,O_RDONLY))<0)
        {
            printf("open error: %s\n", strerror(errno));
            exit(1);
        }
        if(dup2(readfd,STDIN_FILENO)<0)
        {
            printf("dup2 error: %s\n", strerror(errno));
            exit(1);
        }
        close(readfd);
    }
}

void redirect_stdout(char *wfile)
{
    int writefd;
    if (wfile!=NULL)
    {
        if ((writefd=open(wfile, O_WRONLY|O_CREAT|O_TRUNC,644)) < 0)
        {
            printf("open error: %s\n", strerror(errno));
            exit(1);
        }
        if (dup2(writefd,STDOUT_FILENO)<0)
        {
            printf("dup2 error: %s\n", strerror(errno));
            exit(1);
        }
        close(writefd);
    }
}
3.实验结果

3.1编译运行:

代码语言:javascript
复制
gcc shell.c -o shell
image-20201109165046309
image-20201109165046309

编译成功、提示字正常显示

3.2 测试常见命令

cdpwdwhomkdirrm -rvicd带参数、catls带参数:

image-20201109172222094
image-20201109172222094

可以看到,这个自制shell基本能够处理大多数命令,能够成功解析出参数,包括输入输出重定向等更复杂的命令,完成了本实验需要做的基本操作,其中拓展了cd命令以及重定向输入输出命令。

4.实验不足与心得

实验不足:毕竟是套壳的shell,没有正宗shell支持tab补全、回溯上一条命令,复制粘贴、退出输入输出、特定类型高亮等快捷方式。

实验心得:哪怕是一个小小的shell黑窗体,开发难度都不容小觑,要考虑到多种情况,比如用户的输入输出、随着支持功能的拓展,复杂性激增,需要团队协作、科学的软件工程理念指导才能开发出可移植性强、功能丰富的shell。

附,shell.c:

代码语言:javascript
复制
//
//  main.cpp
//  shell
//
//  Created by apple on 2020/11/5.
//
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#define MAX 4096



char pre[MAX+10]="[Testshell ";
char buf[MAX];
char *argv[MAX];
char *rfile, *wfile;
int status;
pid_t pid;
void readgrgv(char *buf, char **argv, char **rfile, char **wfile);
void printpre(char* s);
void redirect_stdin(char *rfile);
void redirect_stdout(char *wfile);



int main()
{
    printpre(pre);
    printf("%s",pre);
    while(fgets(buf,MAX-1,stdin)!=NULL)
    {
        readgrgv(buf,argv,&rfile,&wfile);
        if (!strcmp(argv[0],"cd")) // cd command
        {
            if (argv[1]==NULL||!strcmp(argv[1],"~"))
            {
                argv[1]=getenv("HOME");
            }
            if(chdir(argv[1])<0)printf("chdir error: %s\n", strerror(errno));
        }
        else // other command
        {
            if ((pid=fork())<0)printf("fork error: %s\n", strerror(errno));
            else if(pid==0) // child
            {
                redirect_stdin(rfile);
                redirect_stdout(wfile);
                execvp(argv[0], argv);
                printf("execvp error: %s\n", strerror(errno));
                exit(1);
            }
            else if((pid=waitpid(pid,&status,0))<0) // parent
                printf("waitpid error: %s\n", strerror(errno));
        }
        printpre(pre);
        printf("%s",pre);
    }
    return 0;
}

void printpre(char* s)
{
    if(getcwd(s+11,MAX)==NULL)
    {
        printf("getcwd error: %s\n", strerror(errno));
        exit(1);
    }
    strcat(s,"]$");
}
void readgrgv(char *buf, char **argv, char **rfile, char **wfile)
{
    int flag = 0; // 1 for rfile, 2 for wfile
    *rfile=NULL;
    *wfile=NULL;
    buf[strlen(buf)-1] = '\0';
    int argc = 0;
    char *token = strtok(buf, " ");
    while(token!=NULL)
    {
        if(flag==1)
        {
            *rfile=token;
            flag = 0;
        }
        else if (flag==2)
        {
            *wfile=token;
            flag=0;
        }
        else if (strcmp(token,"<")==0)
            flag = 1;
        else if (strcmp(token, ">") == 0)
            flag = 2;
        else
            argv[argc++] = token;
        token = strtok(NULL, " ");
    }
    argv[argc]=NULL;
}

void redirect_stdin(char *rfile)
{
    int readfd;
    if (rfile!=NULL)
    {
        if((readfd=open(rfile,O_RDONLY))<0)
        {
            printf("open error: %s\n", strerror(errno));
            exit(1);
        }
        if(dup2(readfd,STDIN_FILENO)<0)
        {
            printf("dup2 error: %s\n", strerror(errno));
            exit(1);
        }
        close(readfd);
    }
}

void redirect_stdout(char *wfile)
{
    int writefd;
    if (wfile!=NULL)
    {
        if ((writefd=open(wfile, O_WRONLY|O_CREAT|O_TRUNC,644)) < 0)
        {
            printf("open error: %s\n", strerror(errno));
            exit(1);
        }
        if (dup2(writefd, STDOUT_FILENO) < 0)
        {
            printf("dup2 error: %s\n", strerror(errno));
            exit(1);
        }
        close(writefd);
    }
}
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020-11-24 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 实验三 实现带参数的简单Shell
    • 1. 实验内容
      • 2. 实验设计与实现
      相关产品与服务
      Prowork 团队协同
      ProWork 团队协同(以下简称 ProWork )是便捷高效的协同平台,为团队中的不同角色提供支持。团队成员可以通过日历、清单来规划每⽇的工作,同时管理者也可以通过统计报表随时掌握团队状况。ProWork 摒弃了僵化的流程,通过灵活轻量的任务管理体系,满足不同团队的实际情况,目前 ProWork 所有功能均可免费使用。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档