如何使用Shell实现其基础功能?

2026-05-22 11:281阅读0评论SEO资讯
  • 内容介绍
  • 文章标签
  • 相关推荐

本文共计1440个文字,预计阅读时间需要6分钟。

如何使用Shell实现其基础功能?

在Linux系统中,使用C语言编写一个可运行和终止程序的Shell脚本如下:

shell#!/bin/bash

定义一个C程序编译和运行的基本功能函数compile_and_run() { # 编译C程序 gcc -o program program.c # 运行编译后的程序 ./program # 终止程序 kill %1}

调用函数compile_and_run

该脚本首先定义了一个名为`compile_and_run`的函数,该函数使用gcc编译C程序,并运行编译后的程序,然后使用`kill`命令终止程序。最后,脚本调用这个函数执行操作。

本文介绍如何在Linux系统下使用C语言编写一个能运行和终止程序的Shell

独立博客阅读地址:panqiincs.me/2017/02/26/write-a-shell-basic-functionality/

Shell的功能

Shell是操作系统中管理进程和运行程序的程序。所有常用的shell都有三个主要功能:

  • 运行程序:grep, date, ls等都是一些普通的程序,shell将它们载入内存并运行它们
  • 管理输入和输出:使用<,>和 | 符号可以将输入、输出重定向。这样就可以告诉shell将进程的输入和输出连接到一个文件或是其他的进程
  • 可编程:shell同时也是带有变量和流程控制的编程语言

本文将介绍如何编写包含最基本功能,即能运行和终止程序的shell。

Shell的主流程

Shell打印提示符、输入命令、运行命令,如此反复。因此,主流程由下面的循环组成:

如何使用Shell实现其基础功能?

for (;;) { print_prompt(prompt_str); handle_input(arg_buf, arg_list); run_cmd(arg_list); } 打印提示符

下面是一个提示符的示例:

[psh]krist@linux-szhw:/home/krist/Workspace$

加上前缀[psh]以和系统自带的shell区别开来。后面依次是用户名(用getlogin_r函数获取)、主机名(用gethostname函数获取)、当前目录(用getcwd函数获取)和命令提示符。如果当前登录的用户是root,命令提示符为#,普通用户则打印美元符号$,因此需要用geteuid函数获取当前的Effective User ID。

处理用户输入

一般需要限制用户单次输入的最多字符数MAX_ARG_LEN以及参数的最大个数MAX_ARG_NUM。用户输入命令,按下回车键即表示当前的命令输入完毕。用getline函数读取一行用户输入,然后将一整条命令拆分。例如命令ls -al /home拆分之后变成ls-al/home三个字符串,第一个字符串是命令的名称,后面依次是命令参数。拆分字符使用strsep函数,分割符(delimeter)为空格(Space)。下面是拆分过程的代码:

stringp = input_buf; arg_num = 0; // number of arguments while (((token = strsep(&stringp, " ")) != NULL) && (arg_num < MAX_ARG_NUM)) { // Token is terminated by overwriting the delimiter with a null // byte('\0'), so continuous space will result in a token with // only a null byte, skip it. if (strcmp(token, "") != 0) { arg_vec[arg_num] = token; arg_num++; } } arg_vec[arg_num] = NULL;

其中input_buf是一个字符数组,存放用户输入的单条命令字符串,arg_vec是字符串数组,存放拆分后的多个字符串。

运行命令 fork和exec

想必大家都熟悉shell运行命令的流程:用户输入一条命令后,父进程会fork一个子进程,在子进程中使用exec函数运行用户输入的命令,父进程等待子进程退出后,等待用户输入下一条命令,如此反复。代码如下:

void exec_cmd(char **arg_vec) { int status; pid_t pid; pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // child process, run the command // execvp() returns only if an error occurs execvp(arg_vec[0], arg_vec); perror("execvp"); exit(EXIT_FAILURE); } else { // parent process, wait for children to exit while (wait(&status) != pid) ; } }

exec函数毫无疑问选择最方便的execvp,主进程使用wait函数等待子进程退出。

内建命令

这个时候一个可以跑的shell就完成了。但是却无法运行cd命令,提示错误如下:

execvp: No such file or directory

运行whereis cd命令,并没有显示可执行文件的路径。原来cd命令是内建命令(build in command),需要在shell中自己实现,而且它的执行不需要建立子进程,需要额外判断和处理。

我写了一个函数判断是否是内建命令,如果不是内建命令则返回-1,否则按照内建命令的方式运行。目前只实现了cdexit两个内建命令。代码如下:

int builtin_cmd(char **arg_vec) { if (strcmp(arg_vec[0], "cd") == 0) { // cd if ((arg_vec[1] != NULL) && (arg_vec[2] == NULL)) { if (chdir(arg_vec[1]) == -1) { perror("cd"); } } else { printf("Usage: cd [directory]\n"); } return 0; } else if (strcmp(arg_vec[0], "exit") == 0) { // exit exit(EXIT_SUCCESS); } return -1; }

此时,shell主循环中的run_cmd函数演变为:

void run_cmd(char **arg_vec) { if (arg_vec[0] == NULL) { return; } if (builtin_cmd(arg_vec) == -1) { // run build-in commands exec_cmd(arg_vec); // run normal commands } } 处理信号

我们期望shell能够处理信号。在shell的命令行中敲命令运行程序,在程序还在运行时按下Ctrl-CCtrl-\键(分别产生SIGINTSIGQUIT信号),运行的程序会退出,即中断运行子进程;在shell的命令行中不运行命令,直接按下这两个键组合,不能退出shell程序,即父进程不会中断运行,退出shell程序只能通过Ctrl-D按键。实现上述功能需要在父进程中忽略信号SIGINTSIGQUIT,但是在子进程中恢复对信号SIGINTSIGQUIT的默认操作。代码很简单,在这里不列出。

这样,一个具备基本功能的shell就完成了,完整的代码请参考这里。请继续看下一篇:Shell实现:重定向和管道。

参考
  1. The Linux Programming Interface
  2. Unix/Linux编程实践教程

本文共计1440个文字,预计阅读时间需要6分钟。

如何使用Shell实现其基础功能?

在Linux系统中,使用C语言编写一个可运行和终止程序的Shell脚本如下:

shell#!/bin/bash

定义一个C程序编译和运行的基本功能函数compile_and_run() { # 编译C程序 gcc -o program program.c # 运行编译后的程序 ./program # 终止程序 kill %1}

调用函数compile_and_run

该脚本首先定义了一个名为`compile_and_run`的函数,该函数使用gcc编译C程序,并运行编译后的程序,然后使用`kill`命令终止程序。最后,脚本调用这个函数执行操作。

本文介绍如何在Linux系统下使用C语言编写一个能运行和终止程序的Shell

独立博客阅读地址:panqiincs.me/2017/02/26/write-a-shell-basic-functionality/

Shell的功能

Shell是操作系统中管理进程和运行程序的程序。所有常用的shell都有三个主要功能:

  • 运行程序:grep, date, ls等都是一些普通的程序,shell将它们载入内存并运行它们
  • 管理输入和输出:使用<,>和 | 符号可以将输入、输出重定向。这样就可以告诉shell将进程的输入和输出连接到一个文件或是其他的进程
  • 可编程:shell同时也是带有变量和流程控制的编程语言

本文将介绍如何编写包含最基本功能,即能运行和终止程序的shell。

Shell的主流程

Shell打印提示符、输入命令、运行命令,如此反复。因此,主流程由下面的循环组成:

如何使用Shell实现其基础功能?

for (;;) { print_prompt(prompt_str); handle_input(arg_buf, arg_list); run_cmd(arg_list); } 打印提示符

下面是一个提示符的示例:

[psh]krist@linux-szhw:/home/krist/Workspace$

加上前缀[psh]以和系统自带的shell区别开来。后面依次是用户名(用getlogin_r函数获取)、主机名(用gethostname函数获取)、当前目录(用getcwd函数获取)和命令提示符。如果当前登录的用户是root,命令提示符为#,普通用户则打印美元符号$,因此需要用geteuid函数获取当前的Effective User ID。

处理用户输入

一般需要限制用户单次输入的最多字符数MAX_ARG_LEN以及参数的最大个数MAX_ARG_NUM。用户输入命令,按下回车键即表示当前的命令输入完毕。用getline函数读取一行用户输入,然后将一整条命令拆分。例如命令ls -al /home拆分之后变成ls-al/home三个字符串,第一个字符串是命令的名称,后面依次是命令参数。拆分字符使用strsep函数,分割符(delimeter)为空格(Space)。下面是拆分过程的代码:

stringp = input_buf; arg_num = 0; // number of arguments while (((token = strsep(&stringp, " ")) != NULL) && (arg_num < MAX_ARG_NUM)) { // Token is terminated by overwriting the delimiter with a null // byte('\0'), so continuous space will result in a token with // only a null byte, skip it. if (strcmp(token, "") != 0) { arg_vec[arg_num] = token; arg_num++; } } arg_vec[arg_num] = NULL;

其中input_buf是一个字符数组,存放用户输入的单条命令字符串,arg_vec是字符串数组,存放拆分后的多个字符串。

运行命令 fork和exec

想必大家都熟悉shell运行命令的流程:用户输入一条命令后,父进程会fork一个子进程,在子进程中使用exec函数运行用户输入的命令,父进程等待子进程退出后,等待用户输入下一条命令,如此反复。代码如下:

void exec_cmd(char **arg_vec) { int status; pid_t pid; pid = fork(); if (pid == -1) { perror("fork"); exit(EXIT_FAILURE); } else if (pid == 0) { // child process, run the command // execvp() returns only if an error occurs execvp(arg_vec[0], arg_vec); perror("execvp"); exit(EXIT_FAILURE); } else { // parent process, wait for children to exit while (wait(&status) != pid) ; } }

exec函数毫无疑问选择最方便的execvp,主进程使用wait函数等待子进程退出。

内建命令

这个时候一个可以跑的shell就完成了。但是却无法运行cd命令,提示错误如下:

execvp: No such file or directory

运行whereis cd命令,并没有显示可执行文件的路径。原来cd命令是内建命令(build in command),需要在shell中自己实现,而且它的执行不需要建立子进程,需要额外判断和处理。

我写了一个函数判断是否是内建命令,如果不是内建命令则返回-1,否则按照内建命令的方式运行。目前只实现了cdexit两个内建命令。代码如下:

int builtin_cmd(char **arg_vec) { if (strcmp(arg_vec[0], "cd") == 0) { // cd if ((arg_vec[1] != NULL) && (arg_vec[2] == NULL)) { if (chdir(arg_vec[1]) == -1) { perror("cd"); } } else { printf("Usage: cd [directory]\n"); } return 0; } else if (strcmp(arg_vec[0], "exit") == 0) { // exit exit(EXIT_SUCCESS); } return -1; }

此时,shell主循环中的run_cmd函数演变为:

void run_cmd(char **arg_vec) { if (arg_vec[0] == NULL) { return; } if (builtin_cmd(arg_vec) == -1) { // run build-in commands exec_cmd(arg_vec); // run normal commands } } 处理信号

我们期望shell能够处理信号。在shell的命令行中敲命令运行程序,在程序还在运行时按下Ctrl-CCtrl-\键(分别产生SIGINTSIGQUIT信号),运行的程序会退出,即中断运行子进程;在shell的命令行中不运行命令,直接按下这两个键组合,不能退出shell程序,即父进程不会中断运行,退出shell程序只能通过Ctrl-D按键。实现上述功能需要在父进程中忽略信号SIGINTSIGQUIT,但是在子进程中恢复对信号SIGINTSIGQUIT的默认操作。代码很简单,在这里不列出。

这样,一个具备基本功能的shell就完成了,完整的代码请参考这里。请继续看下一篇:Shell实现:重定向和管道。

参考
  1. The Linux Programming Interface
  2. Unix/Linux编程实践教程