项目--编写bash及常见命令的代码实现

一、项目概述

    在linux终端下我们可以看到用户的相关信息和主机信息等,并且在终端下,可以操作很多的命令(ls、ps、pwd等),并且在终端下可以运行自己的程序。因此本项目实现以下功能:

  • 打印用户、主机等相关信息
  • 可以在mybash中操作相关命令
  • 各种命令的代码实现

二、显示信息

Linux系统下,打开一个终端后会出现以下界面,其中包括用户信息、主机信息和当前位置信息,首先实现这个功能,做到如下输出。

1、获得用户信息(终端中只需打印用户名和用户的权限(管理员打印'#'  普通用户打印'$'))

     实现原理:

  • 每个用户有自己对应的用户标识符UID,可以通过函数getuid来获得当前用户的UID。

getuid函数原型:

# include <stdio.h>
# include <sys/type.h>

//UID的数据类型为uid_t
uid_t getuid(void);
  • 系统文件/etc/passwd包含一个用户账户数据库。它由行组成,每一行对应一个用户,包括用户名、加密口令、用户标识符、组标识符、全名、家目录和默认shell。
  • 可以通过getpwuid函数来获得当前用户的相关信息。

getpwuid函数原型:

# include <sys/types.h>
# include <pwd.h>

//用户的相关信息存储在结构体passwd中
/*
passwd结构体中有以下成员:
    char *pw_name    用户登录名
    uid_t pw_uid     用户标识符
    gid-t pw_gid     组标识符
    char *pw_dir     用户家目录
    chae *gecos      用户全名
    char *shell      用户默认shell
*/
struct passwd *getpwuid(uid_t uid);//函数参数为用户标识符
  • 获得了用户信息passwd之后,用户名就在该结构体中。
  • 如果用户的UID、GID都为0,则该用户为管理员用户,否则为普通用户

2、打印主机信息(终端只需打印主机名)

     实现原理:

  • gethostname函数可以获取主机名

gethostname函数原型:

# include <stdio.h>
int gethostname(char *name, size_t namelen);//将主机名写出字符串数组name中

3、获取当前位置(终端只需输入当前路径的最后一项,并非完整路径)

实现原理:

  • 通过getcwd函数获取当前的绝对路径
  • 从路径尾部向前遍历站到绝对路径中的最后一项

4、代码实现

void print_sys_info()
{
    char buff[128] = {0};    //用来存储要打印到终端的信息
    strcpy (buff,"[");
     
    struct passwd *pwd;
    pwd = getpwuid(getuid());    //获取用户信息
    if(pwd == NULL)
    {
        printf("mybash>>");
        fflush(stdout);
        return ;
    }
    strcat(buff,pwd->pw_name);    //获取用户名
    strcat(buff,"@");

    char hostname[128] = {0};
    gethostname(hostname,128);    //获取主机名

    strcat(buff,hostname);
    strcat(buff," ");

    char cur_dir[128] = {0};

    getcwd(cur_dir,128);    //获取当前位置的绝对路径

    int i = 0;
    while(cur_dir[i] != '\0')    //遍历到绝对路径尾部
    {
        i++;
    }
    while(cur_dir[i] != '/')    //从绝对路径尾部往前找第一个'/'
    {
        i--;
    }
    i++;
    char cur_tmp[128] = {0};
    int j = 0;

    while(cur_dir[i] != '\0')    //获取绝对路径的最后一项
    {
        cur_tmp[j] = cur_dir[i];
        i++;
        j++;
    }
    cur_tmp[j] = '\0';   

    strcat(buff,cur_tmp);

    
    if (pwd->pw_uid == 0 && pwd->pw_gid ==0)    判断用户是否为管理员用户
    {
        strcat(buff,"]#");
    }
    else
    {
        strcat(buff,"]$");
    }

    printf("%s",buff);
    fflush(stdout);    //输出
}

三、主功能块

linux系统下bash终端可以执行其他命令或程序,核心原理是进程的复制和替换,具体步骤如下:

  • bash进程作为父进程,bash进程设置循环永远存在
  • 如果要执行相关命令操作,bash进程复制一个子进程
  • 用相关命令的操作来替换该子进程,可以实现相关命令的操作
  • 子进程实现完功能后退出,返回到父进程(即bash进程)

注意1:

  • linux操作系统中常见命令执行时不需要输入绝对路径,因为其存在默认路径/etc/bin下,执行时内核会自动在该目录下查找命令的可执行程序
  • 在本项目中,我们希望在bash中执行的也是自己实现的命令,因此,我们需要重新设置默认路径

注意2:

  • cd命令不需要通过进程的复制替换实现,直接调用系统调用chdir即可

注意3:

  • 需设置退出mybash的条件
  • 当输入"exit"时,退出mybash

代码如下:

# include <stdio.h>
# include <stdlib.h>
# include <assert.h>
# include <string.h>
# include <unistd.h>
# include <error.h>
# include <fcntl.h>
# include <pwd.h>

#define MAX 10
#define PATH "/home/FUJIA/cy1706/bash/mybin/"  //可执行程序默认存储路径
int main()
{
    while(1)
    {
        char buff[128] = {0};    //用来存储用户输入的命令

        print_sys_info();    //打印显示信息

        fgets(buff,128,stdin);    //获取用户输入
        buff[strlen(buff) - 1] = 0;    //删除结尾'\0'

        char * myargv[MAX] = {0};    //存储命令参数
        
        char * s = strtok(buff," ");    //通过 " " 来对用户输入进行分割
        if (s == NULL)
        {
            continue;
        }
        myargv[0] = s; 
        int i = 1;

        while( (s = strtok(NULL," ")) != NULL)    //依次将参数存入到myargv中
        {
            myargv[i++] = s;
        }
        
        if (strcmp(myargv[0],"exit") == 0)    //如果输入 exit 则退出mybash
        {
            break;
        }
        else if (strcmp(myargv[0],"cd") == 0)    //如果输入的命令是 cd 则直接调用chdir即可
        {
            chdir(myargv[1]);
            continue;
        }

        pid_t pid = fork();    //复制子进程
        assert(pid != -1);

        if(pid == 0)    //子进程完成进程的替换
        {
            char path[256] = {0};
            //如果用户输入没有给定路径,则将默认路径传入
            if (strncmp(myargv[0],"./",2) != 0 && strncmp(myargv[0],"/",1) != 0)
            {
                strcpy(path,PATH);
            }
            //将路径与命令名连接
            strcat(path,myargv[0]);

            //完成进程的替换
            execv(path,myargv);
            perror("execvp error");
            exit(0);
        }

        wait(NULL);
    }

    exit(0);
}   

四、常见命令的实现

   在linux系统中对常见命令的操作是的bash非常实用,只有单独的bash并不能体现出其优势,linux中常见命令的可执行文件在/etc/bin中,本项目也实现了一些常见的命令,是的bash更加完善

1、pwd命令的实现

  • 通过调用getcwd函数即可

代码如下:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <assert.h>

int main()
{
    char buff[256] = {0};

    getcwd(buff,256);    //获得当前路径

    printf("mypwd:%s\n",buff);    //输出路径

    exit(0);
}

2、ls命令的实现

  扫描目录也存在一套完整的库函数,可以像操作文件一样对目录进行扫描。与目录操作有关的函数在dirent.h头文件中声明。与文件类似,文件的数据类型为FILE,目录的数据类型为DIR。与文件操作类似的还有目录的操作也分为打开目录,操作目录,关闭目录三步。

注意:

  • struct stat是linux中用来描述文件属性的结构
  • stat函数是用来获取文件状态
  • S_ISDIR函数用来判断文件是否为目录的

代码如下:

# include <stdio.h>
# include <unistd.h>
# include <string.h>
# include <dirent.h>
# include <pwd.h>
# include <stdlib.h>
# include <fcntl.h>
# include <sys/stat.h>

int main()
{
    char path[256] = {0};
    getcwd(path,256);

    DIR *pdir = opendir(path);    //打开目录
    if(pdir == NULL)
    {
        perror("opendir error");
        exit(0);
    }

    struct dirent * p = NULL;
    struct stat st;    //用来存储文件信息
    while((p = readdir(pdir)) != NULL)    //遍历目录流
    {
        if(strncmp(p -> d_name,".",1) == 0)    //目录流中存在 . 表示当前目录
        {
            continue;
        }
        stat(p->d_name,&st);    //将文件名p->d_name的文件存储到st中
        if ( S_ISDIR(st.st_mode))    //该文件为目录,以以下格式输出
        {
            printf("\033[1;34m%s  \033[0m", p->d_name);
        }
        else
        {
            if(st.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))    //该文件为可执行文件
            {
                printf("\033[1;32m%s  \033[0m", p->d_name);
            }
            else
            {
                printf("%s  ", p->d_name);
            }
        }
    }

    closedir(pdir);    //关闭目录
    printf("\n");

    exit(0);
}

3、clear命令的实现

  • 在终端中,ANSI定义了用于屏幕显示的Escape屏幕控制码,在printf函数调用时会以特定颜色或格式输出。
  • ANSI中\033[2J  表示清屏
  • \033[0;0H          表示光标置顶

代码如下:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <assert.h>

int main()
{
    printf("\033[2J\033[0;0H");

    exit(0);
}

4、su命令的实现

实现原理:

  • 首先获得要切换到的用户x的信息
  • 复制一个子进程b
  • 用x的uid和gid改变子进程的uid和gid。
  • 将b进程的家目录用x的家目录替换,用setenv函数实现,此时已经处于x用户状态
  • 调用x用户的shell终端(替换进程),即可切换到x用户

注意:

  • 如果su命令后没有别的参数,即为切换到管理员用户

代码如下:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <string.h>
# include <pwd.h>
# include <assert.h>

int main(int argc,char *argv[])
{
    char *name = "root";    //默认指定要切换的用户是管理员用户
    if(argc == 2)    //当有两个参数的时候,说明su明后后边有指定要切换到的用户
    {
        name = argv[1];
    }
    struct passwd *p = getpwnam(name);    //获取用户信息
    if(p == NULL)
    {
        perror("su error!");
        exit(0);
    }

    pid_t pid = fork();    //复制进程
    assert(pid != -1);

    if(pid == 0)    //进人子进程
    {
        setgid(p->pw_gid);    //修改当前进程的gid
        setuid(p->pw_uid);    //修改当前进程的uid
        
        setenv("HOME",p->pw_dir,1);    //修改环境变量,指定当前处于要切换到的用户的家目录下

        execl(p->pw_shell,"p->pw_shell",(char*)0);    //用p进程的终端替换该进程,使当前处于p用户的shell下,完成切换功能
        perror("su execl errer!");
        exit(0);
    }

    wait(NULL);
    exit(0);    
}

5、ps命令的实现

  • 实现原理

进程的信息存储在/proc目录下,/proc目录中以数字命名的文件存储着一个进程的所有信息,通过stat系统调用可以获得文件中进程的信息

  • 程序流程图

  • stat结构体和stat函数

       (1)stat结构体

stat是文件(夹)信息的结构体,定义如下:

struct stat  
{   
    dev_t       st_dev;     /* ID of device containing file -文件所在设备的ID*/  
    ino_t       st_ino;     /* inode number -inode节点号*/    
    mode_t      st_mode;    /* protection -保护模式?*/    
    nlink_t     st_nlink;   /* number of hard links -链向此文件的连接数(硬连接)*/    
    uid_t       st_uid;     /* user ID of owner -user id*/    
    gid_t       st_gid;     /* group ID of owner - group id*/    
    dev_t       st_rdev;    /* device ID (if special file) -设备号,针对设备文件*/    
    off_t       st_size;    /* total size, in bytes -文件大小,字节为单位*/    
    blksize_t   st_blksize; /* blocksize for filesystem I/O -系统块的大小*/    
    blkcnt_t    st_blocks;  /* number of blocks allocated -文件所占块数*/    
    time_t      st_atime;   /* time of last access -最近存取时间*/    
    time_t      st_mtime;   /* time of last modification -最近修改时间*/    
    time_t      st_ctime;   /* time of last status change - */    
};  

     (2)stat函数

获取指定路径文件的信息,函数原型如下:

#include <sys/types.h>    
#include <sys/stat.h>   
int stat(
  const char *filename    //文件或者文件夹的路径
  , struct stat *buf      //获取的信息保存在内存中
);  

返回值:正确返回0
       错误返回-1  
  • sprintf函数

将数据格式化输入到字符串。

代码如下:

# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <sys/stat.h>
# include <pwd.h>
# include <sys/types.h>
# include <dirent.h>
# include <string.h>


# define MAX_LEN 20
typedef struct ps_info
{
    char pname[MAX_LEN];
    char user[MAX_LEN];
    int pid;
    int ppid;
    char state;
    struct ps_info *next;
}myps;
void uid_to_name(uid_t uid, struct ps_info *p1);    //根据进程uid获取进程属主
myps *trav_dir(char dir[]);    //获取进程信息,将所有进程信息存储在链表中
int read_info(char d_name[], struct ps_info *p1);    //根据文件名获取某一进城的信息
void print_ps(struct ps_info *head);    //打印进程信息
int is_num(char p_name[]);    //判断文件名是否为数字

myps *trav_dir(char dir[])
{
    DIR *dir_ptr;
    myps *head, *p1, *p2;
    struct dirent *direntp;
    struct stat infobuf;

    if((dir_ptr = opendir(dir)) == NULL)    //打开目录
    {
        printf("open dir error");
    }
    else
    {
        head = (struct ps_info *)malloc(sizeof(struct ps_info ));    //创建链表
        p1 = head;
        p2 = head;
        while((direntp = readdir(dir_ptr)) != NULL)    //遍历该目录
        {
            if((is_num(direntp->d_name)) == 0)    //该文件是数字
            {
                if(p1 == NULL)
                {
                    printf("malloc error\n");
                    exit(0);
                }
                if(read_info(direntp->d_name,p1) != 0)    //获取当前进程信息
                {
                    printf("read_info error");
                    exit(0);
                }
                //将该进程节点插入链表
                p2->next = p1;   
                p2 = p1;
                p1 = (struct ps_info*)malloc(sizeof(struct ps_info));
            }
        }
    }
    p2->next = NULL;
    return head;    //返回链表头结点
}

int read_info(char d_name[], struct ps_info *p1)
{
    FILE *fd;
    char dir[20];
    struct stat infobuf;

    sprintf(dir,"%s%s","/proc/",d_name);    //将文件名和文件路径连接组成绝对路径存储到dir中
    chdir("/proc");    //切换到proc目录
    if(stat(d_name,&infobuf) == -1)    //获取进程(文件)信息失败
    {
        printf("stat errro");
    }
    else
    {
        uid_to_name(infobuf.st_uid,p1);    //获取进程属主
    }

    chdir(dir);    //切换到该进程目录下
    if((fd = fopen("stat","r")) < 0)    //打开文件失败
    {
        printf("open the file error\n");
        exit(0);
    }

    while(4 == fscanf(fd,"%d %s %c %d\n",&(p1->pid), p1->pname, &(p1->state), &(p1->ppid)))    //从文件中读取进程各项信息
    {
        break;
    }
    fclose(fd);    //关闭文件
    return 0;
}

void uid_to_name(uid_t uid, struct ps_info *p1)    //获取文件属主
{
    struct passwd *getpwuid(),*pw_ptr;
    static char numstr[10];
    
    if((pw_ptr = getpwuid(uid)) == NULL)    //通过uid获取用户信息
    {
        sprintf(numstr,"d",uid);
        strcpy(p1->user,numstr);
    }
    else
    {
        strcpy(p1->user,pw_ptr->pw_name);    //获取用户名
    }
}

int is_num(char p_name[])    //判断文件是否是数字
{
    int i,len;
    len = strlen(p_name);
    if(len == 0)
    {
        return -1;
    }
    for(i = 0; i <len; i++)
    {
        if(p_name[i] < '0' || p_name[i] > '9')    //文件名的每一位都为数字才可
        {
            return -1;
        }
    }
    return 0;
}

void print_ps(struct ps_info *head)    //打印进程信息
{
    myps *list;
    printf("user\t\tPID\tSTATE\tPNAME\n");
    for(list = head; list != NULL; list = list->next)    //遍历链表
    {
        printf("%s\t\t%d\t%c\t%s\n",list->user,list->pid,list->state,list->pname);
    }
}

int main()
{
    myps *head;
    myps *link;

    head = trav_dir("/proc/");
    if(head == NULL)
    {
        printf("traverse dir error\n");
    }
    print_ps(head);

    while(head != NULL)    //释放链表
    {
        link = head;
        head = head->next;
        free(link);
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_41727218/article/details/81408687
今日推荐